NodeJSDocker Compose

Docker Compose

A real app isn't just Node — it needs Postgres, Redis, maybe a queue. Getting all of those running consistently, with the right versions, proper networking between services, and initialization in the right order, is painful to manage by hand. Docker Compose solves this with a single docker-compose.yml file that declares every service, how they connect, and what volumes they use — then docker compose up starts the whole stack with one command. It's the standard for local development environments and integration testing, and understanding it also helps you reason about production container orchestration.

A full-stack compose file

docker-compose.yml

YAML
services:
  api:
    build: .                         # build from the local Dockerfile
    ports:
      - "3000:3000"
    environment:
      NODE_ENV: development
      PORT: 3000
      DATABASE_URL: postgres://dev:devpass@db:5432/appdb   # 'db' = the service name
      REDIS_URL: redis://cache:6379
    depends_on:
      db:
        condition: service_healthy   # wait until Postgres is ready, not just started
      cache:
        condition: service_started
    volumes:
      - ./src:/app/src                # mount source for live reload (dev only)

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: dev
      POSTGRES_PASSWORD: devpass
      POSTGRES_DB: appdb
    volumes:
      - pg_data:/var/lib/postgresql/data   # persist data between restarts
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U dev -d appdb"]
      interval: 5s
      retries: 5

  cache:
    image: redis:7-alpine
    volumes:
      - redis_data:/data

volumes:
  pg_data:
  redis_data:
Each `service` is a container; services reach each other by name on a shared network — `db:5432` resolves to the Postgres container
Compose creates a **private network** for all services in the file, and each service's **name becomes its hostname** on that network. So the Node service connects to Postgres at `db:5432` (the service name, not `localhost`) and to Redis at `cache:6379` — and these resolve automatically with no DNS configuration. `depends_on` controls start order: `service_healthy` waits for the Postgres health check to pass (the database is actually accepting connections) rather than just "the container started" — avoiding the race where your app tries to connect before Postgres finishes initializing. Named **volumes** (`pg_data`, `redis_data`) persist data across `docker compose down` and back up; without them every `down` wipes the database. The `volumes` key at the bottom declares them so Compose manages them.
Key commands

Bash
docker compose up                # start all services (logs in terminal)
docker compose up -d             # start in the background (detached)
docker compose up --build        # rebuild image before starting (after Dockerfile changes)
docker compose logs -f api       # tail logs of a specific service
docker compose exec api sh       # open a shell inside the running api container
docker compose down              # stop and remove containers (volumes persist)
docker compose down -v           # also delete named volumes (⚠ destroys data)
`up` starts the stack, `exec` gets you a shell inside a container, `down` tears it down — use `-v` only when you want a clean slate
The daily Compose workflow is short. **`docker compose up`** (or `up -d` to background it) starts everything. **`--build`** forces an image rebuild — you need this after changing the Dockerfile or production dependencies; without it Compose uses the cached image. **`exec api sh`** opens an interactive shell inside the running container — invaluable for inspecting the environment, running database migrations, or debugging without restarting. **`down`** stops and removes containers but keeps volumes (your database data survives). **`down -v`** also deletes volumes, giving you a completely clean state — useful for CI or resetting a broken dev database, but destructive, so confirm before running it. Most teams have a simple `npm run up` / `npm run down` alias in `package.json` wrapping the common flags.
Dev vs production compose files

Bash
# Override with a second file — only the differences go in docker-compose.override.yml:
docker compose -f docker-compose.yml -f docker-compose.prod.yml up

# Common pattern: base file has the services;
# docker-compose.override.yml (auto-loaded) adds dev-only mounts and settings.
Don't use the same Compose file for production as development — dev mounts, hot-reload volumes, and dev secrets don't belong in prod
A single `docker-compose.yml` that works for both development and production is usually a sign that one of them has the wrong settings. Development files typically **mount source code** as volumes (for live reload), set `NODE_ENV=development`, expose extra ports, and use weak local credentials. None of that belongs in production — mounted source bypasses the build, development mode is slow and leaky, extra exposed ports widen the attack surface, and weak passwords are a liability. Compose supports **multiple files** composed with `-f`: put the shared service definitions in `docker-compose.yml` and keep dev-specific overrides in `docker-compose.override.yml` (auto-loaded by default). In production, prefer a proper orchestrator (Kubernetes, ECS) or a PaaS over Compose — Compose has no built-in health-based routing, zero-downtime deploy, or cross-host networking. It excels at local dev; for production, treat the image it builds as the artifact and run that through your real deployment pipeline.
Next
Choose a cloud platform and deploy: [Deploying to the Cloud](/nodejs/cloud-deployment).