Verified10 commandsAI-assisted

Compose, Networking & Volumes

.md

Verified against Docker 29.1.5 (Compose v2, built in), flags verified via `docker <cmd> --help` run locally, 2026-08-29 · official docs

Running multi-container stacks with Compose, and the networking/volume primitives underneath them. 🎯

Compose — starting and stopping a stack#

docker compose up -d                                 # start everything defined in compose.yaml, detached
docker compose up -d --build                          # rebuild images first
docker compose down                                    # stop and remove containers + default network
docker compose down -v                                 # also remove named volumes (destroys persisted data)
docker compose stop                                     # stop without removing containers

docker compose (space, no hyphen) is the current, built-in form — the old standalone docker-compose binary is deprecated. down -v is destructive: it deletes any named volumes the stack owns, including database data, so it's not the default even though it's tempting to reach for when "cleaning up."

Compose — inspecting a running stack#

docker compose ps
docker compose logs -f
docker compose logs -f my-service                       # logs for one service only
docker compose exec my-service /bin/bash
docker compose top

Compose — rebuilding and scaling#

docker compose build my-service
docker compose up -d --force-recreate my-service         # recreate even if config hasn't changed
docker compose up -d --scale worker=3                      # run 3 replicas of the worker service

Networks#

docker network ls
docker network create my-network --driver bridge --subnet 172.20.0.0/16
docker network connect my-network my-container
docker network inspect my-network                          # see connected containers + IPs
docker network rm my-network

Compose creates its own bridge network per project automatically, and every service in that compose file can reach every other one by service name (DNS resolution built in) — you rarely need docker network create by hand unless you're connecting containers started outside Compose.

Network drivers and static IP assignment#

docker network create --driver bridge --subnet 172.20.0.0/16 --gateway 172.20.0.1 my-network
docker network create --driver bridge --internal my-network    # no external/outbound connectivity at all
docker network create --driver bridge --attachable my-swarm-net  # let standalone `docker run` containers join a swarm-scope network
docker network connect --ip 172.20.0.10 my-network my-container   # attach with a fixed IP instead of DHCP-assigned
docker network connect --alias db my-network my-container          # give the container an extra DNS alias on that network

The default driver is bridge — an isolated, host-private virtual network with NAT out to the host. host (--network host on docker run) removes network isolation entirely and shares the host's network namespace directly, trading isolation for eliminating NAT overhead; --internal builds a bridge network with no route out, useful for a database tier that should never reach the internet even if a container on it is compromised. overlay and macvlan drivers exist for multi-host/swarm and "container gets its own MAC on the physical LAN" use cases respectively, but aren't relevant to single-host Compose development.

Volumes#

docker volume ls
docker volume create my-data
docker volume inspect my-data
docker run -v my-data:/var/lib/postgresql/data postgres:16   # named volume — Docker-managed storage
docker run -v ./local-dir:/app/data myapp:latest              # bind mount — a real host path
docker volume rm my-data
docker volume prune                                            # remove all volumes not used by any container

A named volume (my-data:/path) is managed by Docker and portable across containers; a bind mount (./local-dir:/path) ties a container directly to a specific path on the host filesystem — reach for a named volume for anything that needs to survive a container being recreated but doesn't need to be human-editable from the host.

A real compose.yaml worth studying#

# compose.yaml
services:
  api:
    build: .
    ports: ["8080:80"]
    environment:
      - DATABASE_URL=postgres://db:5432/myapp
    depends_on:
      db:
        condition: service_healthy      # wait for the healthcheck, not just container start
    networks: [backend]

  db:
    image: postgres:16
    volumes: ["db-data:/var/lib/postgresql/data"]
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 5
    networks: [backend]

networks:
  backend:

volumes:
  db-data:

Important

depends_on: condition: service_healthy is what actually prevents a startup race — plain depends_on: [db] (no condition) only waits for the db container to start, not for Postgres inside it to actually be ready to accept connections. Without the healthcheck-gated condition, api can boot and hit a connection-refused error against a database container that technically "started" a second ago but hasn't finished its own init sequence yet — a classic source of flaky first-boot failures that "just work" on a retry, which masks the real race condition instead of fixing it.

Environment variables and .env files#

docker compose --env-file .env.production up -d      # load variables from a specific file instead of the default .env
docker compose config                                   # print the fully resolved compose config, with all variables substituted — the fastest way to debug "why is this env var not what I expect"
# compose.yaml referencing a .env-provided variable
services:
  api:
    image: myapp:${TAG:-latest}     # ${VAR:-default} syntax — falls back to "latest" if TAG isn't set anywhere

Real-world scenario: debugging "connection refused" between two Compose services#

A service can't reach another one by its service name — the checklist that actually resolves this fastest:

docker compose ps                                    # confirm both services are actually Up (not restarting/exited)
docker compose exec api ping db                        # confirm DNS resolution + basic connectivity from inside the network
docker network inspect $(docker compose ls -q)_default   # confirm both containers are actually attached to the same network
docker compose logs db --tail 50                         # confirm the target service is actually listening, not still initializing

Note

Compose service names are only resolvable within the same Compose-created network — a container started with a bare docker run (outside Compose) can't resolve db by name unless it's explicitly attached to that same network with docker network connect. This is the most common reason "it works when both are in Compose but not when I run one manually" happens.

Common pitfalls#

  • depends_on without a healthcheck condition — see the IMPORTANT callout above; this is the single most common source of flaky Compose startup races.
  • docker compose down -v used as a routine cleanup habit — it destroys named volumes, including database data; reach for plain down (no -v) unless data loss is genuinely intended.
  • Expecting a bare docker run container to resolve a Compose service by name — see the NOTE above.
  • Not checking docker compose config when an env var substitution isn't behaving as expected — it shows the fully resolved config, catching a missing .env file or a typo'd variable name immediately.

When to reach for something else#

Compose is single-host by design — for multi-host orchestration with the same "declare the desired state" mental model, reach for Kubernetes (this same category's kubectl/helm/kustomize cheat sheets). Many teams use Compose specifically for local development against a stack that's genuinely deployed via Kubernetes in staging/production — the two aren't mutually exclusive, they solve different-scale problems.