Install Semaphore on Ubuntu for a Web-Based Ansible UI

Written by: Bagus Facsi Aginsa
Published at: 11 May 2026


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-playbook with 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.10 as 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-playbook binary 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 StoreNew 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:

  1. Set Key Name to something like ansible-ssh-key.
  2. Set Type to SSH Key.
  3. Paste the contents of the private key (e.g., ~/.ssh/id_ed25519) into the Private Key field.
  4. 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 RepositoriesNew Repository.

Fill in:

  • Name: my-playbooks
  • URL: your Git repository URL, e.g., [email protected]:youruser/ansible-playbooks.git for SSH, or https://github.com/youruser/ansible-playbooks.git for HTTPS.
  • Branch: main
  • Access Key: select the key you created for repository access, or None for a public repo.

Save. Semaphore does not clone it immediately — it pulls when a task runs.


Step 9: Define an Inventory

Click InventoryNew Inventory.

  • Name: webservers
  • User Credentials: select your ansible-ssh-key from 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 TemplatesNew 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-playbook automatically.
  • 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.