Skip to content
BloGrove
devops

Docker Compose: Local Multi-Container Environments Done Right

Run your app, database, caches, and workers in one command — service definitions, networking, volumes for hot reload, and debugging patterns.

BBloGrove Editorial3 min read
Docker Compose: Local Multi-Container Environments Done Right

Modern applications are never one process — app plus Postgres plus Redis plus a worker, each with versions and env config. Docker Compose turns that entire stack into one declarative file and one command: docker compose up. Everyone's local environment becomes identical, onboarding shrinks from a day of setup docs to installing Docker, and "works on my machine" finally dies. Here's the working subset of Compose that covers real daily use.

The anatomy of a working compose.yaml#

services:
  app:
    build: .                      # build from local Dockerfile
    ports:
      - "3000:3000"               # host:container
    environment:
      DATABASE_URL: postgres://postgres:secret@db:5432/myapp
      REDIS_URL: redis://cache:6379
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - ./src:/app/src            # bind mount for hot reload
    command: npm run dev

  db:
    image: postgres:17
    environment:
      POSTGRES_PASSWORD: secret
    volumes:
      - pgdata:/var/lib/postgresql/data   # named volume persists data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      retries: 5

  cache:
    image: redis:7-alpine

volumes:
  pgdata:

docker compose up starts all three; down stops them. That file is your environment documentation.

Networking: the part that surprises everyone#

Compose creates a private network where services reach each other by service name — that's why DATABASE_URL says host db, not localhost. Inside the app container, localhost means the container itself, not your machine or its neighbors. This single fact prevents the most common beginner hour-lost-debugging session:

  • App → database: postgres://...@db:5432/...
  • Your browser → app: localhost:3000 (via the published port)
  • Service-to-service: always the service name as hostname

Volumes: two kinds, two jobs#

Bind mounts (./src:/app/src) map host folders into containers — the mechanism behind live reload: edit code locally, the running dev server sees it instantly. Never bake development source into images (images are immutable templates).

Named volumes (pgdata:/var/lib/postgresql/data) are Docker-managed persistent storage — how your database survives docker compose down. The lifecycle rules worth tattooing somewhere visible:

  • down keeps named volumes (data safe)
  • down -v deletes them (data gone — sometimes exactly what you want for a fresh slate)

Dependencies and startup ordering#

depends_on alone only controls start order, not readiness — Postgres accepting connections takes seconds after its process starts. Hence the condition: service_healthy pattern above with a healthcheck: app waits until the database actually answers. Without it, first-run apps crash-loop while their DB boots, and everyone learns this lesson the hard way once.

The daily command set#

docker compose up -d          # start detached
docker compose logs -f app    # follow one service's logs
docker compose exec db psql -U postgres  # shell into a service
docker compose up --build     # rebuild after Dockerfile changes
docker compose down           # stop everything (data persists)

Debugging workflow mirrors single-container Docker: check logs first, exec in second, rebuild third.

Production? Mostly no — but not entirely#

Compose is built for local development and simple single-host deployments. Real production orchestration wants restart policies across machines, rolling updates, secret management — Kubernetes territory. But two production-adjacent uses keep Compose relevant everywhere:

  1. CI pipelines spin up app + database via Compose to run integration tests against real services
  2. The compose file doubles as executable architecture documentation — new engineers read it to understand system topology even if they never run it

Interview-ready summary#

Compose = declarative multi-container environments: services defined in YAML, networked by name, persisted with named volumes, ordered by healthchecks. One command up, one command down, identical on every machine. Know the localhost-vs-service-name networking trap and the down -v data cliff, and you're past most Compose questions anyone asks.

Related: Docker foundations · Kubernetes next steps · system design framework

Enjoyed this article?

Share it with your network.

Share

Keep reading

Kubernetes Core Concepts, Explained for Developers
devops

Kubernetes Core Concepts, Explained for Developers

Kubernetes without the buzzwords — pods, deployments, services, ingress, config, and scaling, explained through the problems each solves.

4 min read
Anatomy of a Phishing Attack: How to Spot Them Every Time
security

Anatomy of a Phishing Attack: How to Spot Them Every Time

Phishing works through psychology, not technology — the emotional triggers, tell-tale signs in any message, and a verification routine that catches fakes.

3 min read