Skip to content
Docker core 2 min read

Docker Compose

Real applications are rarely a single container. A web app needs a database, maybe a cache, maybe a message broker. Docker Compose lets you describe that whole stack in one declarative docker-compose.yml file and manage it with a single command. It replaces a pile of ad-hoc docker run flags with a versioned, reviewable specification.

The file structure

A Compose file has a few top-level keys. The most important is services — each service becomes one or more containers. volumes and networks declare shared resources the services reference.

services:        # the containers that make up the app
  web: { }
  db: { }
volumes:         # named, persistent storage
  pgdata:
networks:        # custom networks (an implicit default is created too)
  backend:

A web + database example

Here is a complete stack: a Node web service that talks to a Postgres database.

services:
  web:
    build: .                 # build from the local Dockerfile
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgres://app:secret@db:5432/app
    depends_on:
      db:
        condition: service_healthy
    networks:
      - backend

  db:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: app
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 5s
      retries: 5
    networks:
      - backend

volumes:
  pgdata:

networks:
  backend:

Service-to-service networking

Compose creates a network where each service is reachable by its service name as a DNS hostname. That is why the web service connects to db:5432 rather than an IP — Compose resolves db automatically. Only ports listed under ports are published to the host; everything else stays internal to the network.

depends_on and startup order

depends_on controls start ordering. On its own it only waits for the container to start, not for the app inside to be ready. Pairing it with condition: service_healthy (backed by a healthcheck) makes web wait until Postgres actually accepts connections.

depends_on does not retry your application’s connection logic. Always make services resilient to a dependency that is briefly unavailable, even with health checks in place.

Managing the stack

A few commands cover the entire lifecycle:

docker compose up -d        # build (if needed) and start everything detached
docker compose ps           # list the stack's containers
docker compose logs -f web  # follow logs for one service
docker compose down         # stop and remove containers + networks
docker compose down -v      # also remove named volumes (deletes data!)

Output of docker compose up -d:

[+] Running 3/3
 ✔ Network app_backend   Created
 ✔ Container app-db-1     Healthy
 ✔ Container app-web-1    Started

To rebuild after changing your Dockerfile or source, add --build:

docker compose up -d --build

docker compose down -v permanently deletes named volumes. Omit -v to stop the stack while preserving your database data.

Best Practices

  • Keep docker-compose.yml in version control as the single source of truth for the stack.
  • Reference dependencies by service name and avoid hardcoding IPs.
  • Use healthcheck plus depends_on: condition: service_healthy for reliable startup ordering.
  • Store secrets in an .env file or a secrets manager, not inline in the Compose file.
  • Use named volumes for stateful services so data survives down without -v.
  • Split overrides into docker-compose.override.yml for local development versus production.
Last updated June 1, 2026
Was this helpful?