open atlas
↑ Back to track
Docker, containers as a system DOCK · 06 · 01

The Compose model: a project, a network, and services that find each other by name

Compose turns a multi-container topology into a declarative YAML file: a project namespace, a default network with DNS by service name, named volumes, and a desired-state convergence model. The file is the local environment, not a wiki of run commands.

DOCK Senior ◷ 16 min
Level
FoundationsJuniorMiddleSenior

The onboarding doc was a wiki page titled “How to run the stack locally” and it was 41 numbered steps long: create a network by hand, run Postgres with the right port and password, run Redis, build the API image, run it with eleven environment flags pointing at the other two by IP, then the worker, then the frontend. Every new hire lost a day to it, and the page drifted — step 17 still referenced a container name that had been renamed in step 9. A staff engineer deleted the whole page and replaced it with one file, compose.yaml, plus one command. The IPs disappeared because services now found each other by name over a project network the tool created automatically. The eleven flags collapsed into an environment block. The thing that had been tribal knowledge spread across a wiki became a diff in version control that a reviewer could read top to bottom. The win was not fewer keystrokes — it was that the environment stopped being a sequence of commands someone had to perform correctly and became a description of a desired state the tool converges to.

A project is a namespace

A Compose file describes one project, and the project name (the directory name by default, or -p) is the prefix on everything Compose creates: containers, networks, volumes, labels. That namespace is what lets compose up and compose down be idempotent — Compose looks up resources by the com.docker.compose.project label, not by guessing, so re-running up reconciles the existing project rather than spawning duplicates. Two checkouts of the same repo in two directories get two independent projects with two networks and two database volumes, no collision. The model is desired-state convergence, not a script: you declare the set of services you want running, and up diffs that against what exists and makes the minimum changes — start what is missing, recreate what changed, leave what already matches. This is why editing one service’s image and re-running up recreates that one container and not the whole stack.

# compose.yaml — the whole local environment as data
services:
  api:
    build: .
    environment:
      DATABASE_URL: postgres://app:secret@db:5432/app   # "db" is a DNS name
      REDIS_URL: redis://cache:6379
    ports:
      - "8080:8080"
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: app
    volumes:
      - pgdata:/var/lib/postgresql/data   # named volume survives `down`
  cache:
    image: redis:7

volumes:
  pgdata:

Services find each other by name, not by IP

When you run compose up, Compose creates a default network for the project (named <project>_default) and attaches every service to it. On that network Docker runs an embedded DNS server, and every service is resolvable by its service nameapi reaches Postgres at the hostname db, port 5432, with no IP anywhere in the config. This is the single biggest reason the wiki of run commands disappears: the brittle part of manual orchestration was always wiring containers to each other by address, and addresses change on every restart. Service-name DNS makes the wiring stable and readable. Two details bite at senior level: the DNS name is the service key, and container_name does not change it — override the container name and the DNS entry is still db. And the published ports: mapping ("8080:8080") is for the host to reach the container; service-to-service traffic uses the container port directly over the project network and needs no ports: entry at all. A common mistake is publishing the database port to the host “so the API can reach it” — the API reaches it over the network by name regardless; publishing it only exposes the DB to your laptop.

Quiz

In the file above, the api service connects to Postgres using the hostname db. Where does that hostname come from, and what resolves it?

Why this works

Why is convergence better than a startup script? A script encodes a path — do this, then this — and a path can be half-run: it dies at step 17 and leaves the environment in a state no one designed. Convergence encodes a destination: up always drives toward the declared set, so an interrupted up re-run finishes the job rather than compounding the mess, and down followed by up is a clean rebuild because the desired state is the single source of truth. The file says what should be true, and the tool’s job is to make reality match — the same mental model as Kubernetes manifests or Terraform, learned cheaply on your laptop.

Named volumes are the durable layer

Containers are disposable; the data under them usually is not. A named volume (pgdata above) is a Compose-managed, project-scoped storage object that outlives container recreation: compose up after an image change recreates the db container but re-mounts the same pgdata, so your local database keeps its rows. The sharp edges are the destructive commands: plain compose down removes containers and the default network but keeps named volumes, while compose down -v deletes them — that flag is the difference between “restart the stack” and “wipe the local database.” Anonymous volumes (a bare path with no name) are the trap: they are created fresh on each recreate and orphaned, so a service that should persist data but uses an anonymous volume silently loses it on the next up. Name the volume, mount it at the data path, and down/up becomes safe to run all day.

Quiz

A developer runs compose down then compose up after changing the Postgres image tag, and is surprised their seeded local data is gone. Which single fact explains the most likely cause?

Recall before you leave
  1. 01
    Explain how one Compose service reaches another without any IP address in the configuration, and one subtlety that surprises people.
  2. 02
    Contrast a startup script with Compose's convergence model, and explain how named volumes interact with down and down -v.
Recap

Compose collapses a multi-container local environment from a wiki of run commands into a single declarative file describing one project. The project name namespaces everything Compose creates and lets up and down be idempotent: up diffs the declared desired state against what exists and makes the minimum change, so editing one service recreates one container, not the stack. Services find each other over a default project network where Docker’s embedded DNS resolves each service by its service key — api reaches Postgres at the hostname db, no IP anywhere, and container_name does not change that DNS name. Published ports are only for the host; service-to-service traffic uses the container port over the project network, so publishing a database port is exposure, not a requirement. Named volumes are the durable layer that survives container recreation; plain compose down keeps them and compose down -v deletes them, while anonymous volumes are recreated empty on every up and silently lose data. The payoff is conceptual, not cosmetic: the environment becomes a reviewable, version-controlled description of a desired state — the same mental model as Kubernetes manifests, learned on a laptop. Now when you see a teammate’s “just run these twelve commands” setup doc, you know what to replace it with — and what will stop breaking on the next OS upgrade or name collision.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 5 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.