# Docker CLI Cheat Sheet — Compose, Networking & Volumes

> **Tool:** Docker CLI
> **Category:** Containers & Orchestration
> **Verified against:** Docker 29.1.5 (Compose v2, built in), flags verified via `docker <cmd> --help` run locally, 2026-08-29
> **Official docs:** https://docs.docker.com/reference/cli/docker/compose/

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

## Compose — starting and stopping a stack

```bash
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

```bash
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

```bash
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

```bash
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

```bash
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

```bash
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

```yaml
# 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

```bash
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"
```

```yaml
# 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:

```bash
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.
