Skip to main content
RunBook Academy

← All checklists in Docker & Containers

Before deploymentCompose

Checklist: Compose stack production readiness

24 items ·19 critical ·5 warn ·0 info

When to run this

Before a Compose stack first carries production traffic, and again whenever a service is added or its resources, probes, mounts or published ports change. Thirty minutes, most of it reading the rendered configuration.

How to run it

Run from the stack directory, with the same -f files, --profile flags and .env the deploy uses. A checklist run against a different file set than the deploy is a checklist run against a different stack.

STACK_DIR=/srv/stacks/platform
cd "$STACK_DIR"
export COMPOSE_FILE=compose.yaml:compose.prod.yaml

Nearly every item queries docker compose config --format json, which is the rendered model: interpolation applied, overrides merged, extends resolved. That is deliberate. The defect this checklist most often finds is a value that is correct in the file you read and different in the file Compose produced.

Two items need the daemon rather than the stack — logging-bounded reads /etc/docker/daemon.json, and it needs sudo on most hosts.

Reading the results

Most items print only what is wrong. config-renders, no-unset-variables, images-digest-pinned, no-build-in-production, restart-policy-set, healthcheck-defined, memory-limits-set, no-privileged, no-docker-socket, secrets-not-in-environment, production-volumes-external, ports-not-wildcard and committed-and-tagged are all silent on a stack that passes. Any line of output is a finding, and the line names the service.

Three items produce output for a human to judge rather than a pass/fail: rendered-config-reviewed, healthcheck-is-meaningful and networks-explicit. healthcheck-is-meaningful in particular cannot be automated — the command prints each probe command, and somebody has to decide whether that probe could fail for the reason that matters.

stack-starts-from-scratch has no command because nothing you can run on the existing host will answer it. It needs a clean host, a docker compose up -d, and a functional test.

Evidence and sign-off

Attach the rendered docker compose config output and the git revision to the change record. Together they are the complete description of what was deployed, which is what the next incident review will ask for.

  • Rendered config attached: ______ Revision: ______________
  • Clean-host bring-up date: ___________
  • Operator: _________________ Date: ___________
  • Reviewer: ________________ Date: ___________

Critical19 items

  1. docker compose config -q
  2. docker compose config 2>&1 >/dev/null | grep -i "variable is not set"
  3. docker compose config
  4. docker compose config --images | grep -v '@sha256:'
  5. docker compose config --format json | jq -r '.services | to_entries[] | select(.value.build) | .key'
  6. docker compose config --format json | jq -r '.services | to_entries[] | select((.value.restart // .value.deploy.restart_policy) == null) | .key'
  7. docker compose config --format json | jq -r '.services | to_entries[] | select(.value.healthcheck == null) | .key'
  8. docker compose config --format json | jq -r '.services | to_entries[] | select(.value.healthcheck) | "\(.key): \(.value.healthcheck.test | join(" "))"'
  9. docker compose config --format json | jq -r '.services | to_entries[] | select((.value.deploy.resources.limits.memory // .value.mem_limit) == null) | .key'
  10. jq -e '."log-opts"."max-size"' /etc/docker/daemon.json
  11. docker compose config --format json | jq -r '.services | to_entries[] | select(.value.privileged == true or .value.network_mode == "host" or .value.pid == "host") | .key'
  12. docker compose config | grep -n 'docker\.sock'
  13. docker compose config | grep -inE '^[[:space:]]+[A-Z_]*(PASSWORD|SECRET|TOKEN|APIKEY|API_KEY|PRIVATE_KEY):[[:space:]]*[^[:space:]]'
  14. stat -c '%a %U %G %n' "$STACK_DIR/.env"; grep -c '^\.env$' "$STACK_DIR/.gitignore"
  15. grep -q '^COMPOSE_PROJECT_NAME=' "$STACK_DIR/.env" || echo "not pinned: project name derives from the directory name"
  16. docker compose config --format json | jq -r '(.volumes // {}) | to_entries[] | select(.value.external != true) | .key'
  17. docker compose config --format json | jq -r '.services | to_entries[] | select(.value.ports) | .value.ports[] | select((.host_ip // "0.0.0.0") == "0.0.0.0") | "\(.published) -> \(.target)"'
  18. git -C "$STACK_DIR" status --porcelain

Warning5 items

  1. docker compose config --format json | jq -r '.services | to_entries[] | select(.value.depends_on) | select(any(.value.depends_on[]; .condition != "service_healthy")) | .key'
  2. docker compose config --format json | jq -r '.services | to_entries[] | select((.value.deploy.resources.limits.cpus // .value.cpus) == null) | .key'
  3. docker compose config --format json | jq -r '.services | to_entries[] | select(.value.pids_limit == null) | .key'
  4. docker compose config --format json | jq -r '.services | to_entries[] | select(.value.container_name) | .key'
  5. docker compose config --format json | jq -r '.services | to_entries[] | "\(.key): \((.value.networks // {"default":null}) | keys | join(","))"'