Running Ansible from the terminal works fine when it is just you. But the moment you need to give a junior engineer access to run a specific playbook, or you want to schedule a weekly server hardening job, or you want an audit trail of who ran what and when — the command line starts to feel like the wrong tool. You end up either giving people too much SSH access or writing fragile wrapper scripts.
This is the problem that Semaphore solves. It is a free, open-source web UI for Ansible. You point it at your Git repository of playbooks, define your inventories and SSH keys inside the UI, and then run playbooks from a browser — with role-based access control, execution logs, and optional scheduling built in.
Semaphore is the lightweight, self-hosted alternative to AWX (the open-source upstream of Ansible Automation Platform). While AWX requires Kubernetes or Docker Compose to run, Semaphore is a single binary that runs directly on Ubuntu with a plain systemd service. For most small-to-mid-size teams, Semaphore gives you 80% of AWX’s value with 20% of the operational complexity.
In this tutorial, you will install Semaphore on an Ubuntu 22.04 server, connect it to a database, create a systemd service, and walk through setting up a complete project — SSH keys, a Git repository, an inventory, and your first playbook run through the UI.
What Is Semaphore?
Before touching the terminal, it helps to understand how Semaphore organizes things internally, because the mental model is different from running Ansible directly.
Semaphore is built around projects. A project contains everything needed to run a set of playbooks:
- Key Store — SSH private keys and other credentials that Semaphore uses to connect to your managed hosts or pull from private Git repositories. You upload them once, and the UI references them by name from that point forward. Keys are never exposed to users after upload.
- Repositories — Git repository URLs pointing to your Ansible playbook code. Semaphore clones these at run time, so your playbooks are always pulled fresh from the current branch.
- Inventory — the list of hosts Semaphore will pass to Ansible. You can write it as a static file inline or point to a file inside your repository.
- Environment — optional extra variables injected as a JSON object at run time. Useful for passing environment-specific configuration without changing the playbook itself.
- Task Templates — the core concept. A template ties everything together: a repository, a playbook file path, an inventory, an environment, and an SSH key. When you click “Run”, Semaphore clones the repo and executes
ansible-playbookwith all of these configured. Every execution is logged and kept for review.
The flow is: Key Store + Repository → Task Template + Inventory → Execution Log.
Prerequisites
- Ubuntu 22.04 or 24.04 server with at least 1 GB RAM and 10 GB disk. Semaphore itself is lightweight; your database will be the main consumer.
- A domain name or IP address to reach the server. This tutorial uses
192.168.1.10as a placeholder — replace it with your actual IP or hostname. - Ansible installed on the Semaphore server. Semaphore does not bundle Ansible; it calls the
ansible-playbookbinary from the system PATH. - A Git repository containing at least one Ansible playbook. This can be a public GitHub/GitLab repo or a private one — Semaphore supports both with SSH key auth.
- sudo access on the server.
Install Ansible first if you have not already:
sudo apt update
sudo apt install -y software-properties-common
sudo add-apt-repository --yes --update ppa:ansible/ansible
sudo apt install -y ansible git
Verify Ansible is reachable from the system PATH:
which ansible-playbook
It should print /usr/bin/ansible-playbook. Semaphore calls this path at runtime.
Step 1: Install MySQL
Semaphore supports MySQL, PostgreSQL, and BoltDB (an embedded file-based database). BoltDB requires no setup but lacks concurrent write support, so MySQL is the better choice for any multi-user setup. This tutorial uses MySQL.
sudo apt install -y mysql-server
sudo systemctl enable --now mysql
Secure the installation:
sudo mysql_secure_installation
Follow the prompts — set a root password, remove anonymous users, disallow remote root login, and remove the test database. These are the defaults you want in production.
Now create a dedicated database and user for Semaphore:
sudo mysql -u root -p
Inside the MySQL prompt:
CREATE DATABASE semaphore CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'semaphore'@'localhost' IDENTIFIED BY 'StrongPassword123!';
GRANT ALL PRIVILEGES ON semaphore.* TO 'semaphore'@'localhost';
FLUSH PRIVILEGES;
EXIT;
Replace StrongPassword123! with a real password. Write it down — you will need it in the next step.
Step 2: Download and Install Semaphore
Semaphore ships as a prebuilt binary. Check the GitHub releases page for the latest version. At the time of writing, the current stable release is v2.10.x. Adjust the version number in the command below if a newer release is available.
SEMAPHORE_VERSION="2.10.22"
wget "https://github.com/semaphoreui/semaphore/releases/download/v${SEMAPHORE_VERSION}/semaphore_${SEMAPHORE_VERSION}_linux_amd64.deb"
sudo dpkg -i "semaphore_${SEMAPHORE_VERSION}_linux_amd64.deb"
Verify the binary is installed:
semaphore version
You should see output like:
v2.10.22
Step 3: Run the Interactive Setup
Semaphore includes a setup wizard that generates a configuration file. Run it as the user who will own the process — you will create a dedicated system user for this shortly, but the setup itself can be run as your current user to generate the config file first.
semaphore setup
The wizard asks a series of questions. Here is what to enter for a MySQL-backed installation:
What database to use:
1 - MySQL
2 - BoltDB
3 - PostgreSQL
(default 1): 1
db Hostname (default 127.0.0.1:3306): 127.0.0.1:3306
db User (default root): semaphore
db Password: StrongPassword123!
db Name (default semaphore): semaphore
Playbook path (default /tmp/semaphore): /opt/semaphore/tmp
Web root URL (example http://localhost:3000/): http://192.168.1.10:3000/
Enable email alerts? (yes/no) (default no): no
Enable telegram alerts? (yes/no) (default no): no
Enable slack alerts? (yes/no) (default no): no
Enable LDAP authentication? (yes/no) (default no): no
Config output directory (default /home/youruser): /etc/semaphore
For the “Config output directory”, enter /etc/semaphore. Semaphore will create the directory and write config.json there.
At the end, the wizard asks you to create an admin user:
Username: admin
Email: [email protected]
Your name: Admin User
Password: AdminPassword456!
Use a real email and a strong password. This is the account you will log into the web UI with.
The wizard writes the config file and exits. You should see:
Config output directory: /etc/semaphore
Running: semaphore migrate --config /etc/semaphore/config.json
...
Migration complete
The “migrate” step creates all the required database tables.
Step 4: Create a System User and Set Permissions
Running Semaphore as root is a security risk. Create a dedicated system user instead:
sudo useradd --system --shell /bin/false --create-home --home-dir /opt/semaphore semaphore
Give ownership of the config directory and the temp playbook directory to this user:
sudo mkdir -p /opt/semaphore/tmp
sudo chown -R semaphore:semaphore /opt/semaphore
sudo chown -R semaphore:semaphore /etc/semaphore
Step 5: Create a systemd Service
Running Semaphore as a systemd service means it starts automatically on boot and can be managed with standard systemctl commands.
Create the service file:
sudo nano /etc/systemd/system/semaphore.service
Paste the following:
[Unit]
Description=Semaphore
Documentation=https://docs.semaphoreui.com
After=network.target mysql.service
[Service]
Type=simple
User=semaphore
Group=semaphore
ExecStart=/usr/bin/semaphore server --config /etc/semaphore/config.json
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
The After=mysql.service line ensures Semaphore does not try to start before MySQL is up. Restart=on-failure means systemd will restart the process automatically if it crashes.
Enable and start the service:
sudo systemctl daemon-reload
sudo systemctl enable --now semaphore
Check that it started cleanly:
sudo systemctl status semaphore
Look for Active: active (running). If you see an error, check the logs:
sudo journalctl -u semaphore -n 50 --no-pager
Step 6: Open the Web UI
Open a browser and navigate to:
http://192.168.1.10:3000
You should see the Semaphore login page. Log in with the admin credentials you created during setup.
If you cannot reach the UI:
- Confirm Semaphore is listening:
ss -tlnp | grep 3000 - Check whether a firewall is blocking port 3000:
sudo ufw allow 3000/tcp(if you use UFW)
Step 7: Create a Project and Configure the Key Store
Once inside the UI, click New Project in the top-left area. Give it a name like “Production Automation” and save it.
You will land on the project’s dashboard. The left sidebar shows the sections: Key Store, Repositories, Inventory, Environment, Task Templates, and Activity (the execution log).
Start with the Key Store. Click Key Store → New Key.
There are three key types:
- None — no credential, for public repositories or passwordless situations.
- Login with password — a username/password pair.
- SSH Key — paste an SSH private key.
For connecting to managed servers, create an SSH key entry:
- Set Key Name to something like
ansible-ssh-key. - Set Type to
SSH Key. - Paste the contents of the private key (e.g.,
~/.ssh/id_ed25519) into the Private Key field. - Save.
If your Git repository is private, create a second key entry for it — either an SSH key or a login with a personal access token as the password.
Step 8: Add a Repository
Click Repositories → New Repository.
Fill in:
- Name:
my-playbooks - URL: your Git repository URL, e.g.,
[email protected]:youruser/ansible-playbooks.gitfor SSH, orhttps://github.com/youruser/ansible-playbooks.gitfor HTTPS. - Branch:
main - Access Key: select the key you created for repository access, or
Nonefor a public repo.
Save. Semaphore does not clone it immediately — it pulls when a task runs.
Step 9: Define an Inventory
Click Inventory → New Inventory.
- Name:
webservers - User Credentials: select your
ansible-ssh-keyfrom the key store. - Type: choose Static for a manually written inventory.
- Paste your inventory content into the text box:
[webservers]
web1 ansible_host=192.168.1.101
web2 ansible_host=192.168.1.102
[webservers:vars]
ansible_user=ubuntu
If you prefer to keep the inventory file inside your Git repository, choose File as the type and enter the path relative to the repository root (e.g., inventories/production.ini). Semaphore will read it from the cloned repo at run time.
Save the inventory.
Step 10: Create a Task Template and Run a Playbook
Click Task Templates → New Template.
Fill in:
- Name:
Deploy Nginx - Playbook Filename: the path to your playbook relative to the repository root, e.g.,
site.yml - Inventory: select
webservers - Repository: select
my-playbooks - Environment: leave blank for now, or select an environment if you defined one
- Vault Password and Extra CLI Arguments: leave blank unless needed
Save the template.
You will now see the template listed on the Task Templates page. Click the Run button (the play icon) next to it.
A dialog box appears confirming the options. Click Run again.
Semaphore clones the repository, runs ansible-playbook with the configured inventory and SSH key, and streams the output live to the browser — the same output you would see in your terminal, now captured and stored. You can share the execution URL with a colleague and they will see the exact same log.
The Activity section on the sidebar shows all past executions — who triggered them, when, and whether they succeeded or failed.
Common Mistakes and Troubleshooting
“Cannot connect to database” on startup
Double-check the database credentials in /etc/semaphore/config.json. The most common cause is a typo in the password or the wrong database name. Also confirm MySQL is running: sudo systemctl status mysql.
Playbook run fails with “SSH connection timeout”
Semaphore runs ansible-playbook as the semaphore system user. That user does not automatically have access to SSH keys stored in /root/.ssh or /home/yourusername/.ssh. The SSH key must be uploaded to Semaphore’s Key Store — that is the only key material Semaphore passes to Ansible at run time.
“Repository could not be cloned”
For SSH-based Git URLs ([email protected]:...), make sure the SSH key you assigned to the repository has read access. For private GitHub repositories, the easiest approach is to use HTTPS with a Personal Access Token: set the URL to https://github.com/youruser/yourrepo.git and create a Key Store entry with type Login with password, where the username is your GitHub username and the password is the personal access token.
The semaphore user cannot run ansible-playbook
Semaphore relies on the ansible-playbook binary being in /usr/bin or another standard PATH location. If you installed Ansible in a virtualenv, the semaphore system user cannot see it. Install Ansible system-wide via the PPA (as shown in the Prerequisites) rather than in a virtualenv.
Tasks show “changed” every run unexpectedly
This is an Ansible idempotency issue, not a Semaphore issue. Semaphore just runs the playbook you gave it. Review the tasks that keep changing and switch from shell/command modules to purpose-built modules where possible.
Best Practices
Treat Semaphore as a frontend, not a replacement for Git. Keep all your playbooks, roles, inventory files, and variable files in version control. Semaphore clones them fresh on every run. Never edit playbook logic inside Semaphore’s UI — the source of truth is always your Git repository.
Use the RBAC system for team access. In project settings, you can add team members with different roles: Admin, Manager, Task Runner, or Guest. Give engineers the Task Runner role so they can execute pre-approved templates without being able to modify inventories or key store entries.
Enable HTTPS before exposing Semaphore to a non-internal network. By default, Semaphore listens on plain HTTP. For any setup accessible beyond your local network, put Nginx in front of it as a reverse proxy with a TLS certificate from Let’s Encrypt. Configure Nginx to proxy requests to localhost:3000 and update the web_host in config.json to the HTTPS URL.
A minimal Nginx reverse proxy config for Semaphore:
server {
listen 443 ssl;
server_name semaphore.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/semaphore.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/semaphore.yourdomain.com/privkey.pem;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
The Upgrade and Connection headers are required because Semaphore uses WebSockets to stream live task output to the browser.
Schedule recurring tasks using built-in cron. In a Task Template, click the Schedule tab and define a cron expression. Semaphore will run the template automatically on that schedule — useful for things like weekly apt upgrade runs, certificate renewal checks, or configuration drift detection.
Back up the database and config. Semaphore’s state (users, key store, templates, execution history) lives in MySQL. Set up a regular mysqldump semaphore backup. Also back up /etc/semaphore/config.json — it contains the encryption key Semaphore uses to protect stored credentials.
Conclusion
You now have a working Semaphore installation on Ubuntu. Semaphore transforms Ansible from a personal automation tool into a team-accessible service: playbooks run from the browser, execution history is captured automatically, credentials are centrally managed, and access can be granted to teammates without giving them SSH keys or command-line access.
The core workflow you set up — Key Store → Repository → Inventory → Task Template → Run — scales from a single playbook to hundreds of templates across multiple projects. Adding a new server to automate is as simple as adding a new inventory and task template; the underlying playbook code stays in Git where it belongs.
Good next steps from here:
- Notifications: Semaphore supports Slack, Telegram, and email alerts for task success and failure — configure these in the project settings so your team is notified without checking the UI.
- Ansible Vault integration: If your playbooks use Vault-encrypted variables, you can supply the vault password in the task template’s Vault Password field. Semaphore will pass it to
ansible-playbookautomatically. - Environment files: Use Semaphore’s Environment feature to inject extra variables (like environment-specific configuration) as a JSON blob, keeping your playbooks environment-agnostic.
- Multiple projects: Separate staging and production automation into distinct Semaphore projects, each with their own inventories and key stores, to avoid accidental cross-environment runs.
- Self-hosted Git: If you prefer to keep everything internal, pair Semaphore with a self-hosted Gitea or GitLab instance. Semaphore works with any Git remote that supports SSH or HTTPS.