Docker Compose is the standard way to run multiple containers with a single command and describe their networks, volumes and depends-on relationships in one file. It is indispensable at every stage from development to production. This guide covers setting up a typical web application stack.

Basic compose.yml

# compose.yml (docker-compose.yml also works)
services:
  app:
    build: .
    ports: ['3000:3000']
    environment:
      NODE_ENV: production
      DATABASE_URL: postgres://app:secret@db:5432/appdb
      REDIS_URL: redis://cache:6379
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: appdb
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ['CMD', 'pg_isready', '-U', 'app']
      interval: 5s
      retries: 10
    restart: unless-stopped

  cache:
    image: redis:7-alpine
    volumes:
      - redisdata:/data
    restart: unless-stopped

  nginx:
    image: nginx:alpine
    ports: ['80:80', '443:443']
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./certs:/etc/letsencrypt:ro
    depends_on: [app]
    restart: unless-stopped

volumes:
  pgdata:
  redisdata:

Essential Commands

# Build + start (detached)
docker compose up -d --build

# Tail logs
docker compose logs -f app
docker compose logs --since 10m

# Restart a single service
docker compose restart app

# Rebuild a service and start it
docker compose up -d --build app

# Status
docker compose ps

# Shut everything down
docker compose down
# Remove volumes too (data is gone!)
docker compose down -v

Environment Files

Do not leave sensitive data in compose.yml. Use a .env file — Compose reads it automatically.

# compose.yml
services:
  app:
    environment:
      DATABASE_URL: ${DATABASE_URL}
      JWT_SECRET: ${JWT_SECRET}

# .env (git-ignore!)
DATABASE_URL=postgres://...
JWT_SECRET=supersecret

# Use a different env file
docker compose --env-file .env.production up -d

Network Management

By default, Compose creates a project-specific bridge network and services find each other by service name (postgres://db:5432). You can define custom networks for isolation.

services:
  frontend:
    networks: [public]
  backend:
    networks: [public, internal]
  db:
    networks: [internal]  # isolated from the outside world

networks:
  public:
  internal:
    internal: true  # no internet access

Volume Types

  • Named volume: pgdata:/var/lib/postgresql/data — managed by Docker, portable
  • Bind mount: ./config:/etc/app — host directory, ideal for development
  • tmpfs: type: tmpfs — RAM only, good for sensitive temp data
services:
  app:
    volumes:
      - ./src:/app/src           # bind (hot reload)
      - node_modules:/app/node_modules  # named
      - type: tmpfs
        target: /tmp

Production Best Practices

services:
  app:
    image: ghcr.io/user/app:v1.2.3  # pinned tag, not :latest
    restart: unless-stopped
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
        reservations:
          memory: 128M
    logging:
      driver: json-file
      options:
        max-size: '10m'
        max-file: '5'
    read_only: true
    tmpfs: ['/tmp', '/var/run']
    security_opt:
      - no-new-privileges:true
    cap_drop: [ALL]

Override Files (Dev vs Prod)

# compose.yml (base)
services:
  app:
    image: myapp:latest
    restart: unless-stopped

# compose.override.yml (loaded by default, for dev)
services:
  app:
    build: .
    volumes: ['./src:/app/src']
    command: npm run dev

# compose.prod.yml
services:
  app:
    image: ghcr.io/user/app:v1.2.3
    environment:
      NODE_ENV: production

# Usage
docker compose up                             # loads the override automatically
docker compose -f compose.yml -f compose.prod.yml up -d

Healthcheck and depends_on

depends_on only guarantees startup order, not that the service is ready. For actual readiness, use condition: service_healthy combined with a healthcheck.

services:
  app:
    depends_on:
      db:
        condition: service_healthy
  db:
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U app']
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 10s

Profiles

services:
  app: {}
  db: {}
  monitoring:
    image: prom/prometheus
    profiles: [monitoring]  # does not start by default

# Usage
docker compose up                          # app, db
docker compose --profile monitoring up     # + monitoring

Conclusion

Docker Compose is the sweet spot for small and medium production where Kubernetes is overkill. You bring up the whole stack with one command and do migrations/deploys in minutes. Perfect for running 5-10 services on a single VPS.

Docker Compose setup

Reach out to KEYDAL for containerizing existing applications and production deployment with Compose. Contact us

WhatsApp