7.3 Using Compose to Manage Multi-Service Projects
Prerequisites: Write a Dockerfile
Estimated time: 20 minutes
Exercise environment: Docker Engine and
docker composeYou are ready to continue when: you can validate a Compose model and explain project naming, service discovery, readiness conditions, and named volume lifecycles
A single container can start. Real projects may also include databases, caches, and background tasks. You don't want to manually retype names, networks, volumes, and startup order each time.
Compose uses a single YAML file to describe an application project. The project name establishes naming boundaries for containers, networks, and volumes, while service names become default DNS names within the network.
A Secure, Cleanable Exercise
First, create an isolated directory, then save the configuration as the preferred filename compose.yaml:
compose_workdir=$(mktemp -d)
compose_project="workshop-ch07-${RANDOM}-${RANDOM}"
cd "$compose_workdir"name: workshop-ch07
services:
writer:
image: busybox:latest
command: ["sh", "-c", "printf 'volume survives container removal\\n' > /data/message.txt"]
volumes:
- workshop-data:/data
reader:
image: busybox:latest
command: ["cat", "/data/message.txt"]
depends_on:
writer:
condition: service_completed_successfully
volumes:
- workshop-data:/data:ro
volumes:
workshop-data:Every command here uses -p "$compose_project" to override the fixed YAML name and isolate this exercise's resources. Keep using the same project name during cleanup; the workshop-ch07 prefix alone does not prove ownership.
version is now a legacy compatibility field. Compose ignores it and uses the current specification, so the new file does not need to include version: "3.8".
First parse the final model, then create the resources:
docker compose -p "$compose_project" config
docker compose -p "$compose_project" run --rm reader
docker compose -p "$compose_project" ps --all
docker compose -p "$compose_project" downYou should see volume survives container removal. down deletes this project's container and default network, but retains the named volumes by default. Verify that the volumes are still present, then explicitly delete them:
docker volume ls --filter "label=com.docker.compose.project=$compose_project"
docker compose -p "$compose_project" down --volumes
cd
ls -ld -- "$compose_workdir"
rm -r -- "$compose_workdir"Do not use down --volumes in database projects that require data retention. Docker documents that plain down retains named volumes by default; before deletion, you must first determine whether the data is still needed.
Compose Managed Objects
| Object | Description |
|---|---|
| Project | A single deployment of an application and its resource boundaries; can be specified at the top level using name or -p |
| Service | A configuration for a group of containers, not equivalent to a specific container instance |
| Network | The boundary for communication between services; services can resolve each other using service names within the same default network |
| Named volume | A storage managed by Docker, independent of the container's lifecycle |
| Bind mount | Mounting a specific host path into a container, directly affected by the host's path and permissions |
Services communicate using container ports, for example, an application connects to db:5432. Publish ports only when the host also needs access; databases typically do not need to be published to the host when they are only used within the same Compose project.
ports:
- "127.0.0.1:18080:8080"This is a service-level fragment that belongs under an actual service definition. With Docker Engine 28+ and the default bridge/NAT network, it restricts the host publishing address to loopback. If a port is occupied, switch to an unused host port. Do not kill an unfamiliar process first.
depends_on Not Equal to Application Readiness
Short syntax depends_on: [db] only guarantees that dependencies start to the running state. Database processes may still be in recovery. When readiness thresholds are needed, configure healthcheck for dependencies, and use:
depends_on:
db:
condition: service_healthyEven so, applications should still implement bounded retries, timeouts, and error logging for transient network failures. Health checks are not a permanent guarantee.
Passwords and Configuration
Do not submit real passwords directly into compose.yaml. Local development can use uncommitted environment files or Compose secrets, while production environments should integrate with a secret management platform. Note that docker compose -p "$compose_project" config may expand environment variables, so de-identification is still required before sharing the output.
secrets:
db_password:
file: ./secrets/db_password.txtThis only defines the top-level secret source. Add secrets: [db_password] under the intended service to make the file available at /run/secrets/db_password inside its container. The secret file itself must be excluded from version control and have restricted permissions.
Volume & Bind Mount
- Databases are well-suited for named volumes: managed by Docker, data persists even after the container is deleted.
- Local source code hot updates often use bind mounts: changes on the host are directly mapped into the container.
- Bind mounts by default may allow containers to modify host files; if read-only is desired, add
:ro. - Volumes are not backups. Still, validation of backup, recovery, migration, and retention strategies is required.
The model is now clear. Finally, complete a round of reversible experiments using a limited name and loopback port. Start and readiness conditions can be checked again Compose Official Documentation.