Configuring with Ansible
Ansible configures everything that runs on the servers: packages, Docker, application stacks, firewalls, CI runners and monitoring agents. You normally run it through a make target (see the Makefile Reference), not by calling ansible-playbook yourself.
Layout
Each group of servers has its own directory under ansible/, with its own inventory and ansible.cfg:
| Directory | Hosts | Connects as |
|---|---|---|
hetzner-main/ | the six hetzner-main VMs (plus scealai-vm, for Wazuh only) | ansible-control, through the proxmox-host jump host |
proxmox-host/ | the Proxmox host | root |
amergin/ | Amergin | ansible-control (from your SSH config) |
secops/ | Secops | ansible-control |
vaultwarden/ | Vaultwarden | ansible-control |
puca/ | Puca | ansible-control, with the key ~/.ssh/id_ed25519_ansible |
Shared files:
ansible/wazuh-agent-tasks.yml: the Wazuh agent install and enrollment, imported by every host's playbook.ansible/templates/alloy/: Grafana Alloy config templates.
The inventories use SSH aliases (webserver-vm, amergin, and so on), not IP addresses. Your ~/.ssh/config must define them; see Getting Started. Host-key checking is off in every ansible.cfg.
ansible/hetzner-main/site.yml imports all the hetzner-main VM playbooks in order, with docker-build first. make setup-all and make update-all run it.
Tags
Every playbook uses tags, so one run does only what's needed:
| Tag | Meaning |
|---|---|
setup | First-time setup: install packages, Docker and so on. |
update | Safe to re-run on a live server. Re-applies config that can change (Alloy config, nginx sites, .env files, container definitions). |
apps | services-user and services-staff only: deploy the app stacks, and nothing else. |
wazuh-agent | Install and enroll the Wazuh agent. Not part of setup or update; run through the enroll-wazuh-* targets. |
| service-specific | gitea-runner, docker-registry, analytics-plausible, update-model, bridges, template, vswitch, firewall, backup, lock-root, and so on. Each is described on its server's page. |
The UFW tasks on most hetzner-main VMs are tagged setup only, so an update run doesn't change firewall rules.
The common pattern on a VM
Most playbooks follow the same pattern:
- Docker CE from Docker's apt repository, with
10.0.0.12:5000listed as an insecure registry in/etc/docker/daemon.json. Writing that file restarts Docker, which happens on everysetuporupdaterun. - A
deployuser in thedockergroup, authorized with the CI runners' SSH keys. The keys are fetched live from docker-build. CI can then deploy with a plainssh deploy@<vm> "docker compose pull && docker compose up -d", without storing server credentials in CI. - A registry login for
rootanddeploy, usingDocker Registry Password. - Grafana Alloy as a standalone container (
grafana/alloy:latest, host networking), shipping logs and metrics to Secops. See Monitoring. - UFW: SSH, plus whichever VMs need to reach this one.
Secrets
Secrets are never stored in the repo. There are two ways they reach a playbook:
- As variables. The Makefile fetches a password with
bw get password '<item>'and passes it with-e, for example-e registry_password="$(bw get password 'Docker Registry Password')". - As
.envfiles. The playbook itself runsbw get notes "<name>"on your machine (inside the tooling container) and writes the result to the server. It uses your unlocked session, so you must runeval "$(make unlock-bw)"first. These are the notes used:
| Note | Written to |
|---|---|
<app> .env | /opt/apps/<app>/.env on services-user or services-staff |
analytics-plausible .env | /opt/apps/analytics-plausible/.env on services-staff |
api-gateway .env.secrets | /opt/api-gateway/.env.secrets on api-gateway |
Gitea .env | Gitea's mail settings on Secops |
users.json | Grafana team membership (Secops) and the Amergin testing sandbox's admin list. The Makefile writes it to a temporary file, which is deleted after the run. |
If a note comes back empty (it doesn't exist, or you aren't in its collection), the playbook stops instead of deploying an empty file.
Playbooks that build from Gitea source
The Amergin service playbooks build each app from source on the server. They clone it from Gitea, never from your machine, so you can deploy from any machine that can run make:
amergin/setup-predictive-text.ymlamergin/setup-batch-recognition.ymlamergin/setup-fotheidil-api.ymlamergin/setup-streaming-recognition.ymlamergin/setup-fotheidil-recognition.ymlamergin/setup-synthesis.yml
Each one:
- checks whether its directory under
/opt/is already a Gitea checkout, and deletes it only if it isn't; - clones or updates the repo from Gitea's
mainbranch (https://gitea.abair.ie/ABAIR/<repo>.git), using the read-only bot tokenGitea Automation Bot Token; - rebuilds the image and recreates the container only if
mainhas new commits, or the container isn't running.
So re-running a target when nothing has changed reports changed=0 and doesn't restart anything. To deploy a code change, merge it into main on Gitea, then run the service's make update-… target.
Models come from Gitea too. See Amergin.
hetzner-main/setup-synthesis-legacy.yml is the one exception: its source repos aren't on Gitea yet. See Synthesis Legacy.