Docker Compose YAML: Model the Application, Not Just the Containers

Docker Compose YAML: Model the Application, Not Just the Containers

A compose.yaml file is more than a launch script written in YAML. It is a readable, reproducible model of an application: which services exist, which components may talk to one another, which data must survive container replacement, and what needs to happen before a dependent process starts. Docker’s current recommendation is the Compose Specification. The former 2.x and 3.x file formats were merged into that specification, so a new file should be built around top-level services and, where needed, networks and volumes rather than around a legacy format label.

Give each concern a clear home

Under services, define the containers that perform real application roles. A service name is also useful network-facing vocabulary: containers sharing a network can discover a service by that name, so names such as api, cache, and worker make the model easier to operate. When no network is declared, Compose creates a project-scoped default network and connects services to it. That is convenient for a small local setup, but it should be an intentional decision. A public-facing proxy and an internal datastore often should not share the same network. Define separate networks and attach only the bridging service to both when you need a deliberate boundary.

Use a top-level named volume for data that must persist beyond an individual container. Each service that needs it must explicitly mount it. A bind mount, by contrast, maps a host path and is often the right development tool for source files. They are not interchangeable: treating host paths as durable application storage can make a deployment depend on machine-specific directories, while treating every mount as disposable can lose state when containers are replaced.

This secret-free example demonstrates the structural pieces. The worker starts only after Redis reports healthy, both services share an internal network, and app-data is a named volume that survives a service recreation.

services:
  worker:
    image: busybox:1.36
    command: ["sh", "-c", "while true; do sleep 3600; done"]
    depends_on:
      cache:
        condition: service_healthy
    networks:
      - internal
    volumes:
      - app-data:/var/lib/example

  cache:
    image: redis:7-alpine
    command: ["redis-server", "--save", "", "--appendonly", "no"]
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 10
    networks:
      - internal

networks:
  internal:

volumes:
  app-data:

In an application file, add the specifics this minimal model intentionally leaves out: an appropriate image version, carefully scoped published ports, mount permissions, and resource controls supported by the chosen platform. For service-to-service traffic, use the service name and the container port—not localhost. From inside a container, localhost means that same container.

depends_on is not a readiness guarantee

The short form, depends_on: [cache], is frequently overread. Compose guarantees the dependency service is started before the dependent service; it does not wait for the dependency to be ready to accept application traffic. A database process can be running while it is still recovering, applying initialization work, or otherwise unable to serve the client.

If readiness matters, define a meaningful healthcheck on the dependency and use long-form depends_on with condition: service_healthy, as in the example. Compose then waits for that healthcheck to pass before it starts the dependent service. service_completed_successfully is another useful condition when a separate, one-time initialization service must finish first.

That still is not a substitute for application resilience. A health check only proves what its command tests; it does not prove every business-level dependency is ready. Clients should handle connection failures and reconnect after restarts, and teams should choose checks that test the actual capability the next service requires. Dependency ordering helps make startup deterministic; it is not a distributed-systems recovery plan.

Render and validate before up

Checking YAML indentation alone misses the useful part of Compose validation. Compose can merge files passed with -f, resolve variables, and expand shorthand into the canonical model it will apply. Inspect that rendered model before deployment:

docker compose -f compose.yaml config
docker compose -f compose.yaml config --quiet
docker compose -f compose.yaml config --services

The first command prints the normalized configuration. --quiet validates without output, which makes it a practical CI gate. --services gives a quick check that the intended services are present. When interpolation is involved, review the values and sources that your deployment environment supplies rather than relying on an accidental local .env file. Keep credentials and tokens out of the YAML and repository; configuration validation does not make embedded secrets safe.

A Compose file becomes durable operational documentation when it answers four questions unambiguously: what runs, where it can communicate, what data it keeps, and what readiness condition governs startup. Start there, validate the rendered configuration, and the file will scale much better than a collection of ad hoc docker run commands.

Primary source

koen