Ansible is the gold standard for server automation. But writing YAML playbooks and running them from the terminal gets old fast — especially when you manage multiple machines in your homelab.
Semaphore is a modern web UI for Ansible. It lets you manage playbooks, track run history, schedule tasks, and share automation with your team — all from a clean browser interface. Think of it as “Ansible meets GitHub Actions” for your home server.
Why Semaphore?
- Web-based UI — run playbooks without SSH-ing into anything
- Run history — see what ran, when, and whether it succeeded
- Scheduling — cron-like automation for recurring tasks
- Multi-project — organize playbooks by server or function
- Team access — share automation without sharing SSH keys
- Notifications — Slack, Telegram, email alerts on task completion
- Lightweight — single Go binary, runs on a Raspberry Pi
If you’ve ever thought “I should automate this” but didn’t want to deal with Ansible’s CLI, Semaphore is for you.
Prerequisites
- Docker and Docker Compose installed (Docker setup guide)
- Basic understanding of what Ansible does (we’ll cover the essentials)
- At least one server you want to automate (can be the same machine)
Installation with Docker Compose
Step 1: Create the project directory
mkdir -p ~/semaphore && cd ~/semaphore
Step 2: Create the Docker Compose file
# docker-compose.yml
services:
semaphore:
image: semaphoreui/semaphore:latest
container_name: semaphore
ports:
- "127.0.0.1:3000:3000"
environment:
SEMAPHORE_DB_DIALECT: bolt
SEMAPHORE_ADMIN_PASSWORD: changeme-to-something-secure
SEMAPHORE_ADMIN_NAME: admin
SEMAPHORE_ADMIN_EMAIL: admin@localhost
SEMAPHORE_ADMIN: admin
SEMAPHORE_ACCESS_KEY_ENCRYPTION: "generate-a-random-32-char-string"
volumes:
- ./data:/home/semaphore
- ./config:/etc/semaphore
restart: unless-stopped
Using BoltDB keeps things simple — no separate database container needed. For larger installations, Semaphore also supports MySQL and PostgreSQL.
Step 3: Generate the encryption key
# Generate a random encryption key
openssl rand -base64 24
Copy the output and replace generate-a-random-32-char-string in the compose file.
Step 4: Start Semaphore
docker compose up -d
Step 5: Access the UI
Open http://your-server-ip:3000 and log in with the admin credentials you set.
Setting Up HTTPS
For remote access, put Semaphore behind a reverse proxy. With Caddy:
See our reverse proxy comparison for other options.
Core Concepts
Before creating your first automation, understand Semaphore’s building blocks:
Key Store
SSH keys, passwords, and API tokens. Semaphore encrypts these at rest.
Repositories
Git repositories containing your Ansible playbooks. Can be local paths or remote Git URLs.
Inventory
Lists of servers to run playbooks against. Supports static files and dynamic inventories.
Environment
Variables passed to playbooks — like target versions, feature flags, or config values.
Task Templates
Combinations of repository + inventory + environment + playbook that form a runnable task.
Your First Automation: System Updates
Let’s create a playbook that updates all your servers.
Step 1: Create a local playbook repository
mkdir -p ~/ansible-playbooks
Create the update playbook:
# ~/ansible-playbooks/update-servers.yml
---
- name: Update all servers
hosts: all
become: true
tasks:
- name: Update apt cache
apt:
update_cache: yes
cache_valid_time: 3600
when: ansible_os_family == "Debian"
- name: Upgrade all packages
apt:
upgrade: dist
autoremove: yes
when: ansible_os_family == "Debian"
- name: Check if reboot is required
stat:
path: /var/run/reboot-required
register: reboot_required
- name: Report reboot status
debug:
msg: "⚠️ Reboot required on {{ inventory_hostname }}"
when: reboot_required.stat.exists
Step 2: Configure Semaphore
In the Semaphore web UI:
- Key Store → Add your SSH key (the one that can access your servers)
- Repositories → Add a local repository pointing to
/home/semaphore/playbooks- Mount the playbooks directory in Docker:
volumes:
- ./data:/home/semaphore
- ./config:/etc/semaphore
- ~/ansible-playbooks:/home/semaphore/playbooks # Add this
- Inventory → Create a static inventory:
[webservers]
web1.local ansible_host=192.168.1.10
web2.local ansible_host=192.168.1.11
[databases]
db1.local ansible_host=192.168.1.20
[all:vars]
ansible_user=your-ssh-user
ansible_python_interpreter=/usr/bin/python3
- Task Templates → Create a template:
- Name: “Update All Servers”
- Repository: your local repo
- Playbook:
update-servers.yml - Inventory: the one you created
- SSH Key: the one from Key Store
Step 3: Run it
Click Run on the task template. Watch the output in real-time in the browser.
Useful Playbooks for Self-Hosters
Docker Cleanup
# docker-cleanup.yml
---
- name: Clean up Docker resources
hosts: all
become: true
tasks:
- name: Remove unused Docker images
command: docker image prune -af
register: image_prune
- name: Remove unused volumes
command: docker volume prune -f
register: volume_prune
- name: Remove unused networks
command: docker network prune -f
- name: Show space reclaimed
debug:
msg: "Reclaimed: {{ image_prune.stdout_lines | last }}"
SSL Certificate Check
# check-ssl.yml
---
- name: Check SSL certificate expiry
hosts: localhost
connection: local
vars:
domains:
- yourdomain.com
- immich.yourdomain.com
- git.yourdomain.com
tasks:
- name: Check certificate expiry
command: >
openssl s_client -connect {{ item }}:443 -servername {{ item }}
2>/dev/null | openssl x509 -noout -dates
loop: "{{ domains }}"
register: cert_check
ignore_errors: true
- name: Display results
debug:
msg: "{{ item.item }}: {{ item.stdout }}"
loop: "{{ cert_check.results }}"
Docker Compose Update
# update-stacks.yml
---
- name: Update Docker Compose stacks
hosts: all
become: true
vars:
compose_dirs:
- /opt/immich
- /opt/nextcloud
- /opt/traefik
tasks:
- name: Pull latest images
command: docker compose pull
args:
chdir: "{{ item }}"
loop: "{{ compose_dirs }}"
- name: Restart with new images
command: docker compose up -d
args:
chdir: "{{ item }}"
loop: "{{ compose_dirs }}"
Backup Verification
# verify-backups.yml
---
- name: Verify backup health
hosts: all
become: true
tasks:
- name: Check restic backup age
shell: |
restic -r /mnt/backup/{{ inventory_hostname }} snapshots --latest 1 --json |
python3 -c "import sys,json; s=json.load(sys.stdin); print(s[0]['time'][:10])"
register: last_backup
ignore_errors: true
- name: Alert if backup is stale
debug:
msg: "⚠️ Last backup on {{ inventory_hostname }}: {{ last_backup.stdout | default('NONE') }}"
when: last_backup.rc != 0 or last_backup.stdout < (ansible_date_time.date | string)
Scheduling Tasks
Semaphore supports cron-like scheduling:
- Open a Task Template
- Click Schedules tab
- Add a schedule with cron syntax
Recommended schedules:
| Task | Schedule | Cron Expression |
|---|---|---|
| System updates | Weekly, Sunday 3 AM | 0 3 * * 0 |
| Docker cleanup | Weekly, Saturday 2 AM | 0 2 * * 6 |
| SSL cert check | Daily, 9 AM | 0 9 * * * |
| Backup verification | Daily, 7 AM | 0 7 * * * |
| Docker stack updates | Weekly, Wednesday 4 AM | 0 4 * * 3 |
Notifications
Set up alerts so you know when tasks fail:
Telegram
- Create a bot via @BotFather
- In Semaphore: Settings → Integrations → Telegram
- Enter your bot token and chat ID
Slack/Discord
Use webhook URLs in the integration settings.
Configure SMTP in the Semaphore environment:
environment:
SEMAPHORE_TELEGRAM_ALERT: "true"
SEMAPHORE_TELEGRAM_CHAT: "your-chat-id"
SEMAPHORE_TELEGRAM_TOKEN: "your-bot-token"
Semaphore vs Alternatives
| Feature | Semaphore | AWX/Tower | Rundeck | Jenkins |
|---|---|---|---|---|
| Ansible-native | ✅ | ✅ | ⚠️ Plugin | ⚠️ Plugin |
| Resource usage | ~50MB RAM | 4GB+ RAM | 1GB+ RAM | 1GB+ RAM |
| Setup complexity | Easy | Complex | Medium | Complex |
| Web UI | ✅ Clean | ✅ Enterprise | ✅ | ✅ |
| Scheduling | ✅ | ✅ | ✅ | ✅ |
| Free/OSS | ✅ | ⚠️ OSS version | ⚠️ OSS version | ✅ |
| Runs on Pi | ✅ | ❌ | ⚠️ | ❌ |
Semaphore wins for homelab use. AWX (the open-source Ansible Tower) is overkill for personal servers — it needs 4GB+ RAM just to start.
Troubleshooting
Playbook fails with “permission denied”:
Make sure your SSH key in the Key Store matches the key authorized on the target server. Check ansible_user in your inventory.
Can’t connect to target server:
Test SSH manually first: ssh -i /path/to/key user@server. If that works, verify the key is correctly imported in Semaphore’s Key Store.
Slow playbook execution:
Add pipelining = True to your ansible.cfg, and enable SSH multiplexing. For large inventories, increase forks.
BoltDB errors after crash:
Stop Semaphore, back up ./data/database.boltdb, and restart. BoltDB occasionally needs recovery after unclean shutdowns.
Security Considerations
- Bind to
127.0.0.1and use a reverse proxy with HTTPS - Use strong admin passwords and the encryption key
- Store SSH keys with passphrases when possible
- Limit Semaphore’s SSH key to only the servers it needs to manage
- Review our Docker security guide for container hardening
Conclusion
Semaphore turns Ansible from a power-user CLI tool into something anyone can use. Schedule your updates, automate your Docker stacks, and verify your backups — all from a browser.
The best part? Once you set it up, your homelab maintains itself. You just check the dashboard occasionally to make sure everything’s green.
Related guides: