2026-05-30 14:10:01 +02:00
|
|
|
|
# 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`.
|
|
|
|
|
|
|
2026-06-04 16:09:33 +02:00
|
|
|
|
## Service-role standard
|
2026-05-30 14:10:01 +02:00
|
|
|
|
|
2026-06-04 16:09:33 +02:00
|
|
|
|
**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:
|
2026-05-30 14:10:01 +02:00
|
|
|
|
|
2026-06-04 16:09:33 +02:00
|
|
|
|
| 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` |
|
|
|
|
|
|
| `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.
|
2026-05-30 14:10:01 +02:00
|
|
|
|
|
|
|
|
|
|
## 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
|
|
|
|
|
|
|
|
|
|
|
|
- Images are always pinned to a specific digest or tag in templates
|
|
|
|
|
|
- `latest` is never used in production Compose files
|
|
|
|
|
|
- Image updates are a deliberate operation: update the tag 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 2–5 hosts with independent service sets
|
|
|
|
|
|
- Compose files are human-readable and easily auditable
|
|
|
|
|
|
- No distributed state to manage
|
|
|
|
|
|
- Straightforward to back up and restore
|