Files
homelab/ansible/roles/semaphore
Hermes Agent service account 009f244739 feat(semaphore): add config-as-code via Semaphore REST API
Adds an idempotent configuration pass that drives a freshly-deployed
Semaphore instance into its desired state via the REST API. Declared
in group_vars/all/semaphore.yml, applied by tasks/configure.yml,
toggled by semaphore_configure feature flag (default off).

Object types managed:
  - Project (mk-labs)
  - Keys (ansible-vault-pass, gitea-deploy, jarvis-ssh)
  - Repositories (homelab on gitea)
  - Inventories (production -> ansible/inventory.yml in homelab repo)
  - Environments (default with ANSIBLE_HOST_KEY_CHECKING=False)
  - Templates (day0_linux_baseline + variants, day1_deploy_semaphore)
    with survey vars for runtime parameters

Each object found-or-created by name; existing ones never modified.
no_log on token-bearing calls to keep secrets out of stdout.

Inputs (already in vault):
  vault_semaphore_api_token
  vault_jarvis_ssh_private_key
  vault_gitea_deploy_key
  vault_ansible_vault_password
2026-05-29 22:44:15 -05:00
..

semaphore

Deploys SemaphoreUI with a PostgreSQL backend on the mk-labs imagineering VM (figment) via rootful Podman Quadlets.

Architecture

                                 ┌─────────────────────┐
   user ─── HTTPS ─── Traefik ──▶│  figment (10.1.71.37) │
                                 │                       │
                                 │  ┌─────────────────┐  │
                                 │  │ semaphore       │  │
                                 │  │ :3000           │──┼──┐
                                 │  └─────────────────┘  │  │ semaphore-net
                                 │                       │  │ (podman)
                                 │  ┌─────────────────┐  │  │
                                 │  │ semaphore-      │◀─┼──┘
                                 │  │ postgres :5432  │  │
                                 │  └─────────────────┘  │
                                 │                       │
                                 │  /etc/containers/     │
                                 │    systemd/*.container│
                                 └───────────────────────┘
  • Both containers run as rootful Podman services, managed by systemd-generated units from Quadlet files in /etc/containers/systemd/.
  • The two containers share a user-defined Podman network (semaphore-net) so Semaphore can address PostgreSQL by container name.
  • Data persists on two named Podman volumes (semaphore_data, semaphore_postgres_data) so container recreation is non-destructive.
  • Image tags are pinned — no floating :latest.

Required vault variables

Variable Purpose
vault_semaphore_database_password PostgreSQL role password for the semaphore DB user.
vault_semaphore_admin_password Initial admin password (used during manual user-add).
vault_semaphore_access_key_encryption 32-byte key for Semaphore-stored access keys.

Generate the access-key encryption value with:

head -c 32 /dev/urandom | base64

Usage

- name: Deploy SemaphoreUI
  hosts: semaphore_server
  become: true
  roles:
    - semaphore

Or via the dedicated playbook:

ansible-playbook -i inventory.yml playbooks/day1_deploy_semaphore.yml

First-run admin user

The container provisions the initial admin user from the SEMAPHORE_ADMIN_* environment variables on first boot. Credentials:

Field Value
Login admin
Name Administrator
Email admin@local.mk-labs.cloud
Password vault_semaphore_admin_password (in vault)

The admin env vars are only consulted on first boot when no admin exists in the database. Subsequent password rotations should be done through the web UI, not by re-running this role.

Pinned versions

  • Semaphore: docker.io/semaphoreui/semaphore:v2.18.5-ansible2.16.5 (bundles Ansible 2.16.5 inside the container, matching our controller)
  • PostgreSQL: docker.io/library/postgres:16-alpine

To bump versions, update semaphore_image / semaphore_postgres_image in defaults/main.yml. Test against figment, snapshot first.

Verification

The role completes with an HTTP health check against http://127.0.0.1:3000/api/ping. If Semaphore doesn't respond within ~60 seconds, the role fails loudly rather than reporting green-but-broken.

Troubleshooting

# Container status
sudo systemctl status semaphore semaphore-postgres
sudo podman ps -a

# Logs
sudo journalctl -u semaphore -f
sudo journalctl -u semaphore-postgres -f
sudo podman logs semaphore --tail 100

# Network
sudo podman network inspect semaphore-net

# Wipe and redeploy (destructive — destroys all Semaphore data)
sudo systemctl stop semaphore semaphore-postgres
sudo podman volume rm semaphore_data semaphore_postgres_data
sudo rm /etc/containers/systemd/semaphore.container /etc/containers/systemd/semaphore-postgres.container
sudo systemctl daemon-reload
# then re-run the playbook