Deploy Semaphore UI on Docker: Web-Based Ansible Management for HomeLab

HomeLab tutorial - IT technology blog
HomeLab tutorial - IT technology blog

Approach Comparison: Managing Ansible at Home

Managing configuration across several bare-metal boxes, virtual machines, and Proxmox nodes usually starts with running ansible-playbook -i inventory.ini site.yml directly from your primary workstation. Running raw CLI commands works well at first.

Soon, though, pain points pile up. Playbook logs vanish into terminal scrollback, scheduled cron jobs lack central visibility, and private keys sit unprotected in local shell configs. Triggering a run from your phone or a secondary laptop is virtually out of the question.

When looking for a central web orchestrator, homelab engineers typically evaluate three paths:

  • Raw CLI & Crontab: Zero overhead and zero setup. However, you sacrifice execution history, multi-user role separation, central secret management, and webhook integration.
  • Red Hat AWX (Open Source Ansible Tower): The enterprise benchmark. AWX provides full-featured automation controller capabilities, but it mandates a Kubernetes cluster (or K3s) and swallows 4GB to 8GB of RAM just sitting idle. On an Intel NUC or mini PC with 16GB of total memory, that consumes an excessive share of system capacity.
  • Semaphore UI: A responsive, fast web dashboard written in Go and Vue.js. Semaphore runs neatly inside standard Docker containers, consumes roughly 80MB to 120MB of RAM, supports PostgreSQL backends, natively handles inventories and vault secrets, and includes built-in cron scheduling with webhooks.

Side-by-Side Comparison

Criteria CLI + Crontab Red Hat AWX Semaphore UI
Memory Footprint Minimal (<50MB during run) Heavy (4GB – 8GB RAM idle) Lightweight (~100MB RAM)
Deployment Target Bare metal / Workstation Kubernetes / K3s / Minikube Docker / Docker Compose
Web Dashboard & Logs None (Terminal output only) Extensive, enterprise-focused Clean, fast, real-time log streaming
RBAC & Credentials Manual file permissions Granular object-level RBAC Team, Project & Key stores
HomeLab Fit Great for quick tests Often resource-prohibitive Optimal balance of features & footprint

Pros and Cons of Semaphore UI

Before launching containers, consider where Semaphore thrives and where its boundaries lie.

Pros

  • Remarkable Resource Efficiency: You can run Semaphore on a 2GB Raspberry Pi 4 or an unprivileged LXC container without noticing memory pressure.
  • Native Ansible Compatibility: Because Semaphore invokes standard Ansible CLI binaries under the hood, your existing playbooks, roles, and collections run without syntax refactoring.
  • Centralized Credential Store: SSH keys, sudo passwords, and Ansible Vault passphrases remain encrypted at rest in PostgreSQL and inject into memory only during job runs.
  • Clean API & Webhooks: Trigger runs automatically via Gitea push hooks or alerts from Prometheus and Alertmanager.
  • Live Log Streaming: Watch task execution in real time through a browser terminal with persistent color-coded logs and execution duration tracking.

Cons

  • No Dynamic Inventory Visualizer: Semaphore executes dynamic inventory scripts pulled from your Git repository, but it does not let you visually map or draw topology graphs inside the UI.
  • Simpler Permissions Matrix: Semaphore isolates users by project, but lacks the granular per-inventory and per-credential permission policies found in AWX.
  • Collection Dependency Management: If playbooks rely on collections like community.docker or ansible.posix, you must bundle them in your repository’s requirements.yml or bake them into a custom container image.

Recommended Setup Architecture

Running Semaphore alongside PostgreSQL using Docker Compose isolates services cleanly and makes daily backups straightforward with standard pg_dump utilities.

Our target deployment includes:

  1. PostgreSQL 16 Engine: While Semaphore supports MySQL and SQLite, PostgreSQL handles migrations cleanly and provides reliable concurrency when dispatching multiple playbooks simultaneously.
  2. Official Semaphore Container: An Alpine-based image packaging Ansible Core, Git, and OpenSSH client tools out of the box.
  3. Isolated Docker Bridge Network: Restricting database port exposure strictly to Semaphore, while routing web traffic through port 3000 to your reverse proxy (such as Traefik, Nginx Proxy Manager, or Caddy) with TLS.
  4. Named Persistent Volumes: Retaining database state, encryption secrets, and cloned playbook caches across container upgrades.

Implementation Guide: Deploying Semaphore on Docker

Step 1: Directory Setup and Environment Secrets

Log in to your Docker host and create a dedicated directory structure for the stack:

mkdir -p ~/semaphore/{config,data/postgres}
cd ~/semaphore

Generate cryptographic keys for cookie signing, database encryption, and PostgreSQL authentication:

COOKIE_HASH=$(openssl rand -hex 32)
ENCRYPTION_KEY=$(openssl rand -base64 32)
DB_PASS=$(openssl rand -hex 16)

cat <<EOF > .env
SEMAPHORE_COOKIE_HASH=${COOKIE_HASH}
SEMAPHORE_ACCESS_KEY_ENCRYPTION=${ENCRYPTION_KEY}
POSTGRES_PASSWORD=${DB_PASS}
EOF

chmod 600 .env
echo ".env generated with random secrets."

Step 2: Create the Docker Compose File

Create a docker-compose.yml file in your project folder:

nano docker-compose.yml

Add the following service configuration, which reads credentials directly from your .env file:

networks:
  semaphore-net:
    driver: bridge

volumes:
  semaphore-postgres:
  semaphore-data:
  semaphore-config:

services:
  postgres:
    image: postgres:16-alpine
    container_name: semaphore-postgres
    restart: unless-stopped
    networks:
      - semaphore-net
    volumes:
      - semaphore-postgres:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: semaphore
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: semaphore
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U semaphore -d semaphore"]
      interval: 5s
      timeout: 5s
      retries: 5

  semaphore:
    image: semaphoreui/semaphore:v2.10.35
    container_name: semaphore-ui
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy
    networks:
      - semaphore-net
    ports:
      - "3000:3000"
    environment:
      SEMAPHORE_DB_USER: semaphore
      SEMAPHORE_DB_PASS: ${POSTGRES_PASSWORD}
      SEMAPHORE_DB_HOST: postgres
      SEMAPHORE_DB_PORT: "5432"
      SEMAPHORE_DB_DIALECT: postgres
      SEMAPHORE_DB: semaphore
      SEMAPHORE_PLAYBOOK_PATH: /tmp/semaphore
      SEMAPHORE_ADMIN_PASSWORD: InitialAdminPassword123!
      SEMAPHORE_ADMIN_NAME: Lab Admin
      SEMAPHORE_ADMIN_EMAIL: [email protected]
      SEMAPHORE_ADMIN: admin
      SEMAPHORE_ACCESS_KEY_ENCRYPTION: ${SEMAPHORE_ACCESS_KEY_ENCRYPTION}
      SEMAPHORE_COOKIE_HASH: ${SEMAPHORE_COOKIE_HASH}
    volumes:
      - semaphore-config:/etc/semaphore
      - semaphore-data:/tmp/semaphore

Step 3: Start Services and Verify Logs

Bring up the stack in detached mode:

docker compose up -d

Follow container initialization to verify database migrations succeed:

docker compose logs -f semaphore

Within 10 to 15 seconds, you will see output confirming readiness:

Running DB Migrations...
Migrations Finished
Admin created
Server is running on port :3000

Step 4: Initial Login and Project Setup

Point your web browser to http://<your-server-ip>:3000. Log in using the bootstrap credentials: username admin and password InitialAdminPassword123!. Immediately navigate to User Profile in the top-right corner and change your admin password to a strong phrase.

Next, click New Project and name it HomeLab Core. Projects act as isolated workspaces where credentials, repositories, and playbooks are scoped.

Step 5: Configuring Key Store, Repositories, and Inventory

Semaphore organizes automation tasks across five primary building blocks:

1. Key Store (Credentials)

Navigate to Key Store > New Key. This stores the private key Ansible uses to access your target hosts:

  • Name: homelab-ssh-key
  • Type: SSH Key
  • Username: ansible (the deployment user configured on target machines)
  • Private Key: Paste the text of your private key (e.g., your ~/.ssh/id_ed25519 file).

2. Repositories

Navigate to Repositories > New Repository. Semaphore clones your playbook repository prior to execution:

  • Name: homelab-playbooks
  • URL: https://github.com/your-username/homelab-infra.git (or your internal Gitea address)
  • Branch: main
  • Access Key: Choose None for public repositories, or select an SSH deploy key from your Key Store for private repositories.

3. Inventory

Navigate to Inventory > New Inventory:

  • Name: lan-servers
  • Type: Static
  • SSH Key: Select homelab-ssh-key
  • Inventory Contents: Enter your host definitions using standard INI or YAML syntax:
[proxmox]
192.168.1.10 hostname=pve-01.lan
192.168.1.11 hostname=pve-02.lan

[docker_hosts]
192.168.1.20 hostname=docker-apps.lan
192.168.1.21 hostname=docker-media.lan

[all:vars]
ansible_python_interpreter=/usr/bin/python3

4. Environment

Navigate to Environment > New Environment. Provide extra runtime variables in JSON format, identical to Ansible’s --extra-vars argument:

{
  "auto_upgrade_packages": true,
  "dns_servers": [
    "192.168.1.1",
    "1.1.1.1"
  ]
}

5. Task Templates

Navigate to Task Templates > New Template:

  • Name: Nightly Package Updates
  • Playbook Filename: playbooks/patch-all.yml (path relative to the Git repository root)
  • Inventory: lan-servers
  • Repository: homelab-playbooks
  • Environment: Select the environment block defined above.

Step 6: Executing and Scheduling Playbooks

Click Run on your newly created task template. A browser-based terminal will stream the execution output line by line, highlighting ok, changed, and failed tasks in color.

Want hands-off maintenance? Open the task template settings, enter a standard cron pattern such as 0 3 * * 0 (every Sunday at 03:00 UTC), and click Save. Semaphore runs the job automatically on schedule and saves complete logs for later review.

Share: