Skip to content
SP StackPractices
intermediate By Mathias Paulenko

Override Docker Compose Configs per Environment

How to use Docker Compose override files for environment-specific configurations, covering dev, test, staging, production, profiles, and secrets management.

Topics: devops

Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.

Overview

Docker Compose supports multiple files that merge together. The base docker-compose.yml defines the service structure. Override files customize it per environment — different ports, volumes, environment variables, resource limits, or even additional services. Compose merges files in order: later files override or extend earlier ones. This lets you maintain one base config and customize it for dev, test, staging, and production without duplication.

When to Use

  • Local development with hot reload and debug tools
  • Running tests in isolation with test-specific configs
  • Staging/production with production-like settings (resource limits, no debug)
  • When you need different services per environment (e.g., Mailhog in dev, SES in prod)
  • Managing multi-service applications across environments

When NOT to Use

  • Single environment — a single docker-compose.yml is enough
  • Production deployments — use Kubernetes or ECS, not Docker Compose
  • When configs differ drastically — separate files are clearer than overrides
  • When you need complex orchestration — Compose is for dev/test, not production

Solution

Base compose file

# docker-compose.yml — Base configuration
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - LOG_LEVEL=info
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: myapp
      POSTGRES_PASSWORD: ${DB_PASSWORD:-changeme}
    volumes:
      - db-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U myapp"]
      interval: 5s
      timeout: 3s
      retries: 5
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    restart: unless-stopped

volumes:
  db-data:

Development override

# docker-compose.override.yml — Used by default with `docker compose up`
services:
  app:
    build:
      target: dev  # Multi-stage build dev target
    ports:
      - "3000:3000"
      - "9229:9229"  # Node.js debugger
    environment:
      - NODE_ENV=development
      - LOG_LEVEL=debug
    volumes:
      - .:/app           # Hot reload
      - /app/node_modules  # Prevent host overwrite
    command: npm run dev  # Override entrypoint

  db:
    ports:
      - "5432:5432"  # Expose for local tools
    environment:
      POSTGRES_PASSWORD: devpassword

  # Dev-only services
  mailhog:
    image: mailhog/mailhog:latest
    ports:
      - "1025:1025"  # SMTP
      - "8025:8025"  # Web UI

  adminer:
    image: adminer:latest
    ports:
      - "8080:8080"
    depends_on:
      - db

Production override

# docker-compose.prod.yml
services:
  app:
    build:
      target: production
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - LOG_LEVEL=warn
      - DATABASE_URL=postgres://myapp:${DB_PASSWORD}@db:5432/myapp
    deploy:
      replicas: 3
      resources:
        limits:
          cpus: "1.0"
          memory: 512M
        reservations:
          cpus: "0.5"
          memory: 256M
      restart_policy:
        condition: on-failure
        max_attempts: 3
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

  db:
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - db-data:/var/lib/postgresql/data
    deploy:
      resources:
        limits:
          cpus: "2.0"
          memory: 2G

  redis:
    deploy:
      resources:
        limits:
          cpus: "0.5"
          memory: 256M

Test override

# docker-compose.test.yml
services:
  app:
    build:
      target: test
    environment:
      - NODE_ENV=test
      - DATABASE_URL=postgres://myapp:testpass@db:5432/testdb
    command: npm test
    depends_on:
      db:
        condition: service_healthy

  db:
    environment:
      POSTGRES_DB: testdb
      POSTGRES_PASSWORD: testpass
    # No volume — ephemeral database for tests
    volumes: []

  # Remove redis for tests
  redis:
    profiles:
      - donotstart

Running with different overrides

# Development (default — uses docker-compose.yml + docker-compose.override.yml)
docker compose up

# Production
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

# Test
docker compose -f docker-compose.yml -f docker-compose.test.yml up --abort-on-container-exit

# Build production images
docker compose -f docker-compose.yml -f docker-compose.prod.yml build

# Run specific service
docker compose -f docker-compose.yml -f docker-compose.prod.yml up app

# View merged config
docker compose -f docker-compose.yml -f docker-compose.prod.yml config

Using COMPOSE_FILE environment variable

# .env
COMPOSE_FILE=docker-compose.yml:docker-compose.prod.yml

# Now `docker compose up` uses both files
docker compose up -d

Compose profiles

# docker-compose.yml
services:
  app:
    # Always started
    image: my-app

  db:
    # Always started
    image: postgres:16

  debug-tools:
    # Only started with --profile debug
    profiles:
      - debug
    image: nicolaka/netshoot
    network_mode: "service:app"

  load-test:
    # Only started with --profile loadtest
    profiles:
      - loadtest
    image: grafana/k6
    volumes:
      - ./tests:/scripts
# Start without profiles
docker compose up -d  # Only app and db

# Start with debug profile
docker compose --profile debug up -d

# Start with load test profile
docker compose --profile loadtest up

# Start with multiple profiles
docker compose --profile debug --profile loadtest up

Secrets management

# docker-compose.prod.yml
services:
  app:
    secrets:
      - db-password
      - api-key
    environment:
      - DB_PASSWORD_FILE=/run/secrets/db-password
      - API_KEY_FILE=/run/secrets/api-key

  db:
    secrets:
      - db-password
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db-password

secrets:
  db-password:
    file: ./secrets/db-password.txt
  api-key:
    file: ./secrets/api-key.txt

Environment variables from .env

# .env
DB_PASSWORD=supersecret
REDIS_URL=redis://redis:6379
JWT_SECRET=my-jwt-secret
# docker-compose.yml
services:
  app:
    environment:
      - DB_PASSWORD=${DB_PASSWORD}
      - REDIS_URL=${REDIS_URL}
      - JWT_SECRET=${JWT_SECRET}

Multi-file merge behavior

# docker-compose.yml (base)
services:
  app:
    image: my-app:latest
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - LOG_LEVEL=info
# docker-compose.override.yml
services:
  app:
    # Ports are merged (not replaced)
    ports:
      - "9229:9229"  # Added to 3000:3000
    # Environment is merged
    environment:
      - NODE_ENV=development  # Overrides production
      - DEBUG=true             # Added

Variants

Using extends to reuse service definitions

# docker-compose.yml
services:
  web:
    extends:
      file: docker-compose.base.yml
      service: app
    environment:
      - APP_ROLE=web

  worker:
    extends:
      file: docker-compose.base.yml
      service: app
    environment:
      - APP_ROLE=worker
    ports: []  # No ports for worker

Docker Compose with healthcheck dependencies

services:
  app:
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started

  db:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready"]
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 10s

Watch mode for development

# docker-compose.override.yml
services:
  app:
    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src
        - action: rebuild
          path: package.json
docker compose watch  # Auto-sync changes and rebuild

Best Practices

  • For a deeper guide, see Docker Compose Dev/Prod Split: Separate Environments.

  • Keep docker-compose.yml as the base — don’t put environment-specific config in it

  • Use docker-compose.override.yml for dev — Compose picks it up automatically

  • Use explicit -f flags for non-dev environments — prevents accidental dev overrides

  • Use .env for secrets — don’t hardcode passwords in YAML

  • Use profiles for optional services — avoids starting everything when you don’t need it

  • Use depends_on with condition — ensures services start in the right order

  • Use healthcheck for databases — prevents app from connecting before DB is ready

  • Run docker compose config to verify merged output — catches merge issues

Common Mistakes

  • Forgetting override.yml is automatic: docker compose up always merges docker-compose.override.yml if it exists. Use -f to be explicit.
  • Hardcoding secrets in YAML: passwords in docker-compose.yml end up in git. Use .env or Docker secrets.
  • Not using healthchecks: app starts before DB is ready, causing connection errors. Add healthchecks to dependent services.
  • Using Compose for production: Compose is designed for dev/test. Use Kubernetes, ECS, or Docker Swarm for production orchestration.
  • Not cleaning up volumes: docker compose down keeps volumes. Use docker compose down -v to remove data.

FAQ

How does Docker Compose merge files?

It deep-merges: lists (ports, volumes) are concatenated, maps (environment, labels) are merged with later values overriding earlier ones, and scalars (image, command) are replaced.

What is the default override file?

docker-compose.override.yml. If it exists in the same directory, Compose merges it automatically with docker-compose.yml when you run docker compose up.

Can I use multiple override files?

Yes. Use multiple -f flags: docker compose -f docker-compose.yml -f docker-compose.prod.yml -f docker-compose.secrets.yml up.

What are Compose profiles?

A way to mark services as optional. Services with a profiles list only start when you pass --profile <name>. This lets you keep debug/test tools in the same file without always starting them.

How do I pass environment variables?

Use a .env file in the same directory. Compose reads it automatically. Reference variables with ${VAR_NAME} in the YAML.