iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Compose Intro

Docker Compose runs a multi-container app from one YAML file. docker compose up brings everything online; docker compose down tears it down. The default local-dev tool for any non-trivial stack.

compose.yml, services, volumes, networks

EXAMPLE
# 1) Minimal compose.yml
# (Compose V2 uses compose.yml or docker-compose.yml; V1 uses only docker-compose.yml)
services:
    web:
        image: nginx:1.27
        ports:
            - '8080:80'

# Run:
# docker compose up -d        # detached
# docker compose down          # stop + remove
# docker compose ps            # status
# docker compose logs -f       # tail logs
# docker compose restart web

# 2) Real-world stack — Postgres + Redis + app
services:
    app:
        build:
            context: .
            dockerfile: Dockerfile
            target: development                # multi-stage build target
        ports:
            - '3000:3000'
        environment:
            DATABASE_URL: postgres://app:secret@db:5432/myapp
            REDIS_URL:    redis://redis:6379
            NODE_ENV:     development
        depends_on:
            db:    { condition: service_healthy }
            redis: { condition: service_started }
        volumes:
            - ./src:/app/src                    # bind mount for hot reload
            - /app/node_modules                 # anonymous volume — preserves container's node_modules
        develop:
            watch:
                - action: sync
                  path:   ./src
                  target: /app/src

    db:
        image: postgres:16-alpine
        environment:
            POSTGRES_USER:     app
            POSTGRES_PASSWORD: secret
            POSTGRES_DB:       myapp
        volumes:
            - pgdata:/var/lib/postgresql/data
            - ./db/init:/docker-entrypoint-initdb.d:ro
        ports:
            - '127.0.0.1:5432:5432'          # localhost only
        healthcheck:
            test:     ['CMD-SHELL', 'pg_isready -U app']
            interval: 5s
            timeout:  5s
            retries:  5

    redis:
        image: redis:7-alpine
        ports: ['127.0.0.1:6379:6379']
        command: ['redis-server', '--maxmemory-policy', 'allkeys-lru']
        volumes:
            - redisdata:/data

    mailhog:                                     # dev-only mail capture
        image: mailhog/mailhog:latest
        ports:
            - '127.0.0.1:1025:1025'              # SMTP
            - '127.0.0.1:8025:8025'              # web UI

volumes:
    pgdata: {}
    redisdata: {}

# 3) Service discovery
# Within compose, services find each other by NAME:
#   db, redis, app, mailhog
# 'db:5432' → resolves to the db container's IP
# No need to know IPs; Docker DNS handles it.

# 4) Build vs image
#   image: nginx:1.27                — pull from registry
#   build: ./web                      — build from Dockerfile in ./web
#   build:
#       context: ./web
#       dockerfile: Dockerfile.dev
#       target: development           — pick a multi-stage stage
#       args:                          # build-time args
#           NODE_VERSION: '20'

# 5) Env files — keep secrets out of compose.yml
services:
    app:
        env_file:
            - .env
            - .env.local                         # overrides
        environment:
            EXTRA_VAR: 'literal'

# .env file gets auto-loaded into 'docker compose' for variable substitution
# .env.local typically gitignored

# Variable substitution
services:
    app:
        image: myapp:${TAG:-latest}             # default if TAG not set
        ports: ['${PORT:-3000}:3000']

# 6) Profiles — opt-in services
services:
    web:    { image: nginx }
    db:     { image: postgres }
    debug:
        image: alpine
        command: tail -f /dev/null
        profiles: ['debug']                       # only when explicitly enabled

# docker compose --profile debug up
# Without --profile debug, only web + db start.

# 7) Volumes — three types
volumes:
    pgdata: {}                                    # named volume (Docker-managed)

services:
    app:
        volumes:
            - pgdata:/var/lib/postgresql/data    # named — survives container restart
            - ./code:/app                         # bind — host directory
            - /tmp/app                             # anonymous — per-container

# Use named for DB data; bind for source code (hot reload); anonymous rarely.

# 8) Networks — isolation
networks:
    frontend: {}
    backend:
        internal: true                           # no internet access from this network

services:
    web:    { networks: [frontend] }
    api:    { networks: [frontend, backend] }
    db:     { networks: [backend] }              # only api can reach db

# 9) Override files — per-environment config
# compose.yml          (base)
# compose.override.yml (auto-loaded; dev defaults)
# compose.prod.yml     (explicit: -f)

# Dev (default):
docker compose up

# Prod:
docker compose -f compose.yml -f compose.prod.yml up

# 10) Useful commands
docker compose up -d                 # detached
docker compose up --build           # rebuild before up
docker compose down -v              # also remove volumes
docker compose ps
docker compose logs -f app
docker compose exec app sh           # shell into running service
docker compose run --rm app npm test  # one-off command in a NEW container
docker compose restart app
docker compose pause app
docker compose unpause app
docker compose top app
docker compose port app 3000        # which host port is bound

# 11) Health checks — wait for ready, not just started
services:
    db:
        image: postgres:16
        healthcheck:
            test:     ['CMD-SHELL', 'pg_isready -U postgres']
            interval: 5s
            retries:  5
    app:
        depends_on:
            db: { condition: service_healthy }   # waits for db to be HEALTHY, not just started

# 12) Resource limits (Compose V2)
services:
    web:
        deploy:
            resources:
                limits:
                    cpus:   '0.5'
                    memory: 512m
                reservations:
                    memory: 256m

# 13) Watch mode (Compose V2.22+) — auto-reload on changes
services:
    app:
        develop:
            watch:
                - action: sync                       # sync files to container
                  path:   ./src
                  target: /app/src
                - action: rebuild                    # rebuild image on package.json change
                  path:   package.json

# docker compose watch

# 14) Logging driver
services:
    app:
        logging:
            driver: json-file
            options:
                max-size: 10m
                max-file: '3'

# 15) Common patterns

# Dev DB seeded from SQL file
services:
    db:
        image: postgres:16
        volumes:
            - ./db/seed.sql:/docker-entrypoint-initdb.d/seed.sql:ro

# Background worker
services:
    worker:
        build: .
        command: ['node', 'worker.js']
        depends_on: { redis: { condition: service_started } }
        restart: unless-stopped

# Reverse proxy in front
services:
    proxy:
        image: nginx
        ports: ['80:80']
        depends_on: [api, web]
        volumes: ['./nginx.conf:/etc/nginx/nginx.conf:ro']

# 16) Common bugs
#   • Forgetting depends_on → app starts before db, crashes
#   • depends_on without healthcheck → starts when db PROCESS is up, before it's READY
#   • Bind-mount over node_modules → host's node_modules masks container's
#   • Same port in two services → 'port already in use'
#   • Forgetting to expose internal port → can't connect from host
#   • Editing image inside container → lost on restart (changes don't persist)

# 17) Tips
#   ✅ Use .env for secrets; gitignore it; commit .env.example
#   ✅ Bind localhost: '127.0.0.1:5432' for DBs (don't expose to LAN)
#   ✅ Healthchecks → reliable depends_on
#   ✅ Profiles for optional services (debug, e2e tests)
#   ✅ Override files for per-env config
#   ✅ Watch mode for hot reload during dev
#   ✅ docker compose down -v when changing volume schema

Why it matters

Docker Compose is the local-dev stack-in-one-file. Use named volumes for state, bind mounts for source, healthchecks + depends_on for boot order, and an .env file for secrets — that’s 90% of any setup.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
# Define multi-container apps in YAML.
docker compose up
docker compose down
Try it Yourself »

Discussion

Loading…