Happy Dussehra! May good always win. Happy Diwali! Wishing you a bright year ahead. Merry Christmas! Warm wishes from Vizag. Happy New Year! Here’s to a great year ahead.

Archive ยท Docker

Docker Compose for Multi-Container Apps

Describe a web app, cache and database in one file with networks, volumes, health checks and environment files, and run it reliably on a single host.

3 min read

  • Docker Compose 1.26
  • Docker Engine 19.03.12
  • Redis 6
  • PostgreSQL 12

Real applications are several containers: a web service, a database, a cache, maybe a worker. Typing a long docker run for each is slow and error-prone. Docker Compose describes them all in one YAML file so docker-compose up creates the network, volumes and containers consistently, on a laptop or a single server.

Versions in use on this date. Docker Compose 1.26.2 (the Python-based docker-compose tool) with Docker Engine 19.03.12. Redis 6.0 was released in May 2020. Compose files use version: "3.8" syntax with this release; conditional depends_on health waits are not part of the 3.x format, so the example handles start-up ordering in the application and with restart policies.

The stack

flowchart TB
  U[Users] -->|HTTPS 443| PX[nginx proxy<br/>published port]
  PX --> API[api service<br/>Node.js, 2 replicas by scale]
  API --> DB[(postgres:12<br/>volume pgdata)]
  API --> RD[redis 6 cache<br/>volume redisdata]
  W[worker service] --> RD
  W --> DB
  subgraph NET[Compose network app_default - names resolve]
    PX
    API
    DB
    RD
    W
  end

docker-compose.yml

version: "3.8"

services:
  db:
    image: postgres:12
    restart: unless-stopped
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: app
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:6.0
    restart: unless-stopped
    command: ["redis-server", "--appendonly", "yes"]
    volumes:
      - redisdata:/data

  api:
    build: ./api
    image: myorg/api:${APP_VERSION:-dev}
    restart: unless-stopped
    environment:
      DATABASE_URL: postgres://postgres:${DB_PASSWORD}@db:5432/app
      REDIS_URL: redis://redis:6379
      NODE_ENV: production
    depends_on: [db, redis]
    expose: ["3000"]
    deploy:
      resources:
        limits: { memory: 512M }

  proxy:
    image: nginx:1.18
    restart: unless-stopped
    ports: ["80:80"]
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on: [api]

volumes:
  pgdata:
  redisdata:

.env next to the file (not committed to Git):

DB_PASSWORD=use-a-long-random-value
APP_VERSION=1.4.2

Everyday commands

docker-compose config                     # validate and show the merged config
docker-compose up -d                      # create and start in the background
docker-compose ps
docker-compose logs -f --tail 100 api
docker-compose exec db psql -U postgres app
docker-compose up -d --scale api=2        # needs no fixed container_name/ports on the api service
docker-compose pull && docker-compose up -d       # update images
docker-compose stop; docker-compose start
docker-compose down                       # remove containers and network, keep volumes
docker-compose down -v                    # ALSO deletes volumes (data loss)

Compose names resources with a project prefix (the directory name by default), which is why two copies of the same stack in different directories do not collide. Set -p name or COMPOSE_PROJECT_NAME to choose it.

Start-up order and readiness

depends_on only controls start order, not readiness: Postgres may take several seconds to accept connections after its container starts. Handle it in three ways:

  1. Make the application retry its database connection with backoff (best, and also helps in production restarts).
  2. Use restart: unless-stopped, so an app that failed early simply restarts.
  3. Run a small wait script (wait-for-it.sh) as the entrypoint.

The healthcheck on the database still helps: docker-compose ps shows healthy, and you can use it in monitoring.

Configuration practices

  • Keep secrets out of the file: .env is fine for a lab; for production use Docker secrets (Swarm) or inject from a vault at deploy time.
  • Use docker-compose.override.yml for development settings (bind-mounted source, debug ports); the production file stays clean.
  • Do not publish the database port (5432:5432) unless you need it, and then bind to 127.0.0.1.
  • Pin image tags and keep restart policies explicit.
  • Back up named volumes (see the volumes post), and test the restore.

Where Compose fits

Compose is excellent for development, CI test environments and small single-host deployments. It does not spread containers across servers or reschedule them when a host dies; for that you move to an orchestrator such as Kubernetes. The Compose file is still a useful starting point, because the same services, volumes and environment variables become your Kubernetes manifests.

Troubleshooting

  • ERROR: Version in "./docker-compose.yml" is unsupported: your Compose binary is older than the version: key; upgrade Compose.
  • Service cannot reach another: use the service name as hostname (db), not localhost.
  • Stale image used: docker-compose build --no-cache api or docker-compose pull.
  • Port already allocated: another container or process owns the host port.