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]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
971034e630
commit
e1f152ec3e
@@ -0,0 +1,85 @@
|
||||
---
|
||||
name: adding-a-stack
|
||||
description: 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.
|
||||
Reference in New Issue
Block a user