I needed to spin up some local services for development. docker-compose makes it easy to orchestrate multiple containers without the faff of managing individual docker run commands with dozens of flags.

My typical docker-compose.yml structure Link to heading

Here’s what a typical development setup looks like for me:

version: '3.8'

services:
  postgres:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: localdev
      POSTGRES_DB: myapp
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data

volumes:
  postgres_data:
  redis_data:

I always include health checks for databases - they prevent your application from trying to connect before the database is ready. Learnt that the hard way after debugging race conditions on startup.

Compose v1 vs v2 Link to heading

If you’ve been using Docker for a while, you might have noticed docker-compose (with a hyphen) is now deprecated in favour of docker compose (no hyphen). The new version is a plugin for the Docker CLI and is significantly faster. I made the switch in 2023 and haven’t looked back.

The good news: most commands work identically. Just drop the hyphen.

Basic commands Link to heading

Start services in the background:

docker compose up -d redis

Start multiple services:

docker compose up -d redis postgres

Start everything defined in the file:

docker compose up -d

Check what’s running:

docker compose ps

View logs:

docker compose logs redis

Follow logs:

docker compose logs -f redis

Stop services:

docker compose down

Stop and remove volumes (careful):

docker compose down -v

Rebuild images:

docker compose build

The -d flag runs in detached mode (background). Without it, logs stream to your terminal, which is useful when you’re first setting things up and want to see what’s happening.

Things I wish I’d known earlier Link to heading

Named volumes are your friend. I used to skip defining volumes and wonder why my database reset every time I ran docker compose down. Named volumes persist data between container lifecycles.

Don’t expose all ports in production. The ports directive publishes them to your host machine. In production with Kubernetes or Swarm, containers can talk to each other over internal networks without exposing ports.

Health checks save debugging time. A container can be “running” but not actually ready to accept connections. Health checks let docker-compose (and orchestrators) know when a service is truly ready.

For more details, the Compose file reference is comprehensive and well-organised. I keep it bookmarked.