Real projects don’t stop at one container — the app, a database, a cache, and a queue all need to be up. Compose describes that in a single YAML file and starts it with docker compose up.

compose.yaml

  services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgres://postgres:secret@db:5432/app
      REDIS_URL: redis://cache:6379
    volumes:
      - .:/app
      - /app/node_modules      # protect it from being shadowed by the host
    depends_on:
      db:
        condition: service_healthy

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

  cache:
    image: redis:7-alpine

volumes:
  pgdata:
  

Containers find each other by service name. That’s why the app above connects to db rather than localhost.

Commands

CommandAction
docker compose up -dStart everything in the background
docker compose up --buildRebuild images, then start
docker compose psStatus
docker compose logs -f appLogs for one service
docker compose exec app shShell into a running container
docker compose run --rm app npm testOne-off command
docker compose downStop and clean up
docker compose down -vAlso delete volumes (wipes data)

Splitting environments

Use a base file plus an override.

  # compose.yaml (shared) + compose.override.yaml (local, applied automatically)
docker compose up

# with a production configuration
docker compose -f compose.yaml -f compose.prod.yaml up -d
  

compose.override.yaml merges automatically by filename, so local-only volume mounts and debug ports belong there.

The .env file

  # .env
POSTGRES_PASSWORD=secret
APP_PORT=3000
  
  services:
  app:
    ports:
      - "${APP_PORT}:3000"
  

Always gitignore .env and commit a .env.example instead so the required keys are discoverable.

Frequent problems

SymptomFix
App can’t reach the databaseUse the service name, not localhost
The database isn’t ready yetPair depends_on with a healthcheck
Code changes don’t applyCheck the source volume mount and hot reload settings
node_modules disappearsProtect it with an anonymous volume (/app/node_modules)
Port already in useChange the port value in .env

Next

When containers spread across machines you need an orchestrator → kubectl and k9s

Last updated 19 Aug 2026, 00:00 UTC. history