3.1 · The compose.yml file: services, ports, volumes

Level 3 · Docker Compose

3.1The compose.yml file: services, ports, volumes

Goal: describe a complete application (web + database) in a readable compose.yml.
Estimated time: 14 min · self-paced reading · If you only have 5 min: read “The essentials in 30s” + the diagram.

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.

Common misinterpretation: "depends_on waits until the database is ready, and ports can be written without quotes". Wrong, twice. depends_on ORDERS startup without waiting for availability — robustness comes from healthchecks and retries (level 4). And 8080:80 without quotes is read as a sexagesimal number by YAML: ports are always quoted. Ordering is not waiting, and quoting is not decorative.
Describe the app instead of ordering Services, volumes, networks, validation ServicesImage + quoted ports + env + volumes + depends_on+Volumes + networksDeclared at the bottom, created automatically+docker compose configValidates and shows the resolved file before up One versioned file describes everything: reproducible everywhere.
Services, volumes, networks, validation
Text version of the diagram (SEO / screen readers / translation): 1) Services: Idir declares web (image, quoted ports) and db (image, password in a variable, volume) instead of two run lines. 2) Foundation: volumes and networks declared at the bottom, created automatically on the first up. 3) Validation: docker compose config shows the resolved file or the exact error before any launch. Lesson: declare the desired state, Compose executes — the committed file replaces the docs.

🔎 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

  1. Create a folder with the web + db compose.yml above (password in ${POSTGRES_PASSWORD}, local .env not committed).
  2. Validate it with docker compose config: read the resolved file shown.
  3. Fix until zero errors (indentation, quoted ports, ":" + space).
  4. Commit the file: it is now your official launcher, reviewed like code.
Your application is described in one versioned, validated file: reproducible by the whole team.

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.

Caution: Database password via the local .env, never hard-coded in the committed compose: ${POSTGRES_PASSWORD} in the file, real value only in the .env (not versioned, provided via a .env.example).

✓ 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.

Section exit checklist
  • 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.
Self-rating 1-5: can someone else start my app without asking me any question?is every password kept out of the committed file?

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.

Check your understanding

What does "docker compose config" validate before any launch?

Additional resources (optional, to go further)

Level 3 examFresh, harder questions to test what you've learned