DevOps & SRE No. 152

Docker Compose skill for Claude Code

One command to a working local stack. Claude builds compose.yaml files where the app waits for a healthy database instead of sleeping, migrations run as a one-shot service, ports bind to localhost, and optional tools sit behind profiles. It separates .env interpolation from container env, sets up Compose Watch for hot reload, and validates everything with docker compose config and up --wait.

  • namedocker-compose
  • size226 lines · ≈2,100 tokens
  • files1
  • licenseMIT
  • updated

What Claude sees before loading it

This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.

description: Builds local development stacks with Docker Compose v2 — services, healthchecks with depends_on conditions, named volumes vs. bind mounts, profiles, env files and interpolation, Compose Watch for hot reload, and override files. Use when writing or fixing a compose.yaml, when services start before the database is ready, when hot reload does not work in a container, or when onboarding needs a one-command local environment.

Try asking

  • Set up a compose file with Postgres, Redis and our API that works with one command
  • The app container crashes because Postgres is not ready yet. Fix the startup order
  • Hot reload stopped working inside the container on my Mac

Use it when

  • A project needs a reproducible local environment for new developers
  • Services race each other on startup
  • Environment variables are not reaching containers as expected
  • Code changes are not picked up inside a dev container

Skip it when

How to check the result

docker compose config passes, docker compose up -d --wait exits zero, and docker compose ps shows every service with a healthcheck as healthy.

The common mistake it prevents

Expecting variables in .env to appear inside containers; that file only feeds interpolation in compose.yaml unless it is also listed under env_file.

~/.claude/skills/docker-compose/SKILL.md

--- name: docker-compose description: Builds local development stacks with Docker Compose v2 — services, healthchecks with depends_on conditions, named volumes vs. bind mounts,… license: MIT ---

A good Compose stack comes up with one command on a clean machine, starts services in dependency order based on real readiness, keeps data across restarts, reloads code without rebuilds, and never requires editing the file to switch personal settings.

Ground rules

  • Use docker compose (v2 plugin), file name compose.yaml. Drop the top-level version: key; it is obsolete and only produces a warning.
  • Readiness comes from healthcheck + depends_on: condition: service_healthy, never from sleep in an entrypoint.
  • Services talk to each other by service name (postgres:5432), not localhost.
  • Bind published ports to 127.0.0.1 unless another device must reach them.
  • Secrets for local dev live in a git-ignored .env; commit .env.example.
  • docker compose down -v deletes named volumes (database data). Ask before running it.

Workflow

- [ ] 1. Inventory services, ports, data that must persist, and dev-only tools
- [ ] 2. Write compose.yaml: app + backing services with healthchecks
- [ ] 3. Wire env: .env for interpolation, env_file/environment for containers
- [ ] 4. Add hot reload (Compose Watch or bind mount)
- [ ] 5. Put optional services behind profiles
- [ ] 6. Validate with `docker compose config`, then `up --wait`
- [ ] 7. Document the commands in the README

2. Baseline file

name: shop

services:
  api:
    build:
      context: .
      target: dev            # dev stage of a multi-stage Dockerfile
    ports:
      - "127.0.0.1:3000:3000"
    env_file:
      - path: .env
        required: false
    environment:
      DATABASE_URL: postgres://app:app@postgres:5432/app
      REDIS_URL: redis://redis:6379
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
      migrate:
        condition: service_completed_successfully
    init: true
    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src
        - action: rebuild
          path: package-lock.json

  migrate:
    build: { context: ., target: dev }
    command: ["npm", "run", "db:migrate"]
    environment:
      DATABASE_URL: postgres://app:app@postgres:5432/app
    depends_on:
      postgres:
        condition: service_healthy

  postgres:
    image: postgres:17
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: app
      POSTGRES_DB: app
    ports:
      - "127.0.0.1:5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app"]
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 10s

  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 10

  mailpit:
    image: axllent/mailpit
    profiles: ["tools"]
    ports:
      - "127.0.0.1:8025:8025"

volumes:
  pgdata:

Note for Postgres 18+ images: the data directory layout changed; mount the volume at /var/lib/postgresql per the image docs instead of /var/lib/postgresql/data.

Healthcheck commands that work

Servicetest
Postgres["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
MySQL/MariaDB["CMD", "mysqladmin", "ping", "-h", "localhost"] (MariaDB 11: healthcheck.sh --connect)
Redis/Valkey["CMD", "redis-cli", "ping"] / ["CMD", "valkey-cli", "ping"]
HTTP app with curl["CMD", "curl", "-fsS", "http://localhost:3000/health"]
HTTP app without curl["CMD", "wget", "-qO-", "http://localhost:3000/health"] or a tiny script in the app’s runtime
Kafka (bitnami/apache)kafka-topics.sh --bootstrap-server localhost:9092 --list

$$ escapes $ so the variable is expanded inside the container, not by Compose.

3. Environment: two different mechanisms

MechanismWho reads itUse for
.env in project dirCompose itself, for ${VAR} interpolation in compose.yamlPorts, image tags, personal overrides
env_file:Injected into the containerApp config and local secrets
environment:Injected into the container, overrides env_fileValues that must be fixed (service URLs)
Shell env when running composeInterpolation, highest precedenceOne-off overrides: API_PORT=4000 docker compose up

Interpolation with defaults and required values:

ports:
  - "127.0.0.1:${API_PORT:-3000}:3000"
image: "myorg/api:${TAG:?TAG must be set}"

Check the resolved result before debugging anything else: docker compose config.

4. Hot reload

Prefer Compose Watch (docker compose up --watch, or docker compose watch):

ActionWhen
syncSource files the dev server already watches (copied into the container)
sync+restartConfig files that need a process restart but not a rebuild
rebuildLockfiles, Dockerfile, anything that changes the image

Bind-mount alternative (needed when tools inside the container write back to the host):

volumes:
  - ./:/app
  - /app/node_modules   # anonymous volume: keep the image's Linux node_modules

If file changes are not detected through a bind mount on macOS/Windows, enable polling in the dev server (CHOKIDAR_USEPOLLING=1, WATCHPACK_POLLING=true, Vite server.watch.usePolling).

5. Profiles and overrides

  • Services with profiles: start only when asked: docker compose --profile tools up. Never put a service that others depends_on behind a profile.
  • compose.override.yaml is merged automatically; use it for personal tweaks and git-ignore it, or commit a compose.override.example.yaml.
  • Split large stacks with top-level include: (other compose files with their own relative paths) rather than long -f chains.

6. Validate and run

docker compose config --quiet && echo "compose file valid"
docker compose up -d --wait          # returns when all healthchecks pass, non-zero if not
docker compose ps                    # STATUS column must show (healthy)
docker compose logs -f --tail=100 api
docker compose exec postgres psql -U app -d app -c 'select 1'

Debugging table

SymptomLikely causeCheck
ECONNREFUSED 127.0.0.1:5432 from appUsing localhost inside a containerUse service name postgres
App starts then crashes on DB connectNo healthcheck conditiondepends_on.condition: service_healthy
port is already allocatedHost service or another stack on that portlsof -i :5432; change host side via ${PORT:-...}
Env var empty inside containerDefined only in .env (interpolation), not passedAdd to env_file/environment
Data gone after restartNo named volume, or someone ran down -vdocker volume ls
Code changes ignoredWatch not running or path mismatchdocker compose up --watch; compare target to WORKDIR
exec format errorImage built for other archplatform: linux/amd64 or multi-arch image
Healthcheck stuck startingTool missing in image (curl)docker compose exec svc which curl

Gotchas

  • depends_on without a condition only orders container start, not readiness.
  • restart: always hides crash loops in dev; prefer unless-stopped or none, and read logs.
  • Changing POSTGRES_PASSWORD after the volume exists does nothing; the database was initialized on first boot. Recreating requires deleting the volume (confirm first).
  • Scripts in /docker-entrypoint-initdb.d run only on an empty data directory.
  • Bind-mounting the project over /app also hides node_modules/.venv built in the image.
  • container_name breaks --scale and collides across checkouts; let Compose name containers.
  • Compose project name defaults to the directory name; set top-level name: so two worktrees do not share volumes unexpectedly, or deliberately vary it per worktree.

Output

Deliver compose.yaml, .env.example, any Dockerfile dev stage, and a README block:

## Local development
cp .env.example .env
docker compose up -d --wait        # api on http://localhost:3000
docker compose --profile tools up -d mailpit   # optional: mail UI on :8025
docker compose logs -f api
docker compose down                # stop (keeps data); `down -v` also wipes the DB

About this skill

Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.

esc