Server

Docker Compose for Local Development

By Prabath Thalangama· October 24, 2026· 3 min read
#docker#docker-compose#local-dev

Introduction

Docker Compose is at its best as a local development orchestrator: one docker compose up brings up your app plus its database, cache, and message queue, wired together, with your source code live-mounted. Here's a setup that actually works day to day.

A realistic compose.yaml

services:
  api:
    build:
      context: .
      target: dev
    command: npm run dev
    volumes:
      - ./src:/app/src              # live reload
      - /app/node_modules           # don't shadow container's node_modules
    ports:
      - "3000:3000"
    env_file: .env.dev
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started

  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: devpass
      POSTGRES_DB: app
    volumes:
      - db-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 3s
      timeout: 3s
      retries: 10
    ports:
      - "5432:5432"

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

  worker:
    build: { context: ., target: dev }
    command: npm run worker
    env_file: .env.dev
    depends_on:
      db: { condition: service_healthy }
    profiles: ["workers"]           # only starts with --profile workers

volumes:
  db-data:

Key techniques

Healthchecks + depends_on: condition. Plain depends_on only waits for the container to start, not to be ready. With condition: service_healthy the API won't start until Postgres answers pg_isready. No more "connection refused" on the first boot.

The node_modules anonymous volume. Bind-mounting ./ over /app hides the node_modules the image built. The - /app/node_modules line (no host path) keeps the container's copy.

Profiles. Services with a profiles: key are skipped unless you pass --profile <name>. Keep heavy or occasional services (workers, mailhog, elasticsearch) out of the default up.

Override files. compose.yaml is committed; compose.override.yaml is auto-merged and can be gitignored for personal tweaks. docker compose -f compose.yaml -f compose.prod.yaml for a different target.

Everyday commands

docker compose up -d                  # start in background
docker compose up --build api         # rebuild just api
docker compose --profile workers up   # include profiled services
docker compose logs -f api
docker compose exec db psql -U postgres app
docker compose down                   # stop + remove containers/networks
docker compose down -v                # ...also delete named volumes (fresh DB)
docker compose ps
docker compose watch                  # sync files / rebuild on change (Compose 2.22+)

Verification and troubleshooting

  • depends_on ignored — you're on Compose v1 (docker-compose, hyphen) or the long-form condition: syntax needs Compose v2. Use docker compose (no hyphen).
  • Code changes not reflected — bind mount path wrong, the process isn't a watcher (node dist/main.js vs npm run dev), or on macOS/Windows the FS event didn't propagate — use compose watch or a polling watcher.
  • node_modules errors after switching branches — the anonymous volume is stale. docker compose down -v or docker compose up --build --force-recreate.
  • Port already allocated — another compose project or a local service on 5432/6379. Change the host side of ports: or stop the other.
  • DB data gone after down — you ran down -v, or you used an anonymous volume instead of the named db-data.
  • Healthcheck never passes — test command wrong for the image (pg_isready path, missing shell in alpine). docker inspect --format '{{json .State.Health}}' <container>.
  • Slow bind-mount performance (macOS) — use :delegated, or mount only what changes, or move to compose watch sync.
PrabathStuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.