All posts
4 min read

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.

dockerdeveloper experiencetoolingintermediate

"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 across down/up. down -v deletes 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: .env loads variables from a git-ignored file; commit a .env.example. (What Is an Environment Variable?.)
  • Compose also reads a .env file in the project directory for variable substitution in the compose file itself (image: postgres:${PG_VERSION}) — a different mechanism from env_file, which is a common source of confusion.
  • Local-only passwords like app/app are 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 default up.
  • compose.override.yaml is merged automatically — handy for personal tweaks (git-ignore it) — and -f lets 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 build or up --build.
  • Disk usage — old images and volumes accumulate; docker system df then 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