Files
Lucas WintherandClaude Opus 5 e1f152ec3e
lint / yamllint + ansible-lint + syntax (push) Successful in 2m52s
lint / shellcheck (push) Successful in 7s
lint / secret scan (push) Failing after 3s
Add the documentation and the agent instructions
README for humans, AGENTS.md for agents and contributors, and the docs that
outlive any one platform: architecture, secrets, connectivity, triggering a
deploy from another repository. CLAUDE.md, GEMINI.md and .claude/skills/ are
pointers rather than copies, so every agent and every human reads the same
text.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-16 03:58:24 +02:00

5.4 KiB

name, description
name description
adding-a-stack Use when adding a new service to this repository, moving a service to a different host, or bringing a hand-deployed service under Ansible and CI.

Adding a stack

A stack is one Docker Compose deployment on one host. Adding one touches four files and one button. AGENTS.md holds the conventions and the full compose_stack role contract — read it first; this file is the order of work.

Steps

  1. Payload. Put the compose file and its configuration in server/<host>/<stack>/. Payloads are copied verbatim, so do not template or restructure them. Never commit a .env, a key, or a secret written inline in a compose file — read docs/secrets.md before you decide something is fine to commit.

  2. Playbook. Create ansible/playbooks/<stack>.yml. Copy static-site.yml; most stacks are a name and a network. Tag the play with the stack name.

  3. Register it. Add an import_playbook entry to ansible/playbooks/site.yml in dependency order — the reverse proxy owns proxy-net and the vhost configuration, so it stays first.

  4. Give it a button. All three CI configurations need the stack by name, and none of them can generate the list:

    • .github/workflows/deploy.yml — add it to the stack input's options:.
    • .gitea/workflows/deploy.yml — the same options: list again. Gitea reads .gitea/workflows/ instead of .github/workflows/, so the two files are never both in play and never both wrong at once — which is exactly why one of them gets forgotten.
    • .gitlab-ci.yml — add a check:<stack> and deploy:<stack> pair extending .check and .deploy; copy the pair above them, three lines each. Pin the stack with STACK_FIXED, never STACK — a pipeline variable outranks a job's YAML variables:, so a button named with STACK is hijacked by any pipeline triggered with a different one.

    Delete whichever platforms you are not using rather than letting them rot.

  5. Verify statically. make check from the repository root. It must pass; this is the definition of done for the playbook.

  6. Deploy. Run the check mode, read the diff, then deploy. Never run a playbook against a host unless the person you are working with asked for it in that turn.

Does it fit the shared role?

If Do Example
It is not a Compose stack — one file into another stack's directory, or a system service Write plain tasks. Do not force the role banner.yml
Something must happen between the payload landing and the stack starting (backup, migration) Use the two entry points, tasks_from: sync then tasks_from: up, and gate the middle on pull | default('policy') == 'always' webapp.yml
The payload is bind-mounted config, which Compose never recreates a container for Add a post_tasks reload keyed on compose_stack_synced reverse-proxy.yml, metrics.yml
The compose file is not named docker-compose.yml, or there are several stack_files webapp.yml
The same payload serves several hosts, or several instances stack_src, and stack_dest for the second instance metrics.yml, webapp-staging.yml
The stack needs a .env that is deliberately never shipped stat + assert that it exists on the host first webapp.yml
It needs an external Docker network stack_networks any of them
A bind mount would otherwise be created root-owned by Docker stack_dirs; omit mode so an existing directory is left alone reverse-proxy.yml
A bind mount must be owned by a uid from inside the container stack_dirs with owner/group and recurse, and no mode metrics.yml
The image is built from a Dockerfile in the payload stack_build, set to always only on an update run webapp.yml
Hosts differ in some small way A dict keyed by inventory_hostname, looked up in the role vars metrics.yml

Extend the role's variable contract rather than adding a second role or loose tasks. If a stack needs something the contract cannot express, say so instead of working around it — that is information about the contract.

Common mistakes

  • Skipping step 3 or 4. The stack then works from a workstation and is invisible in CI, which is exactly how services end up hand-deployed and undocumented. This is the most common failure and the least visible.
  • Pre-creating a Postgres data directory in stack_dirs. Postgres refuses to start unless it is 0700 or 0750, so leave it to the image.
  • Setting mode on a bind mount that already has data. It rewrites the permissions of everything the container has written there. Set owner/group with recurse instead, and no mode.
  • A pre_tasks chown instead of stack_dirs. pre_tasks run before the stack directory exists, and the escalated file task stamps its owner onto every parent it creates — leaving the payload sync unable to write into the stack's own directory.
  • Expecting .env to reach the host. It is excluded from every sync by design. Secrets live on the host.
  • A reload task with no when: compose_stack_synced. Every run then reports a change, the playbook is never idempotent, and changed=0 stops meaning anything.
  • Putting the stack_* vars inside roles: for a two-phase playbook. They are scoped to that invocation and the second include_role will not see them. Declare them at play level.