Docker Compose: Services, Networks, Volumes, Scaling & Health Checks
Docker Compose is a declarative tool for defining and running multi-container applications from a single YAML file (compose.yaml). It is the standard local-development and small-deployment tool for an
Docker Compose is a declarative tool for defining and running multi-container applications from a single YAML file (compose.yaml). It is the standard local-development and small-deployment tool for anything beyond a single container — a typical Spring Boot stack (app + Postgres + Redis + Kafka) is described entirely in one file.
Modern Compose (V2, the docker compose plugin, bundled with Docker Desktop and Docker Engine 28.x) replaces the old standalone Python docker-compose binary; the YAML schema is the same but the CLI is a native Go plugin integrated into the docker command itself.
Compose automatically creates a dedicated user-defined bridge network for the project, so every service can reach every other service by its service name (Chapter 5's DNS resolution applies automatically) — this is the main reason Compose 'just works' for multi-service local stacks.
-
On docker compose up, Compose parses compose.yaml, resolves variable interpolation (${VAR} from shell env or a .env file), and computes a dependency-ordered build/start plan honoring depends_on.
-
Compose creates one network per project (named _default unless overridden) and attaches every service to it by default, enabling name-based service discovery exactly as in Chapter 5.
-
Each services: entry maps to a docker run-equivalent specification; Compose translates volumes:, ports:, environment:, healthcheck:, etc. directly into the corresponding Docker Engine API calls.
-
depends_on controls start order only by default — it does not wait for the dependency to be actually ready (e.g., Postgres accepting connections) unless you add a condition: service_healthy clause tied to that dependency's HEALTHCHECK.
-
docker compose up --scale service=3 runs multiple containers for one service definition; since each needs a distinct container name and (if published) cannot share the exact same host port, scaling typically requires removing fixed host port mappings and routing through a load balancer or reverse proxy container instead.
-
Named volumes and networks declared under volumes:/networks: top-level keys are created once per project and reused across up/down cycles unless you pass -v to down to also remove them.
-
Define services: block with one entry per container (app, db, redis, kafka), each with image or build:, ports:, environment:, volumes:, and depends_on:.
-
Define a healthcheck: on the database service so dependent services can wait for real readiness, not just process start.
-
Define depends_on with condition: service_healthy on the app service so it only starts after the database reports healthy.
-
Define top-level volumes: for named volumes (pgdata) so data persists across docker compose down (without -v) and subsequent up.
-
Run docker compose up -d — Compose creates the network, volumes, then starts containers honoring dependency order and health conditions.
-
Run docker compose logs -f app to tail just the app service's logs, or docker compose ps to see current status of every service.
-
Run docker compose down to stop and remove containers and the project network, while named volumes persist; add -v to also delete volumes (destructive).
-
Local development environment replicating production-like topology: app + Postgres + Redis + Kafka, started with a single command, identical for every developer on the team.
-
Integration testing in CI: docker compose up -d, run the test suite against the stack, then docker compose down -v for a clean teardown every run.
-
Small-scale production or staging deployments on a single VM where full Kubernetes is unnecessary overhead — Compose with restart policies and healthchecks can run a real (if not highly-available) production workload.
-
Demonstrating microservice interaction patterns (service discovery, environment-based config, health-gated startup ordering) in interviews and architecture discussions using a runnable, inspectable example.
-
Always define healthcheck: for stateful dependencies (databases, brokers) and gate dependents on condition: service_healthy — relying on depends_on alone only orders container start, not readiness.
-
Keep secrets out of compose.yaml directly; use a .env file (gitignored) for local dev, and Docker secrets or external secret managers for anything beyond local development (Chapter 7).
-
Use named volumes for any service holding state (databases, message queues) so docker compose down (without -v) is a safe, non-destructive way to tear down and rebuild the stack.
-
Split large Compose setups using multiple files (docker-compose.yml plus docker-compose.override.yml or -f chaining) to separate base config from environment-specific overrides.
-
Assuming depends_on waits for the dependency to be ready — by default it only waits for the container to start, not for the application inside it to finish initializing.
-
Publishing the same host port for a scaled service (ports: "8080:8080" with --scale app=3) — this fails immediately since only one container can bind a given host port; scaled services need a load balancer in front and should use expose instead of ports for the scaled service itself.
-
Hardcoding environment-specific values (passwords, hostnames) directly in compose.yaml instead of using variable interpolation from a .env file or the shell environment.
-
Forgetting that docker compose down without -v leaves named volumes (and their data) behind — useful most of the time, but surprising when you expect a truly clean slate.
-
Use docker compose build with BuildKit caching (default in modern Compose) to avoid rebuilding unchanged service images on every up --build.
-
Limit per-service resources (deploy.resources.limits in Compose V2) during local development to catch resource-hungry services before they reach production.
-
Use docker compose pull ahead of time in CI to warm the image cache before the timed test run starts.
-
For genuine production use of Compose (single-VM deployments), pair restart: unless-stopped on every service with real healthchecks, and consider docker compose up -d --wait (waits for healthy status before returning) in deployment scripts to fail fast on broken rollouts.
-
For anything requiring multi-node high availability, auto-scaling, or rolling updates with zero downtime, migrate from Compose to Kubernetes (Chapter 11) — Compose intentionally does not solve multi-host orchestration.
-
Keep one compose.yaml as the source of truth per environment tier (dev/staging) via override files, rather than maintaining drifted, hand-edited copies.
-
Write a compose.yaml for a Spring Boot app + Postgres + Redis stack with healthchecks and condition: service_healthy gating.
-
Run docker compose up --scale app=3 -d after removing the fixed port mapping and adding an nginx load balancer in front; verify requests get distributed.
-
Run docker compose down then docker compose down -v separately and observe volume survival in the first case vs deletion in the second.
-
Use docker compose config to print the fully resolved YAML after variable interpolation from a .env file.
-
Compose declares a full multi-container app in one YAML file: services, networks, volumes, env vars, healthchecks.
-
All services share an auto-created network with embedded DNS — service-name-based discovery works out of the box.
-
depends_on only orders container start; use condition: service_healthy with a healthcheck for real readiness gating.
-
Scaling requires removing fixed host port mappings and fronting replicas with a load balancer.
-
down preserves named volumes; down -v destroys them — know the difference before running either in any real environment.
Want a visual for this concept?
Generate a diagram tailored to “Docker Compose: Services, Networks, Volumes, Scaling & Health Checks” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.
Sign in to generate a visual →