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:

s}emaprheovreer.syeo_uprrdooxmyailno.ccaolmho{st:3000

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:

  1. Key Store → Add your SSH key (the one that can access your servers)
  2. 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
  1. 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
  1. 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:

  1. Open a Task Template
  2. Click Schedules tab
  3. Add a schedule with cron syntax

Recommended schedules:

TaskScheduleCron Expression
System updatesWeekly, Sunday 3 AM0 3 * * 0
Docker cleanupWeekly, Saturday 2 AM0 2 * * 6
SSL cert checkDaily, 9 AM0 9 * * *
Backup verificationDaily, 7 AM0 7 * * *
Docker stack updatesWeekly, Wednesday 4 AM0 4 * * 3

Notifications

Set up alerts so you know when tasks fail:

Telegram

  1. Create a bot via @BotFather
  2. In Semaphore: Settings → Integrations → Telegram
  3. Enter your bot token and chat ID

Slack/Discord

Use webhook URLs in the integration settings.

Email

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

FeatureSemaphoreAWX/TowerRundeckJenkins
Ansible-native⚠️ Plugin⚠️ Plugin
Resource usage~50MB RAM4GB+ RAM1GB+ RAM1GB+ RAM
Setup complexityEasyComplexMediumComplex
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.1 and 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: