Docker Compose for Local Dev skill for Codex
One command up, every service healthy. Codex writes compose.yaml files where the app waits for a healthy database instead of crashing on boot, migrations run as one-shot services, rarely used tools sit behind profiles, and Compose Watch syncs code or rebuilds on lockfile changes. Ports bind to loopback, secrets stay out of the file, and the stack is proven with docker compose up --wait.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- docker-compose-dev Installs to ~/.agents/skills/docker-compose-dev/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install docker-compose-dev The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project docker-compose-dev Installs to .agents/skills/docker-compose-dev/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/docker-compose-dev.zipunzip -o docker-compose-dev.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) docker-compose-dev PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $docker-compose-dev, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Builds Docker Compose setups for local development - compose.yaml with healthchecked databases, depends_on conditions, profiles for optional services, Compose Watch for live sync and rebuilds, override files, env files and named volumes. Use when a project needs a one-command local stack, when the app starts before Postgres is ready, when bind mounts are slow or clobber node_modules, when optional tools (queues, mail catchers, admin UIs) should stay off by default, or when reviewing a compose file.
Use it when
- A repo needs a one-command local stack for new contributors
- Services race each other at startup
- Optional dev tools should not start by default
- Bind mounts are slow or overwrite installed dependencies
Use something else when
- You are writing or optimising the image itself: use dockerfile-best-practices
- Containers cannot reach each other and you need to debug Docker networking: use docker-networking
- You want a full editor environment in a container: use devcontainers
How to check the result
docker compose config --quiet passes and docker compose up -d --wait exits zero with every service healthy, then a source edit shows up in the running app without a manual rebuild.
The mistake it prevents
Using depends_on without a condition and assuming it waits for the database to accept connections; it only orders container start.
Done well means a new contributor runs one command, every service reports healthy, the app only starts once its dependencies accept connections, code edits show up without a manual rebuild, and nothing in the file is a production secret.
Inspect first
- Read
AGENTS.mdand the README for how the project is run today and which commands tests use. Compose should wrap those commands, not replace them. - Look for existing
compose.yaml,docker-compose.yml,compose.override.yaml,.env,.env.exampleand Dockerfiles. Keep service names that scripts or CI reference. - Check the Compose version:
docker compose version. Watch needs 2.22+,env_filewithrequired:needs 2.24+,start_intervalin healthchecks needs 2.20+ with Docker Engine 25+. The legacydocker-compose(v1, Python) binary is dead; usedocker compose. - List each service’s dependencies (database, cache, queue, object storage, mail) and which are needed for everyday work versus occasionally.
Pulling images needs network. In a Codex sandbox with network off, write the file and
validate it with docker compose config, then hand the up step to the user.
File layout
| File | Purpose | Committed? |
|---|---|---|
compose.yaml | Services shared by everyone | Yes |
compose.override.yaml | Merged automatically; personal or dev-only tweaks | Usually no (gitignore it, ship compose.override.example.yaml) |
.env | Variable interpolation for the compose file itself and project name | No; commit .env.example |
compose.ci.yaml | CI variant, used with -f compose.yaml -f compose.ci.yaml | Yes |
Do not add a top-level version: key; it is obsolete and only produces a warning.
Reference compose.yaml
name: shop
x-app-env: &app-env
DATABASE_URL: postgres://app:app@db:5432/app
REDIS_URL: redis://cache:6379/0
services:
web:
build:
context: .
target: dev # dev stage in the Dockerfile with dev deps installed
command: npm run dev -- --host 0.0.0.0
ports:
- "127.0.0.1:3000:3000" # bind to loopback, not every interface
environment: *app-env
env_file:
- path: .env.local
required: false
depends_on:
db:
condition: service_healthy
restart: true # restart web if db is restarted by compose
cache:
condition: service_started
migrate:
condition: service_completed_successfully
develop:
watch:
- action: sync
path: ./src
target: /app/src
- action: rebuild
path: package-lock.json
- action: sync+restart
path: ./config
target: /app/config
migrate:
build: { context: ., target: dev }
command: npm run db:migrate
environment: *app-env
depends_on:
db: { condition: service_healthy }
db:
image: postgres:17
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: app
POSTGRES_DB: app
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 10s
timeout: 3s
retries: 5
start_period: 20s
start_interval: 1s
cache:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
mail:
image: axllent/mailpit:latest
profiles: [tools]
ports: ["127.0.0.1:8025:8025"]
adminer:
image: adminer
profiles: [tools]
ports: ["127.0.0.1:8081:8080"]
volumes:
pgdata:
Postgres 18 images moved the data directory convention (mount at /var/lib/postgresql
instead of .../data); check the image docs when bumping majors, and never reuse a
volume across Postgres major versions without a dump and restore.
Decisions
| Situation | Do |
|---|---|
| App crashes on start because DB is not ready | Healthcheck on the DB plus depends_on: condition: service_healthy. Keep retry logic in the app anyway |
| Migrations or seed must run first | One-shot service, depended on with service_completed_successfully |
| Optional tools (mail catcher, admin UI, tracing) | profiles: [tools]; start with docker compose --profile tools up |
| Code edits must appear live | develop.watch with sync; use rebuild for lockfiles, sync+restart for config read at boot |
| Framework already hot-reloads from a bind mount and it is fast enough | Bind mount ./src:/app/src is fine; never bind-mount over node_modules |
| Slow file I/O on macOS/Windows bind mounts | Prefer Watch sync, or keep dependencies in an anonymous volume |
| Shared settings across services | x- extension field plus YAML anchor, as above |
| Splitting a large stack | Top-level include: of other compose files (2.20+) |
| Same image, different command | Reuse build: and set command:; Compose builds once per unique config |
Watch rules: sync copies changed files into the running container (the target path
must be writable by the container user); rebuild rebuilds the image and recreates the
container; sync+restart syncs then restarts. Add ignore: patterns for build output so
the app’s own writes do not loop. Start with docker compose up --watch or
docker compose watch.
Hard rules
- Ports for dev services bind to
127.0.0.1, otherwise a laptop on café Wi-Fi exposes Postgres with passwordapp. - Containers talk to each other by service name (
db:5432), neverlocalhost. The published host port is only for tools on the host. depends_onwithout a condition only orders container start; it does not wait for readiness.- Healthcheck commands run inside the container: the binary (
curl,pg_isready) must exist in that image. Slim images often lack curl. env_filesets container environment;.envsets compose-file interpolation. They are different mechanisms and confuse people constantly.- Named volumes persist across
down.docker compose down -vdeletes them and all local data: confirm with the user before running it. - Do not put real API keys in
compose.yaml. Useenv_filewithrequired: falseand document variables in.env.example. - Keep dev images out of production: the
devtarget belongs to local compose only.
Workflow
- [ ] 1. Inventory services, ports, and existing commands
- [ ] 2. Write compose.yaml with healthchecks and depends_on conditions
- [ ] 3. Put optional services behind profiles; add .env.example
- [ ] 4. docker compose config --quiet (validates and merges)
- [ ] 5. docker compose up --wait (returns when healthy, fails otherwise)
- [ ] 6. Edit a source file and confirm the change appears via Watch
- [ ] 7. Document the commands in README or AGENTS.md
Verification
docker compose config --quiet && echo "config ok"
docker compose config --services
docker compose up -d --wait # exits non-zero if any healthcheck fails
docker compose ps --format 'table {{.Service}}\t{{.Status}}'
docker compose logs --tail=50 web
docker compose exec web sh -c 'nc -z db 5432 && echo db reachable' # if nc is in the image
docker compose --profile tools up -d mail && curl -sf http://127.0.0.1:8025 >/dev/null && echo mailpit up
If --wait fails, docker inspect --format '{{json .State.Health}}' <container> shows the
last healthcheck outputs; the usual causes are a missing binary in the image or a
start_period shorter than the service’s first boot.
Output
**Start:** `docker compose up --watch` (core) / `docker compose --profile tools up` (adds mailpit, adminer)
**Services:** web (3000), db postgres:17 (healthy), cache redis:7, migrate (one-shot)
**Live reload:** src synced, lockfile triggers rebuild, config restarts web
**Data:** named volume pgdata; reset with `docker compose down -v` (destroys local data)
**Verified:** config valid, up --wait healthy, edit to src/app.ts visible in 1 s What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Docker Compose for Local Dev"
short_description: "One command up, every service healthy"
default_prompt: "$docker-compose-dev Set up docker compose for this app with Postgres and Redis" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.