3.1The compose.yml file: services, ports, volumes
The essentials in 30 seconds
- Declare the desired state, not the orders: each service has its image (or build), its ports, its variables, its volumes and its network — Compose creates everything from this versioned file.
- One service = image + ports + env + volumes: ports in quotes ("8080:80"), volumes and networks declared at the bottom and created automatically, depends_on for startup order.
- YAML: indentation is law: spaces, never tabs, two spaces per level — docker compose config validates and shows the resolved file before any launch.
Running mini-case — Idir replaces his 200-character lines
Idir, junior DevOps in Algiers, launches his team's mock-up with two 200-character docker run lines copied from a cheat sheet — one forgotten port, and the demo fails. He writes a 25-line compose.yml (web + database, named volume, depends_on), validates it with docker compose config, commits it: from now on, a single file is the source of truth and the whole team starts the same application. No more "how to launch" docs recited from memory.
This same case returns in the lesson, the diagram, the script and the 48h challenge.
1. Declare instead of ordering: the file that is the source of truth
Compose replaces your docker run lines with a versioned file: it describes the DESIRED STATE of the application (which services, which ports, which volumes, which networks) and Compose creates everything — networks, volumes, containers in order. No more 200-character lines copied from memory: the compose.yml is committed, reviewed like code, and is the official launcher of the app. This is the shift from manual action to declarative.
Each service (web, db, cache…) declares its image (or its local build), its ports ("8080:80", quotes mandatory in YAML), its environment, its volumes (pgdata:/var/lib/postgresql/data) and its network. depends_on sets the startup order between services. Volumes and networks are declared at the bottom of the file: Compose creates them automatically on the first up.
2. YAML: indentation is law, config is the judge
Compose speaks YAML: spaces and never tabs, two spaces per level, ":" followed by a space. Almost all Compose errors come from there: broken indentation, unquoted ports (8080:80 read as a sexagesimal number!), a ":" without a space. Write slowly, indent cleanly: that is 80% of the work.
The reflex before every launch: docker compose config. This command validates the file and shows the RESOLVED compose — or the exact error with its line. Idir never runs up on an unvalidated file any more: config first, up second. One file per app, committed, validated: reproducible everywhere.
🔎 Concrete example — Web + database in 25 lines, validated before launch
Idir writes his first web + database stack, password in a variable.
services:
web:
image: nginx:1.27
ports: ["8080:80"]
depends_on: [bdd]
networks: [front]
bdd:
image: postgres:16.4
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes: [pgdata:/var/lib/postgresql/data]
networks: [front]
volumes:
pgdata:
networks:
front:
# Puis : docker compose config (valider), mot de passe dans .env non commité.
Reading: two services, one persistent volume, one network, one startup order.
Lesson: this committed file replaces all the "how to launch" docs.
Section vocabulary
- Service
- A container described in Compose (web, db, cache): image or build, ports, variables, volumes, network.
Simple example: Idir's web service (nginx, published port 8080).
What it is not: a docker run line copied from memory.
Reflex: one application role = one declared service.
- depends_on
- Startup order between services: starts the database before the web, without waiting until it is ready.
Simple example: web depends_on bdd: the database starts first.
What it is not: a guarantee that the database is available.
Reflex: order with depends_on, harden with healthcheck + retry.
- Compose volume
- Named volume declared at the bottom of the file and created automatically: data persistence.
Simple example: pgdata mounted on /var/lib/postgresql/data.
What it is not: data trapped in an ephemeral container.
Reflex: any data to keep = named volume.
- config
- Command that validates the compose file and shows the resolved file — or the exact error.
Simple example: Idir runs config before every up.
What it is not: a blind up on a broken YAML.
Reflex: config first, up second, always.
- YAML
- The indented format of compose.yml: spaces (never tabs), ":" + space, ports in quotes.
Simple example: "8080:80" quoted, two spaces per level.
What it is not: tabs and unquoted ports.
Reflex: spaces, two per level, quotes on ports.
In practice — Write your first stack
- Create a folder with the web + db compose.yml above (password in ${POSTGRES_PASSWORD}, local .env not committed).
- Validate it with docker compose config: read the resolved file shown.
- Fix until zero errors (indentation, quoted ports, ":" + space).
- Commit the file: it is now your official launcher, reviewed like code.
Ready-to-say script — presenting the compose to the team (30 s)
"No more 200-character run lines: a single compose.yml describes web + database — ports, volume, startup order — validated by config and committed. Everyone starts exactly the same application, with no cheat sheet."
Say in a team stand-up; show the resolved config.
Copy-paste template — valid web + database stack
services:
web:
image: nginx:1.27
ports:
- "8080:80"
depends_on:
- bdd
networks:
- front
bdd:
image: postgres:16.4
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- front
volumes:
pgdata:
networks:
front:
# .env local (NON commité) : POSTGRES_PASSWORD=
Freelancer
One compose.yml validated by config and your demo starts identically everywhere.
SME in Algiers
Idir commits the web + database stack: the team starts the same mock-up with no cheat sheet.
Project team
Require green config + committed compose before any shared demo.
Beginner
This week: one web + database stack validated with the config button, then committed.
✓ Best practices
- Put ports in quotes in YAML ("8080:80"), systematically.
- Run docker compose config before every up, and read the resolved file.
- One file per app, committed and reviewed like code.
✗ Common pitfalls
- Tabs in YAML: spaces only, two per level.
- Unquoted ports (8080:80 becomes a sexagesimal number).
- Believing depends_on waits until the database is ready (it only orders).
Key takeaways
- Compose DESCRIBES the desired state, Docker executes: the versioned file replaces run lines.
- SERVICE = image (or build) + quoted ports + env + volumes + depends_on for order.
- YAML: spaces, never tabs, ":" + space — 80% of errors come from there.
- VOLUMES and NETWORKS declared at the bottom, created automatically; config validates before up.
48h challenge — apply tomorrow
Replace 1 manual launch with a validated (config) and committed compose.yml, with the password in a variable.
- My app is described in one versioned file.
- My ports are quoted and my YAML has no tabs.
- My config is green before every up.
Frequently asked questions
Does depends_on wait until the database is ready?
NO: depends_on ORDERS startup (the database starts before the web) without waiting for its availability. If the web connects too early, it fails: robustness comes from healthchecks and application retries (level 4.2). Remember: ordering ≠ waiting. For a simple mock-up, restarting the web is enough; for a real stack, add a reconnection loop at application startup.
Should the compose.yml be versioned, and where do secrets go?
YES, always: the compose.yml is the official launcher of the app, reviewed like code and committed. SECRETS never appear in it hard-coded: use variables (${POSTGRES_PASSWORD}) fed by a local uncommitted .env (or a managed secret in production), and version a .env.example with dummy values documenting the expected keys.
What does "docker compose config" validate before any launch?