boma/docs/decisions/004-docker-model.md
sjat f0d189ca09 Thread the VERIFY.md convention through ADR-004/new-role/README
Review O1-O3: ADR-017's per-service VERIFY.md requirement now appears in the
ADR-004 service-role file table, as a new-role runbook step, and the README
docs index/tree are refreshed (ADRs 010-017, security/testing/hardware dirs).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 18:52:42 +02:00

109 lines
4.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ADR-004 — Docker and Compose service model
## Context
All services run as Docker containers managed via Docker Compose. This document
defines how services are structured, deployed, and maintained.
## Core principles
- **No hand-edited files on hosts**: all Compose files are rendered by Ansible
from Jinja2 templates. If a file exists on a host, it was put there by Ansible.
- **Compose per service**: each service (or tightly coupled service group) gets
its own Compose file and directory under a standard path.
- **Variables drive differences**: the same template renders differently per host
via `group_vars` and `host_vars`. No host-specific templates.
## Directory layout on hosts
```
/opt/services/
├── servicename/
│ ├── docker-compose.yml # rendered by Ansible, never edited manually
│ ├── .env # rendered by Ansible from vault variables
│ └── data/ # persistent volumes (bind mounts)
│ └── ...
```
All services live under `/opt/services/`. The path is defined in
`group_vars/all/vars.yml` as `services__base_dir`.
## Service-role standard
**Every service has its own self-contained role** — one service, one role. Shared
roles serving multiple services are no longer used (see "Why not a shared engine"
below). Each service role contains a standard set of files:
| File | Purpose |
|---|---|
| `tasks/main.yml` | The standard deploy mechanics (below) |
| `templates/docker-compose.yml.j2` | The Compose definition |
| `templates/env.j2` | `.env` rendered from vault variables |
| `defaults/main.yml` | Tuneables, `rolename__` namespace |
| `README.md` | Purpose, variables, usage (role convention) |
| `SECURITY.md` | Per-service security record — see ADR-002 and `docs/security/service-security-template.md` |
| `VERIFY.md` | Per-service UI acceptance spec — see ADR-008 Level 4 / ADR-017 and `docs/testing/service-verify-template.md` |
| `meta/main.yml`, `molecule/default/` | Metadata + Debian 13 test scenario |
### Standard deploy mechanics
Every service role's `tasks/main.yml` follows the same sequence, so all roles are
uniform and predictable:
1. Create `/opt/services/<service>/` directory
2. Render `docker-compose.yml` from `templates/docker-compose.yml.j2`
3. Render `.env` from `templates/env.j2` (secrets from vault variables)
4. Run `docker compose up -d --remove-orphans` via `ansible.builtin.command`
5. Optionally run `docker compose pull` before up (controlled by a variable)
### Why not a shared engine
A shared `compose_service` engine role — service roles delegating the mechanics to
one place — is **intentionally not built**. Duplicating the ~5 standard tasks per
role is accepted in favour of legible, self-contained roles a reader can understand
without indirection, and AI authorship makes the duplication cheap to generate
uniformly from this standard.
**Revisit trigger:** extract a shared engine role if maintaining the duplicated
mechanics across service roles becomes painful — a pattern change that means editing
many roles, or drift between them that this standard alone isn't preventing.
## Docker daemon configuration
Managed by the `docker_host` role. Key settings:
- `"log-driver": "json-file"` with size limits (prevents disk exhaustion)
- `"iptables": false` — firewall managed entirely by nftables (see ADR-002)
- TCP socket disabled — Unix socket only (`/var/run/docker.sock`)
- User namespace remapping: evaluated per use case, not enabled by default
## Networking
- Each service Compose file defines its own named network(s)
- Services that need to communicate are placed on a shared named network
defined in a dedicated `docker-compose.networks.yml` (if cross-service
networking is needed on a host)
- External port publishing is explicit and matches nftables rules
## Image management
- Image pinning follows the tiered model in ADR-011: **stateful** services pin
`tag@digest` (readable tag + integrity digest); **stateless** services use rolling
tags (`latest`/`stable`), refreshed deliberately and watched by DIUN
- Bare `latest` is therefore acceptable only on the stateless tier; the stateful tier
is always pinned
- Image updates are a deliberate operation: update the tag/digest variable, run deploy
## Persistent data
- Bind mounts preferred over named volumes for data that must be backed up
- All bind mount paths are under `/opt/services/<name>/data/`
- Backup strategy is defined separately (not in scope of this repo)
## Decision
Docker Compose was chosen over Kubernetes/Swarm because:
- Appropriate complexity level for 25 hosts with independent service sets
- Compose files are human-readable and easily auditable
- No distributed state to manage
- Straightforward to back up and restore