LFCE 134 ๐ง Docker Compose โ Concepts
Docker Compose is a tool for defining and running multi-container applications from a single YAML file. Instead of remembering the order of docker run commands, the network flags, the volume mounts, and the environment variables for each container, you describe the entire stack in one file and start it with one command. Compose translates that file into standard Docker objects: services become containers, networks become Docker networks, and volumes become Docker volumes .
This chapter covers the conceptual model behind Compose: the three top-level elements that define an application (services, networks, volumes), the service configuration that maps to docker run flags, the default network behavior that makes service names resolvable, and the depends_on distinction between “container started” and “service ready.” By the end, you will understand what Compose is actually doing when it reads the YAML, and why the difference between start order and readiness is the source of most Compose startup bugs.
Key point: depends_on without a condition only waits for the container to start, not for the application inside it to be ready. A web service that depends on a database will start immediately, connect before Postgres has initialized, and crash. The fix is a healthcheck on the dependency plus condition: service_healthy.
Why Compose exists
The command-remembering problem. A three-tier application has a web service, a database, and a cache. Starting it manually means three docker run commands, each with the right image, ports, environment variables, volume mounts, and network flags. The order matters: the database must be running before the web service tries to connect. Remembering the order and the flags every time is error-prone, and the commands are not recorded anywhere except the shell history .
The reproducibility problem. A docker-compose.yml is a file that can be committed to the repository, reviewed in a pull request, and checked out on any machine. A new developer clones the repository and runs docker compose up. The environment that runs is the environment the file describes, not the environment that happens to exist on the developer’s laptop .
The lifecycle problem. Compose manages the entire lifecycle of the application, not just the startup. docker compose down stops and removes the containers, networks, and (optionally) volumes that the file created. The objects are labeled with the project name, so Compose can find exactly what it created and leave everything else alone .
a. The three top-level elements
A Compose file has three elements that matter for a single-host application: services, networks, and volumes. The services element is required. The other two are optional but become important as the application grows .
# compose.yaml
services:
web:
image: nginx:1.27
ports:
- "8080:80"
networks:
- frontend
db:
image: postgres:16
volumes:
- db-data:/var/lib/postgresql/data
networks:
- backend
networks:
frontend:
backend:
internal: true
volumes:
db-data:
The services block defines the containers. Each service name becomes a DNS name on the network, so web can reach db at http://db:5432 without knowing the container’s IP address . The networks block defines named networks. The frontend network is a normal bridge; the backend network has internal: true, which means containers on it cannot reach the outside world. The volumes block defines named volumes. The db-data volume is managed by Docker and persists across container recreation .
The legacy version field is no longer needed with Compose v2. The specification is unified, and the version key is ignored or deprecated depending on the Compose version .
b. Service configuration and the docker run mapping
Each service is the Compose equivalent of a docker run command. The fields map directly to the flags.
services:
web:
image: myapp:1.0 # docker run myapp:1.0
build: # or build from a Dockerfile
context: .
dockerfile: Dockerfile
ports:
- "8080:80" # docker run -p 8080:80
environment:
NODE_ENV: production # docker run -e NODE_ENV=production
env_file:
- .env # load environment variables from a file
volumes:
- ./config:/app/config:ro # bind mount, read-only
- app-data:/app/data # named volume
restart: unless-stopped # docker run --restart unless-stopped
depends_on:
db:
condition: service_healthy
The image field specifies the image to pull. The build field specifies a Dockerfile to build instead. The ports field publishes ports in host:container format. The environment and env_file fields inject environment variables. The volumes field mounts storage. The restart field sets the restart policy. The depends_on field controls startup order .
The command and entrypoint fields override the image’s defaults. The user field sets the user the container runs as. The healthcheck field defines the command that determines whether the service is ready, not just running .
c. Networks and service discovery
Compose creates a default network for the project and attaches every service to it. All services on the default network can reach each other by service name. This is the same DNS resolution that a user-defined bridge network provides: Docker runs an embedded DNS server, and the service name becomes a network alias .
services:
web:
networks:
- frontend
db:
networks:
- backend
networks:
frontend:
backend:
internal: true
When services are attached to different networks, they cannot reach each other unless a service is attached to both. The web service is on frontend only. The db service is on backend only. They cannot communicate. To allow web to reach db, both must be attached to a shared network, or web must be attached to both .
The internal: true option on a network prevents containers on that network from accessing the internet. A database network does not need outbound access, and isolating it limits the blast radius if the database container is compromised .
d. Volumes and persistence
A container’s filesystem is ephemeral. When the container is removed, the data inside it is gone. A named volume is stored outside the container’s filesystem and persists across container recreation. A bind mount maps a host directory directly into the container.
services:
db:
volumes:
- db-data:/var/lib/postgresql/data # named volume
- ./init:/docker-entrypoint-initdb.d:ro # bind mount
volumes:
db-data:
The named volume db-data is declared at the top level. Docker manages its location on the host. The bind mount ./init maps a host directory into the container. The :ro suffix makes the mount read-only. Bind mounts are useful for configuration files and development code; named volumes are better for database data because Docker manages the permissions and location .
Complete Example Session
# ============================================
# PART 1: THE MINIMAL COMPOSE FILE
# ============================================
services:
web:
image: nginx:1.27
ports:
- "8080:80"
# ============================================
# PART 2: ADD A DATABASE
# ============================================
services:
web:
image: nginx:1.27
ports:
- "8080:80"
depends_on:
- db
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
# ============================================
# PART 3: THE DEPENDS_ON PROBLEM
# ============================================
# The web container starts immediately after the db container starts.
# But Postgres may take several seconds to initialize.
# The web container may crash with "connection refused".
# ============================================
# PART 4: THE HEALTHCHECK FIX
# ============================================
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
start_period: 10s
web:
image: nginx:1.27
depends_on:
db:
condition: service_healthy
# ============================================
# PART 5: NAMED VOLUMES
# ============================================
services:
db:
image: postgres:16
volumes:
- db-data:/var/lib/postgresql/data
volumes:
db-data:
# ============================================
# PART 6: NETWORK ISOLATION
# ============================================
services:
web:
networks:
- frontend
db:
networks:
- backend
networks:
frontend:
backend:
internal: true
# ============================================
# PART 7: ATTACHING A SERVICE TO MULTIPLE NETWORKS
# ============================================
services:
web:
networks:
- frontend
- backend
db:
networks:
- backend
networks:
frontend:
backend:
internal: true
# ============================================
# PART 8: BUILDING FROM A DOCKERFILE
# ============================================
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
# ============================================
# PART 9: ENVIRONMENT VARIABLES FROM .env
# ============================================
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
# ============================================
# PART 10: THE COMPLETE STACK
# ============================================
services:
app:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://app:${DB_PASSWORD}@db:5432/appdb
depends_on:
db:
condition: service_healthy
networks:
- frontend
- backend
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
networks:
- backend
networks:
frontend:
backend:
internal: true
volumes:
db-data:
The ten parts covered the minimal file, adding a database, the depends_on problem, the healthcheck fix, named volumes, network isolation, multi-network attachment, building from a Dockerfile, environment variables from .env, and the complete stack.
Quick Reference
Top-level elements
| Element | Required | Purpose |
|---|---|---|
services | Yes | Defines the containers |
networks | No | Defines named networks |
volumes | No | Defines named volumes |
version | No (legacy) | Ignored in Compose v2 |
Service fields
| Field | Equivalent | Purpose |
|---|---|---|
image | docker run <image> | Image to pull |
build | docker build | Build from Dockerfile |
ports | -p | Publish ports |
environment | -e | Set environment variables |
env_file | --env-file | Load variables from file |
volumes | -v | Mount storage |
restart | --restart | Restart policy |
depends_on | โ | Startup order |
healthcheck | โ | Readiness check |
networks | --network | Attach to networks |
depends_on conditions
| Condition | Meaning |
|---|---|
service_started | Container process exists (default) |
service_healthy | Healthcheck passes |
service_completed_successfully | Container exits 0 |
Volume types
| Type | Syntax | Managed by |
|---|---|---|
| Named volume | name:/path | Docker |
| Bind mount | ./host:/path | Host |
| Read-only bind | ./host:/path:ro | Host (read-only) |
Best Practices
โ Do This:
# Use healthcheck + condition for dependencies
depends_on:
db:
condition: service_healthy # โ
# Use named volumes for database data
volumes:
- db-data:/var/lib/postgresql/data # โ
# Use internal networks for backend services
networks:
backend:
internal: true # โ
# Use .env for secrets
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD} # โ
โ Don’t Do This:
# Rely on depends_on alone for readiness
depends_on:
- db # โ (only waits for start)
# Use bind mounts for database data
volumes:
- ./pgdata:/var/lib/postgresql/data # โ (permission issues)
# Expose database ports to the host
ports:
- "5432:5432" # โ (unless needed)
# Hardcode secrets in the compose file
environment:
POSTGRES_PASSWORD: secret # โ
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Web crashes on startup | depends_on without healthcheck | Add healthcheck and condition: service_healthy |
| Database data lost | No named volume | Add a top-level volumes entry and mount it |
| Services cannot resolve each other | On different networks | Attach both to a shared network |
| Container cannot reach internet | internal: true on its network | Attach to a non-internal network for outbound access |
| Permission denied on bind mount | Host directory permissions | Use a named volume or fix host permissions |
| Secrets in version control | Hardcoded in YAML | Use .env and .gitignore |
Real-World Examples
1. Minimal web service
services:
web:
image: nginx:1.27
ports:
- "8080:80"
2. Web with database dependency
services:
web:
depends_on:
db:
condition: service_healthy
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
3. Named volume
volumes:
db-data:
4. Internal network
networks:
backend:
internal: true
5. Multi-network service
services:
web:
networks:
- frontend
- backend
6. Build from Dockerfile
services:
app:
build:
context: .
7. Environment from .env
environment:
DB_PASSWORD: ${DB_PASSWORD}
8. Read-only bind mount
volumes:
- ./config:/app/config:ro
9. Restart policy
restart: unless-stopped
10. Healthcheck
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 10s
retries: 3
Visual
The Compose object model
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ COMPOSE FILE โ
โ โ
โ services: โ
โ web โโโบ container (nginx) โ
โ db โโโบ container (postgres) โ
โ โ
โ networks: โ
โ frontend โโโบ docker network create frontend โ
โ backend โโโบ docker network create backend โ
โ โ
โ volumes: โ
โ db-data โโโบ docker volume create db-data โ
โ โ
โ Each object is labeled with the project name. โ
โ docker compose down removes exactly these objects. โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
depends_on: start vs ready
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ depends_on (NO CONDITION) โ
โ โ
โ db container starts โ
โ โ โ
โ โผ โ
โ web container starts โ
โ โ โ
โ โผ โ
โ db is still initializing โ
โ โ โ
โ โผ โ
โ web crashes: connection refused โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
โ depends_on (condition: service_healthy) โ
โ โ
โ db container starts โ
โ โ โ
โ โผ โ
โ healthcheck runs: pg_isready โ
โ โ โ
โ โผ โ
โ healthcheck passes โ
โ โ โ
โ โผ โ
โ web container starts โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Network isolation
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ NETWORK ISOLATION โ
โ โ
โ frontend network โ
โ โโโ web (nginx) โ
โ โ โ
โ โ can reach db because web is also on backend โ
โ โผ โ
โ backend network (internal) โ
โ โโโ db (postgres) โ
โ โ
โ External traffic โ web โ db โ
โ db cannot reach the internet โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Volume persistence
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ VOLUME PERSISTENCE โ
โ โ
โ Container (db) โ
โ โโโ /var/lib/postgresql/data โ
โ โ โ
โ โผ โ
โ Named volume (db-data) โ
โ โโโ Managed by Docker โ
โ โ
โ docker compose down โ
โ โ container removed โ
โ โ volume remains (unless -v) โ
โ โ
โ docker compose up โ
โ โ new container mounts the same volume โ
โ โ data is still there โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
| File names | compose.yaml, compose.yml, docker-compose.yml |
| Required element | services |
| Optional elements | networks, volumes |
| Service DNS | Service name resolves on the network |
| Default network | Created automatically, all services attached |
depends_on default | Waits for container start only |
depends_on readiness | condition: service_healthy |
| Named volume declaration | Top-level volumes block |
| Network isolation | internal: true |
| Secrets | .env file, not hardcoded |
Key takeaways:
- Compose turns
docker runcommands into a declarative file. Theservicesblock defines the containers, thenetworksblock defines the networks, and thevolumesblock defines the persistent storage. The file is the single source of truth for the application’s environment . - Service names are DNS names on the Compose network. A service named
dbis reachable atdbfrom any other service on the same network. This is why the application configuration usespostgres://db:5432instead of an IP address that changes on restart . depends_onwithoutconditionis not a readiness check. It waits for the container process to exist, not for the application inside it to start accepting connections. Thecondition: service_healthywith ahealthcheckon the dependency is the correct pattern .- Named volumes persist; bind mounts are host paths. A named volume is managed by Docker and survives container recreation. A bind mount maps a host directory directly and is useful for configuration and development code, but it inherits the host’s permission model .
- Network isolation is a security boundary. A database on an
internal: truenetwork cannot reach the internet. Only the services that need external access should be on a non-internal network, and only those services should publish ports to the host . - The
versionfield is legacy. Compose v2 does not require it, and the unified specification is the current model. The file is a YAML document that describes the application stack, not a versioned configuration format .
Remember: Docker Compose is a declarative interface to the Docker API. The YAML file does not contain logic; it contains a description of the objects that should exist. The services block describes the containers, the networks block describes the networks, and the volumes block describes the storage. The service name is the DNS name, the depends_on condition is the readiness gate, and the named volume is the persistence mechanism. Get the healthcheck right first: a depends_on without a healthcheck is a race condition waiting to crash on startup.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!