boma/docs/decisions/004-docker-model.md
sjat fe4228fb38 Add architecture decision records and runbooks
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 14:10:01 +02:00

3 KiB
Raw Blame History

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.

Compose file delivery

Each service has a corresponding Ansible role (or is managed by a shared role with per-service variables). The role:

  1. Creates /opt/services/servicename/ directory
  2. Renders docker-compose.yml from templates/docker-compose.yml.j2
  3. Renders .env from templates/env.j2 (pulling secrets from vault variables)
  4. Runs docker compose up -d --remove-orphans via ansible.builtin.command
  5. Optionally runs docker compose pull before up (controlled by variable)

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 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