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.dockeroransible.posix, you must bundle them in your repository’srequirements.ymlor 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:
- PostgreSQL 16 Engine: While Semaphore supports MySQL and SQLite, PostgreSQL handles migrations cleanly and provides reliable concurrency when dispatching multiple playbooks simultaneously.
- Official Semaphore Container: An Alpine-based image packaging Ansible Core, Git, and OpenSSH client tools out of the box.
- 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.
- 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_ed25519file).
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
Nonefor 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.

