Skip to main content

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:

DirectoryHostsConnects 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 hostroot
amergin/Amerginansible-control (from your SSH config)
secops/Secopsansible-control
vaultwarden/Vaultwardenansible-control
puca/Pucaansible-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:

TagMeaning
setupFirst-time setup: install packages, Docker and so on.
updateSafe to re-run on a live server. Re-applies config that can change (Alloy config, nginx sites, .env files, container definitions).
appsservices-user and services-staff only: deploy the app stacks, and nothing else.
wazuh-agentInstall and enroll the Wazuh agent. Not part of setup or update; run through the enroll-wazuh-* targets.
service-specificgitea-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:

  1. Docker CE from Docker's apt repository, with 10.0.0.12:5000 listed as an insecure registry in /etc/docker/daemon.json. Writing that file restarts Docker, which happens on every setup or update run.
  2. A deploy user in the docker group, authorized with the CI runners' SSH keys. The keys are fetched live from docker-build. CI can then deploy with a plain ssh deploy@<vm> "docker compose pull && docker compose up -d", without storing server credentials in CI.
  3. A registry login for root and deploy, using Docker Registry Password.
  4. Grafana Alloy as a standalone container (grafana/alloy:latest, host networking), shipping logs and metrics to Secops. See Monitoring.
  5. 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 .env files. The playbook itself runs bw 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 run eval "$(make unlock-bw)" first. These are the notes used:
NoteWritten 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 .envGitea's mail settings on Secops
users.jsonGrafana 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.yml
  • amergin/setup-batch-recognition.yml
  • amergin/setup-fotheidil-api.yml
  • amergin/setup-streaming-recognition.yml
  • amergin/setup-fotheidil-recognition.yml
  • amergin/setup-synthesis.yml

Each one:

  1. checks whether its directory under /opt/ is already a Gitea checkout, and deletes it only if it isn't;
  2. clones or updates the repo from Gitea's main branch (https://gitea.abair.ie/ABAIR/<repo>.git), using the read-only bot token Gitea Automation Bot Token;
  3. rebuilds the image and recreates the container only if main has 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.