Docker Compose for Local Development: App, Postgres, and Redis in One Command
A compose.yaml that starts your whole stack — app, database, cache, and workers — with one command. Services, networking by service name, volumes for data and code, health-checked startup order, env files, profiles, watch mode for live reload, and the pitfalls on macOS and Windows.
"Install Postgres 17, then Redis, then set these six environment variables, then run migrations" is an onboarding doc that's always slightly wrong. Docker Compose replaces it with a file in the repository and one command: docker compose up. (What Is Docker? covers containers themselves.)
A complete example
# compose.yaml
services:
db:
image: postgres:17
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: app
POSTGRES_DB: app
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 2s
timeout: 3s
retries: 20
redis:
image: redis:8
ports:
- "6379:6379"
app:
build: .
command: npm run dev
env_file: .env
environment:
DATABASE_URL: postgres://app:app@db:5432/app
REDIS_URL: redis://redis:6379
ports:
- "3000:3000"
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
develop:
watch:
- action: sync
path: ./src
target: /app/src
- action: rebuild
path: package-lock.json
worker:
build: .
command: npm run worker
env_file: .env
environment:
DATABASE_URL: postgres://app:app@db:5432/app
REDIS_URL: redis://redis:6379
depends_on:
db:
condition: service_healthy
profiles: ["worker"]
volumes:
pgdata:
docker compose up -d db redis # just the dependencies
docker compose watch # app with live sync
docker compose --profile worker up # include the worker
docker compose logs -f app
docker compose down # stop (keeps volumes)
docker compose down -v # stop and delete data
compose.yaml is the current preferred filename; docker-compose.yml still works. Use docker compose (the plugin, v2) rather than the legacy docker-compose binary.
Networking: use service names
Compose puts services on a shared network where each is reachable by its service name. Inside the app container, the database is db:5432, not localhost:5432 — localhost inside a container is the container itself. (What Is an IP Address and a Port?.)
ports: "5432:5432" is only for reaching the database from your host (a GUI client, or running the app outside Docker). Bind it to loopback to avoid exposing it on your network: "127.0.0.1:5432:5432".
Startup order: health checks, not sleeps
depends_on alone only waits for the container to start, not for Postgres to accept connections. condition: service_healthy with a healthcheck waits for readiness. Your app should still retry connections on boot — in production, nothing guarantees ordering.
Volumes: data vs code
- Named volumes (
pgdata) persist database data acrossdown/up.down -vdeletes them — your "reset the database" button. - Bind mounts (
./src:/app/src) map your source into the container for live editing. (Docker Volumes vs Bind Mounts.)
A common trap with bind-mounting the whole project: your host's node_modules (built for macOS or Windows) overwrites the container's (built for Linux), and native modules crash. Either don't mount node_modules (add an anonymous volume - /app/node_modules), or use watch mode as above, which syncs only the paths you list.
Watch mode
docker compose watch (the develop.watch block) syncs changed files into running containers, restarts, or rebuilds on specific file changes. It avoids the performance problems of bind mounts on macOS and Windows, where file-system sharing between the host and Docker's Linux VM can make large projects sluggish.
Environment and secrets
env_file: .envloads variables from a git-ignored file; commit a.env.example. (What Is an Environment Variable?.)- Compose also reads a
.envfile in the project directory for variable substitution in the compose file itself (image: postgres:${PG_VERSION}) — a different mechanism fromenv_file, which is a common source of confusion. - Local-only passwords like
app/appare fine; never reuse production credentials. (Secrets Management Beyond .env Files.)
Seeding and migrations
Two common patterns:
- A one-off service:
docker compose run --rm app npm run db:migrate. - Postgres's init hook: SQL or shell files mounted into
/docker-entrypoint-initdb.d/run only on first initialisation of an empty data volume — good for extensions and roles, not for evolving schema.
Profiles and overrides
- Profiles (
profiles: ["worker"]) keep optional services out of the defaultup. compose.override.yamlis merged automatically — handy for personal tweaks (git-ignore it) — and-flets you layer files explicitly.
Match production where it matters
Pin the same major versions as production (postgres:17, not postgres:latest), and the same extensions. Many "works locally" bugs are version drift. (Why Does My App Work Locally but Not in Production?.) Compose is for development; production usually runs the same images on a platform rather than the same compose file.
Pitfalls
- Port already in use — a native Postgres already on 5432. Change the host side:
"5433:5432". - Stale images after dependency changes —
docker compose buildorup --build. - Disk usage — old images and volumes accumulate;
docker system dfthen prune deliberately. - Line endings on Windows breaking shell scripts in containers — use LF in
.gitattributes.
EasySpawn workspaces are persistent Docker environments with managed Postgres, and MySQL, MongoDB, and Redis on the Team plan — so the stack your compose file describes is already running when Claude Code starts. See pricing or join the waitlist.
Related: What Is a Dev Container? · Writing a Production Dockerfile for a Node.js App · Onboard a New Developer on Day One
Keep reading
Getting AI to Write Tests That Actually Catch Bugs
Ask an AI for tests and you'll get plenty: tests that mock everything, assert nothing useful, and pass no matter what the code does. How to get tests that fail when behaviour breaks — what to test, how to prompt, how to check a test is real, and how tests become the agent's safety net.
What Is a Dev Container? devcontainer.json Explained
A dev container defines your development environment as code — the tools, versions, services, and settings a project needs — so it runs the same on every machine and in the cloud. What goes in devcontainer.json, how it differs from a Dockerfile, and where it falls short.