Ansible

Run Semaphore Ansible Web UI in Docker with PostgreSQL

Managing Ansible playbooks from the command line works fine until you have a dozen playbooks, multiple environments, and a team that needs visibility into what ran, when, and whether it succeeded. Semaphore gives you a clean web UI for all of that, and running it in Docker keeps the setup minimal.

Original content from computingforgeeks.com - post 117729

Semaphore is an open-source alternative to Ansible AWX (the upstream of Red Hat’s Ansible Automation Platform). It handles playbook execution, scheduling, inventory management, and access control through a browser. Written in Go, it’s lightweight compared to AWX and supports MySQL, PostgreSQL, and BoltDB as backends. This guide walks through deploying Semaphore with Docker Compose using PostgreSQL on Rocky Linux 10 and Ubuntu 24.04.

Tested April 2026 on Rocky Linux 10.1 (SELinux enforcing) with Docker 29.3.1, Docker Compose v5.1.1, Semaphore v2.17.33, PostgreSQL 17

Prerequisites

Verify Docker and Compose are installed and running:

docker --version
docker compose version

Expected output on Rocky Linux 10:

Docker version 29.3.1, build c2be9cc
Docker Compose version v5.1.1

If Docker is not running yet, enable and start it:

sudo systemctl enable --now docker

Create the Docker Compose Configuration

Create a dedicated directory for Semaphore and the Compose file. This setup uses PostgreSQL 17 as the database backend, which is the recommended choice for production workloads.

sudo mkdir -p /opt/semaphore
sudo vi /opt/semaphore/docker-compose.yml

Add the following configuration:

services:
  postgres:
    image: postgres:17
    container_name: semaphore-db
    hostname: postgres
    restart: unless-stopped
    environment:
      POSTGRES_USER: semaphore
      POSTGRES_PASSWORD: YourDbPassword
      POSTGRES_DB: semaphore
    volumes:
      - semaphore-postgres:/var/lib/postgresql/data

  semaphore:
    image: semaphoreui/semaphore:v2.17.33
    container_name: semaphore
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      SEMAPHORE_DB_USER: semaphore
      SEMAPHORE_DB_PASS: YourDbPassword
      SEMAPHORE_DB_HOST: postgres
      SEMAPHORE_DB_PORT: 5432
      SEMAPHORE_DB_DIALECT: postgres
      SEMAPHORE_DB: semaphore
      SEMAPHORE_PLAYBOOK_PATH: /tmp/semaphore/
      SEMAPHORE_ADMIN_PASSWORD: YourAdminPassword
      SEMAPHORE_ADMIN_NAME: admin
      SEMAPHORE_ADMIN_EMAIL: [email protected]
      SEMAPHORE_ADMIN: admin
      SEMAPHORE_ACCESS_KEY_ENCRYPTION: CHANGE_ME
    depends_on:
      - postgres

volumes:
  semaphore-postgres:

A few things to note about this configuration:

  • SEMAPHORE_DB_DIALECT must be set to postgres when using PostgreSQL. The other supported values are mysql and bolt
  • SEMAPHORE_ACCESS_KEY_ENCRYPTION encrypts access keys stored in the database. Generate a unique key for your deployment
  • The PostgreSQL port is not exposed to the host (no ports mapping on the postgres service) because only Semaphore needs access to it through Docker’s internal network

Generate the encryption key before starting the containers:

head -c32 /dev/urandom | base64

Copy the output and replace CHANGE_ME in the Compose file with the generated key.

Start Semaphore Containers

Launch the stack from the Semaphore directory:

cd /opt/semaphore
sudo docker compose up -d

Docker pulls the PostgreSQL 17 and Semaphore v2.17.33 images, then starts both containers:

[+] Running 3/3
 ✔ Network semaphore_default  Created                                0.3s
 ✔ Container semaphore-db     Started                                1.6s
 ✔ Container semaphore        Started                                1.5s

Verify both containers are running:

sudo docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}"

Both containers should show as “Up”:

NAMES          IMAGE                            STATUS         PORTS
semaphore      semaphoreui/semaphore:v2.17.33   Up 6 seconds   0.0.0.0:3000->3000/tcp, [::]:3000->3000/tcp
semaphore-db   postgres:17                      Up 6 seconds   5432/tcp

Check the Semaphore logs to confirm the server started successfully:

sudo docker logs semaphore

The output confirms the server is running on port 3000:

Postgres semaphore@postgres:5432 semaphore
Tmp Path (projects home) /tmp/semaphore
Semaphore v2.17.33
Port :3000
Server is running

Configure the Firewall

Open the required ports in the firewall. Port 3000 is for direct access (useful for initial testing), while ports 80 and 443 are for the Nginx reverse proxy with SSL.

Rocky Linux / AlmaLinux (firewalld):

sudo firewall-cmd --add-port={80/tcp,443/tcp,3000/tcp} --permanent
sudo firewall-cmd --reload

Ubuntu / Debian (ufw):

sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 3000/tcp
sudo ufw reload

Access the Semaphore Web UI

Open your browser and navigate to http://10.0.1.50:3000 (replace with your server’s IP). The Semaphore login page appears:

Semaphore Ansible UI Docker login page

Log in with the admin credentials you set in the Compose file (admin / your password). After the first login, Semaphore prompts you to create a project. Projects are how you organize playbooks, inventories, and credentials.

Semaphore Ansible UI project dashboard with sidebar navigation

The project dashboard gives you access to Task Templates, Schedule, Inventory, Variable Groups, Key Store, Repositories, Integrations, and Team management from the sidebar.

Using Semaphore: Keys, Repos, Inventory, and Playbooks

Before running playbooks, Semaphore needs four things configured: credentials (Key Store), a Git repository containing your playbooks, an inventory of target hosts, and a task template that ties them together.

Add Credentials in Key Store

Navigate to Key Store in the sidebar and click NEW KEY. Semaphore supports several key types: SSH keys for connecting to remote hosts, login/password pairs for API or database credentials, and “None” for local connections or public repositories that don’t require authentication.

Semaphore key store for managing SSH keys and credentials

For SSH key authentication, paste your private key directly. Semaphore stores it encrypted using the SEMAPHORE_ACCESS_KEY_ENCRYPTION key you configured in the Compose file.

Add a Playbook Repository

Go to Repositories and click NEW REPOSITORY. Point it at a Git repository containing your Ansible playbooks. This can be a GitHub/GitLab URL for remote repos, or a local path inside the container if you mount a volume.

Semaphore repositories page showing Git repository configuration

Select the appropriate key from the Key Store for repository access. Public repositories can use the “None” key type.

Define Your Inventory

Under Inventory, click NEW INVENTORY to define your target hosts. Semaphore supports static inventory (inline host definitions), static YAML files from your repository, or Terraform state as inventory source.

Semaphore inventory page with host groups

Create Task Templates

Task Templates are where everything comes together. Go to Task Templates and click NEW TEMPLATE. Each template combines a playbook file from your repository, an inventory, an environment (variables), and optional settings like the Ansible vault password or extra CLI arguments.

Semaphore task templates with playbook configurations

The templates list shows each template’s status (last run result), the playbook file it uses, and the assigned inventory. From here you can run, schedule, or edit any template.

Run a Playbook

Click RUN on any template to execute it. Semaphore opens a dialog where you can add an optional message (useful for tracking why a run was triggered) and choose between a dry run or a diff-only check.

Semaphore new task run dialog with options

After clicking RUN, Semaphore clones the repository, installs any Galaxy requirements, and executes the playbook. The output streams in real time:

Semaphore task output showing Ansible playbook execution results

The task output shows the full Ansible output including the PLAY RECAP with host-level results. All task history is retained on the Dashboard, where you can review past runs, filter by status, and track who triggered each execution.

Semaphore Docker dashboard showing task execution history

Set Up Nginx Reverse Proxy with SSL

Running Semaphore behind Nginx with SSL is essential for production. Install Nginx on the host:

Rocky Linux / AlmaLinux:

sudo dnf install -y nginx

Ubuntu / Debian:

sudo apt install -y nginx

Create the Nginx virtual host configuration:

sudo vi /etc/nginx/conf.d/semaphore.conf

Add the following server block. Replace semaphore.example.com with your actual domain:

upstream semaphore {
    server 127.0.0.1:3000;
}

server {
    listen 80;
    server_name semaphore.example.com;

    location / {
        proxy_pass http://semaphore/;
        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_buffering off;
        proxy_request_buffering off;
    }

    location /api/ws {
        proxy_pass http://semaphore/api/ws;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Origin "";
    }
}

The WebSocket configuration under /api/ws is important. Semaphore uses WebSockets for real-time task output streaming. Without it, you won’t see live playbook output in the browser.

Test and start Nginx:

sudo nginx -t
sudo systemctl enable --now nginx

SELinux (Rocky / AlmaLinux only): Allow Nginx to connect to the upstream Semaphore port:

sudo setsebool -P httpd_can_network_connect 1

Obtain a Let’s Encrypt SSL Certificate

Install certbot with the Nginx plugin:

Rocky Linux / AlmaLinux:

sudo dnf install -y epel-release
sudo dnf install -y certbot python3-certbot-nginx

Ubuntu / Debian:

sudo apt install -y certbot python3-certbot-nginx

Request the certificate. Certbot automatically updates the Nginx configuration with SSL settings:

sudo certbot --nginx -d semaphore.example.com --non-interactive --agree-tos -m [email protected]

After certbot finishes, your Nginx config will include the SSL certificate paths and a redirect from HTTP to HTTPS. Verify the certificate renewal timer is active:

sudo certbot renew --dry-run

You can now access Semaphore at https://semaphore.example.com with a valid certificate.

Container Management

Docker Compose handles container lifecycle. All commands should be run from /opt/semaphore:

Stop the stack:

sudo docker compose down

Start it again:

sudo docker compose up -d

View logs from both services:

sudo docker compose logs -f

To upgrade Semaphore to a newer version, update the image tag in docker-compose.yml and recreate the container:

sudo docker compose pull
sudo docker compose up -d

The PostgreSQL data persists in a Docker volume (semaphore-postgres), so upgrades preserve your projects, templates, and run history.

Backup and Restore

Back up the PostgreSQL database while the containers are running:

sudo docker exec semaphore-db pg_dump -U semaphore semaphore > semaphore_backup_$(date +%Y%m%d).sql

Restore from a backup:

cat semaphore_backup_20260405.sql | sudo docker exec -i semaphore-db psql -U semaphore semaphore

Troubleshooting

Docker fails to start on Rocky Linux 10: “missing kernel module”

If Docker fails with an error about iptables or missing kernel modules like xt_addrtype, install the kernel modules extra package:

sudo dnf install -y kernel-modules-extra
sudo modprobe br_netfilter
sudo systemctl restart docker

This is common on minimal Rocky Linux 10 installations where the base kernel package doesn’t include all networking modules Docker needs. The error message typically includes Extension addrtype revision 0 not supported.

Semaphore container exits immediately

Check the logs for database connection errors:

sudo docker compose logs semaphore

Common causes: the SEMAPHORE_DB_DIALECT is missing or wrong (must be postgres for PostgreSQL), the database password doesn’t match between the postgres and semaphore services, or the postgres container hasn’t finished initializing. Restart with sudo docker compose restart semaphore if the database needed more time.

Nginx returns 502 Bad Gateway

On Rocky Linux and AlmaLinux with SELinux enforcing, Nginx cannot connect to backend services by default. Run sudo setsebool -P httpd_can_network_connect 1 and reload Nginx. Check ausearch -m avc -ts recent to confirm SELinux was blocking the connection.

Related Articles

Ansible Ansible Commands and Playbook Cheat Sheet Automation Separate Jenkinsfiles from Sources and Prevent unwarranted editing Containers Install Kubernetes on Alpine Linux with k3s Containers Run FreeIPA Server in Docker or Podman Containers

Leave a Comment

Press ESC to close