Files
config-public/skills/adding-a-stack/SKILL.md
T
Lucas WintherandClaude Opus 5.5 792f7cde48 Add the host bootstrap: deploy user, key, docker access, stack root
bootstrap.yml prepares a host as an admin via -u; new-deploy-key.sh makes the
key pair and refuses to write inside the repo. CI deploys refuse the bootstrap
playbook, since the dispatch API ignores the dropdown.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 15:07:31 +02:00

5.5 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, host preparation, or a system service Write plain tasks. Do not force the role banner.yml, bootstrap.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.