Skip to content
docktionary

Docker

Docker Compose

One YAML file, one command, a whole stack running together. Compose is how you avoid typing five docker run commands by hand every time you sit down to work.

A full example

A typical web app: a Node service, a Postgres database, and a Redis cache, wired together and persisted.

docker-compose.yml
services:
  web:
    build: .
    ports:
      - "8080:3000"
    environment:
      DATABASE_URL: postgres://app:app@db:5432/app
      REDIS_URL: redis://cache:6379
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    restart: unless-stopped

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

  cache:
    image: redis:7-alpine

volumes:
  db-data:

Running it

terminal
docker compose up -d        # build if needed, start everything in the background
docker compose ps             # see what's running
docker compose logs -f web    # follow logs for one service
docker compose exec web sh    # shell into a running service
docker compose down           # stop and remove containers + network
docker compose down -v        # also remove named volumes (deletes data!)

Why "web" can reach "db" by name

Compose creates a dedicated bridge network for the project and attaches every service to it, registering each service name in Docker's embedded DNS. That's the whole mechanism behind postgres://app:app@db:5432/app — db resolves to whichever container is running the db service, even after it's recreated with a new IP. See volumes & networking for the same mechanism without Compose managing it for you.

depends_on and health

Plain depends_on: [db] only waits for the container to start, not for Postgres inside it to be ready to accept connections. A condition: service_healthy check (paired with a healthcheck on that service) makes Compose actually wait until the dependency reports healthy.

Environment-specific overrides

Compose automatically merges docker-compose.yml with docker-compose.override.yml if present — the standard way to keep local-dev tweaks (bind-mounting source, exposing extra ports) out of the file you ship to production.

docker-compose.override.yml
services:
  web:
    volumes:
      - .:/app
    environment:
      NODE_ENV: development
    command: npm run dev

Or keep environments fully separate and pick the file explicitly:

terminal
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

Profiles

Tag optional services with a profile so they only start when asked for — handy for a debugging tool or a seed job you don't want running by default.

yaml
services:
  adminer:
    image: adminer
    profiles: ["debug"]
    ports:
      - "8081:8080"
terminal
docker compose --profile debug up -d

.env files and variable substitution

Compose automatically reads a .env file next to the compose file and substitutes ${VAR} anywhere in the YAML — the standard way to keep secrets and per-environment values out of a file you commit to git.

.env
POSTGRES_PASSWORD=change-me
APP_PORT=8080
yaml
services:
  web:
    ports:
      - "${APP_PORT}:3000"
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}

Add a default so the file still works if the variable isn’t set: ${APP_PORT:-8080}.

Resource limits

The same CPU/memory limits available to docker run are set per-service under deploy.resources — useful even outside Swarm, since docker compose up applies the limits block too.

yaml
services:
  web:
    deploy:
      resources:
        limits:
          cpus: "1.5"
          memory: 512M
        reservations:
          memory: 256M

Taking this to Kubernetes

Compose and Kubernetes solve the same problem for very different environments -- a services: block doesn’t translate line for line into manifests. See coming from Compose for how each piece of a compose file maps across, and where it doesn't.

Using an existing network or volume

By default Compose creates its own network and any named volumes from scratch. external: true instead points a service at a network or volume that already exists — created by another compose project, or by hand — rather than creating a new one.

yaml
services:
  web:
    networks:
      - shared

networks:
  shared:
    external: true
    name: shared-edge-network