skip to content

Compose File Model

A compose.yaml declares services with image or build, ports, command and environment, plus top-level networks, volumes, configs and secrets. Interviewers start here because the Compose Specification retired the version key and resources are named per project.

on this pageshow

explore

questions

1

What problem does Docker Compose solve, and what are the main top-level sections of a compose.yaml file?

level: juniorimportance: must knowfreq 70%

answer

  1. one file, one host, one project
  2. services / networks / volumes
  3. project name = directory = resource prefix
  4. up converges, down keeps named volumes
  5. `compose config` prints the merged truth

basics

~20 s

Compose runs a multi-container stack from one declarative YAML file instead of many long docker run commands. The main top-level keys are services (the containers), networks, and volumes. docker compose up creates everything; down removes it.

solid answer

~50 s

Compose turns "start a database, a cache and my app, on a shared network, in the right order" from a pile of `docker run` flags in someone's shell history into one reviewable file in the repo. A `compose.yaml` has a few top-level keys: - **services** — one entry per container role, each with `image` or `build`, plus `ports`, `environment`, `volumes`, `depends_on`, `healthcheck`, `command`. - **networks** — user-defined networks; if you declare none, Compose still creates a `default` network for the project and attaches every service. - **volumes** — named volumes so data survives container recreation. - optionally **secrets** / **configs**. Everything belongs to a *project* (default: the directory name, override with `-p`), which prefixes the created container, network and volume names and lets Compose reconcile a re-run instead of duplicating it. Key commands: `up -d`, `ps`, `logs -f`, `exec`, `down` (add `-v` to drop named volumes), and `config` to print the fully merged file.

code

yaml · 19 lines
yaml
services:
  api:
    build: .
    ports:
      - "8080:8080"
    environment:
      DB_URL: jdbc:postgresql://db:5432/app
    depends_on:
      - db

  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: devpassword
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:

go deeper

for a junior

Be able to name services/networks/volumes, write a two-service file, and use up/down/logs/exec correctly.

for a middle

Explain the project concept, how re-running up converges instead of duplicating, and what down does and does not delete.

for a senior

Talk about the file as a reviewed artifact shared by devs and CI, and about compose config as the debugging tool when interpolation or file merging surprises you.

for a principal

Frame Compose as the single-host end of the orchestration spectrum and be explicit about where a team should stop using it and what it buys them in dev/CI parity.

## The problem it solves A realistic application is not one process. Locally you need the web service, a database, maybe a cache and a message broker, all on a shared network, with published ports, environment variables and mounted volumes. Doing that with plain `docker run` means a long flag list per container, started in the right order, typed the same way on every laptop and CI runner. Those commands live in shell history, drift between machines, and cannot be code-reviewed. Docker Compose replaces them with a single declarative YAML file committed next to the code. ## The file The default filename is `compose.yaml` (Compose also accepts `compose.yml`, `docker-compose.yml`, `docker-compose.yaml`). Top-level keys: - **`services`** — the heart of the file. Each key under it is a *service* (a role, such as `api` or `db`), and its value says how to create containers for it: `image:` to pull a published image or `build:` to build from a Dockerfile in the repo, plus `ports`, `environment`/`env_file`, `volumes`, `command`, `depends_on`, `healthcheck`, `restart`, resource limits, and so on. - **`networks`** — user-defined bridge networks. Declaring them is optional: if you declare none, Compose creates one network named `<project>_default` and attaches every service to it. - **`volumes`** — named volumes that Docker manages, so a database's data outlives the container that wrote it. - **`secrets`** and **`configs`** — files mounted into containers, mostly used for parity with Swarm-style deployments. The old `version: "3.8"` header is obsolete under the Compose Specification; modern Compose ignores it and warns. ## Projects: how Compose knows what it owns Every `docker compose` invocation operates on a *project*. The project name defaults to the sanitized directory name and can be set with `-p` or `COMPOSE_PROJECT_NAME`. Compose labels every resource it creates with that project name and derives names from it: containers like `myapp-db-1`, network `myapp_default`, volume `myapp_pgdata`. This is why re-running `up` does not duplicate your stack: Compose lists existing resources with the project label, compares them to the file, and *converges* — it recreates only the containers whose image or configuration changed and leaves the rest running. It is also why two checkouts of the same repo in different directories get two fully independent stacks, and why running Compose from the wrong directory can appear to "lose" your containers. ## Lifecycle commands worth knowing - `docker compose up` — build/pull images, create networks and volumes, create and start containers. `-d` detaches; `--build` forces a rebuild; `--wait` blocks until healthchecks pass. - `docker compose down` — stop and remove containers and the project's networks. It **keeps named volumes** unless you add `-v`, which is the usual reason "my database still has yesterday's data". - `docker compose ps`, `logs -f <svc>`, `exec <svc> sh` — inspect a running stack. - `docker compose run --rm <svc> <cmd>` — a one-off container (migrations, a shell) that does not join the published ports by default. - `docker compose config` — renders the fully merged and interpolated file. This is the single most useful debugging command: it shows exactly what Compose thinks you wrote after variable substitution and file merging. ## Compose v1 vs v2 Compose v2 is a Go plugin invoked as `docker compose` (with a space) and is the supported implementation; v1 was a separate Python `docker-compose` binary and is end-of-life. Practical differences: v2 separates name parts with `-` instead of `_`, supports `profiles`, `--wait`, and tracks the Compose Specification. Interview answers should use the v2 spelling. ## What Compose is not Compose orchestrates containers on **one Docker host**. It has no scheduler, no multi-node placement, no rolling-update controller and no self-healing beyond `restart:` policies. Multi-node scheduling belongs to a cluster orchestrator. Its sweet spot is local development, integration-test stacks in CI, demos, and small single-VM deployments. ## Typical junior mistakes Putting `build:` and `image:` semantics backwards (with both, `image:` names the tag the build gets); publishing every service's ports when only the edge needs to be reachable from the host; and assuming `up` re-reads a changed Dockerfile without `--build`.

  • What exactly happens when you run `docker compose up` a second time without changing anything?
    Compose reads the file, finds the existing resources by project label, and converges rather than recreating. Containers whose image ID and effective configuration are unchanged are left running (or started if stopped); only the ones whose config hash or image changed are recreated. Networks and named volumes that already exist are reused.
  • Does `docker compose down` delete my database data?
    Not by default. `down` removes containers and the project's networks but leaves named volumes in place, so the next `up` reattaches the same data. Passing `-v` (or `--volumes`) removes the named volumes declared in the file, which is destructive. Bind-mounted host directories are never removed by Compose.
  • If both `build:` and `image:` are set for a service, what does Compose do?
    It builds from the `build` context and tags the resulting image with the value of `image:`. That is the standard way to give a locally built image a stable name so it can be pushed or reused, rather than a choice between pulling and building.

A docker run command is a spoken instruction; a compose file is the written recipe — same dish, but reviewable, repeatable, and it does not depend on who is in the kitchen.

saying these in an interview costs you the question

  • Saying Compose is a clustering or multi-host tool — it orchestrates containers on a single Docker host.
  • Claiming `docker compose down` wipes all data; named volumes survive unless `-v` is passed.
  • Still writing `version: "3"` at the top and treating it as required.
  • Assuming `up` rebuilds the image after a Dockerfile edit without `--build` or a changed build context.
  • Confusing `docker compose run` with `up`: `run` starts a one-off container and does not publish the service's ports by default.

context

open as a page