Skip to content

Acceptance record: Ansible Compose deployment (deploy.yml)

This records the proofs for the Compose deployment wrapper of the Ansible re-platform (the behavior contract). It implements the COMPOSE DEPLOY section of the behavior contract (DEP-1..5): playbooks/deploy.yml plus the compose_deploy role. With the eleven current host.yml roles from slices 1-3 (rehearsed as twelve; homepage_config was retired when Grafana became the service launcher), this completes the playbook surface; install.sh stayed frozen as the rollback at the time and nothing here ran on production (it was retired on 2026-07-14 once every proof closed).

Per the contract's proof strategy, the evidence stays split into VM-simulation proofs (all green below) and PROD-validation proofs (deferred to the scheduled cutover).

Safety design

Ownership split, enforced both ways

Compose remains authoritative for container lifecycle: the role shells out to the Compose CLI (docker compose config/pull/up) from each project directory and uses no community.docker or container modules. In the other direction, deploy.yml refuses to grow host-convergence side effects: the Docker daemon and the external proxy network are asserted, never created (creation is DOCKER-5 in host.yml), and a root-run deploy is refused outright because SEC-1 keys the .env files to the operator precisely so that Compose runs non-root.

Gate before mutation

deploy.yml reuses role: preflight unchanged, so PRE-1..PRE-5 gate a deploy exactly as they gate a converge. The role then validates every project's Compose configuration before any project deploys: one invalid project aborts the run with zero container changes, instead of leaving the fleet half-converged behind a mid-loop failure.

No /run pending-work marker is needed here, unlike DOCKER-2: the role has no handlers whose loss would strand a deferred action, and docker compose up --detach is itself the convergence, so a re-run is the recovery.

Rehearsal harness extensions

The VM cannot run the real fleet, so fixtures.sh compose-good seeds three pinned-BusyBox stand-in projects mirroring the real repo shape (a root project plus monitoring/ and actual/, giving the same dir-derived project names as production); the root stand-in declares the external proxy network, which is what makes the absent-network negative proof load-bearing, and the monitoring stand-in is a buildable service with a local-only image tag (mirroring the real intel-gpu-exporter), which is what makes the DEP-5 --ignore-buildable pull proof load-bearing. ansible/tests/vm/vm-overrides.yml points compose_deploy_projects at the stand-ins; production always uses the role defaults. Bootstrap now also installs a Compose v2 CLI on the VM (Debian's docker.io does not ship the plugin).

Reproduce from ansible/tests/vm:

./rehearse.sh deploy   # the DEP-1..5 stage alone
./rehearse.sh all      # full acceptance sequence

VM-simulation proofs - ALL PASSING

Deploy stage on Debian 13 (trixie), ansible-core 2.19.4, Docker 26.1.5, Compose v5.1.4: 12 checks passed, 0 failed, and the full rehearsal (slices 1-3 plus the deploy stage, re-run after rebasing onto the Homepage retirement) reports 84 passed, 0 failed, so the eleven current host roles show no regression.

Negative proofs

Contract Precondition Result
DEP-1 Deploy invoked with sudo refused before any probe (the operator must be exactly homelab_user, so root or any other user is rejected)
PRE-3 (inherited) A secret .env removed deploy aborts in preflight before touching Docker
PRE-3 via --tags compose_deploy Same, with a tag-scoped invocation preflight is always-tagged in deploy.yml, so the gate cannot be tag-skipped
DEP-3 Docker daemon stopped aborts on the daemon probe; nothing deployed
DEP-3 External proxy network removed aborts pointing at host.yml; nothing deployed
DEP-2 Second project's Compose file invalid whole deploy aborts on the validation assert
DEP-2 Same, checked after the abort zero containers exist for any of the three project names

Positive proofs

Contract Evidence on VM
DEP-1 Unprivileged deploy as homelab_user converges all three stand-in projects; one running container per project name
DEP-4 Immediate second deploy reports changed=0
DEP-5 Deploy with -e compose_deploy_pull=true succeeds and stays changed=0 with the pinned images already present

PROD-validation proofs - CONFIRMED at the 2026-07-13 production cutover

Contract Production proof Result (2026-07-13 cutover, 2026-07-14 DEP-5)
DEP-1 The three real projects converge with today's project names; docker compose ps per project matches the pre-cutover fleet ✅ first real deploy.yml run as homelab_user (no sudo): ok=16 changed=0 failed=0; container IDs and creation times identical to the pre-cutover snapshot
DEP-2 Gate exercised only if a bad Compose file ever lands (CI already validates all three projects per PR) ✅ gate ran and passed for all three projects; failure branch intentionally not exercised (CI covers it)
DEP-3 proxy exists from host.yml (DOCKER-5); daemon active proxy present, daemon active
DEP-4 A routine re-deploy on the converged fleet reports changed=0 changed=0, zero containers recreated
DEP-5 The post-Renovate flow (-e compose_deploy_pull=true) pulls exactly the bumped images and recreates only their containers ✅ 2026-07-14, first post-cutover Renovate bump (PR #113, tautulli-exporter 0.2.6→0.2.7): deploy.yml -e compose_deploy_pull=true as homelab_user ran ok=19 changed=2 failed=0 (only the monitoring project changed); before/after docker ps --no-trunc diff shows exactly one container recreated (tautulli-exporter on the new tag), the other 39 container IDs identical; new exporter healthy (up{job="tautulli"} == 1)

The deferred proofs from slices 1, 2, and 3 remain part of the same cutover checklist.