Files
config-public/server
Lucas WintherandClaude Opus 5.5 665ac7d52f Render stack secrets from OpenBao in compose_stack
stack_secrets reads KV v2 on the target host and writes the stack's .env at
0600, failing loudly on a sealed vault, a bad token, a missing field or an
unsafe value. Adds an example OpenBao stack on app and moves webapp onto it;
metrics keeps a hand-placed .env.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 15:15:04 +02:00
..
2026-09-16 03:58:01 +02:00

server/ — the payloads

Everything under this directory is data. These files are copied to the hosts verbatim: no templating, no variable substitution by Ansible, no generation.

server/<host>/<stack>/     a stack that runs on exactly one host
server/shared/<stack>/     a payload used by more than one host or instance
server/shared/components/  single files that belong to another stack's directory

The directory names under server/ must match the inventory host names, because the role resolves a payload as server/{{ inventory_hostname }}/{{ stack_name }} unless a playbook overrides stack_src.

server/ is excluded from ansible-lint and yamllint. A compose file is not Ansible content, and most of these come from upstream projects that format them their own way — linting them produces noise and pressure to reformat files you want to be able to diff against upstream.

The examples

All seven are fictional. example.com is reserved by RFC 2606 and can never resolve to a real service. Replace them with your own; the framework around them is the reusable part.

Payload Used by Notes
edge/reverse-proxy/ reverse-proxy.yml nginx-proxy + acme-companion. Owns proxy-net. Its proxy-data/ subdirectories are bind mounts holding plain config files.
app/static-site/ static-site.yml One container, one network, no state. The simplest thing that works.
app/webapp/ webapp.yml, webapp-staging.yml Postgres plus an app built from app/Dockerfile. A second compose file holds maintenance-only services. Every credential comes from OpenBao, via stack_secrets.
app/openbao/ openbao.yml The vault, on 127.0.0.1:8200 only. config/openbao.hcl is bind-mounted; a change needs a manual restart and unseal.
shared/metrics/ metrics.yml Prometheus and Grafana, identical on both hosts, so it lives here rather than being duplicated per host. Its .env is placed on each host by hand.
shared/components/banner.html banner.yml A single file that belongs to the reverse proxy's directory on every host.

Conventions worth copying

Take secrets from .env, and require them. Every credential in these payloads is written as ${VAR:?message}. Compose then refuses to start and names the missing variable, instead of expanding it to an empty string and starting the service with no password. The .env itself is never synced: the role renders it on the host from OpenBao for a stack that sets stack_secrets, and otherwise leaves one placed by hand alone. Ship a .env.example listing the names with no values as documentation. See docs/secrets.md.

Pin image tags. nginx:1.27-alpine, not nginx:latest. A latest tag means a plain deploy will not pick up a new build (Compose keeps the image it has) and an update run picks up whatever was published this morning. Both are surprises.

Declare shared networks external. A network two stacks need is created by the playbook through stack_networks, not by whichever compose file happened to run first. That is what lets every playbook stand on its own.

Do not put host paths in the compose file. Use ${PWD} if a sibling container needs one — the role exports it as the stack's directory on the host, which is the only value that is correct from inside a deploy.