| |

LFCE 134 ๐Ÿง Docker Compose โ€” Concepts

โฑ๏ธ Estimated read: 12 min | |

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

ElementRequiredPurpose
servicesYesDefines the containers
networksNoDefines named networks
volumesNoDefines named volumes
versionNo (legacy)Ignored in Compose v2

Service fields

FieldEquivalentPurpose
imagedocker run <image>Image to pull
builddocker buildBuild from Dockerfile
ports-pPublish ports
environment-eSet environment variables
env_file--env-fileLoad variables from file
volumes-vMount storage
restart--restartRestart policy
depends_onโ€”Startup order
healthcheckโ€”Readiness check
networks--networkAttach to networks

depends_on conditions

ConditionMeaning
service_startedContainer process exists (default)
service_healthyHealthcheck passes
service_completed_successfullyContainer exits 0

Volume types

TypeSyntaxManaged by
Named volumename:/pathDocker
Bind mount./host:/pathHost
Read-only bind./host:/path:roHost (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

PitfallWhy It HappensFix
Web crashes on startupdepends_on without healthcheckAdd healthcheck and condition: service_healthy
Database data lostNo named volumeAdd a top-level volumes entry and mount it
Services cannot resolve each otherOn different networksAttach both to a shared network
Container cannot reach internetinternal: true on its networkAttach to a non-internal network for outbound access
Permission denied on bind mountHost directory permissionsUse a named volume or fix host permissions
Secrets in version controlHardcoded in YAMLUse .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

ItemValue
File namescompose.yaml, compose.yml, docker-compose.yml
Required elementservices
Optional elementsnetworks, volumes
Service DNSService name resolves on the network
Default networkCreated automatically, all services attached
depends_on defaultWaits for container start only
depends_on readinesscondition: service_healthy
Named volume declarationTop-level volumes block
Network isolationinternal: true
Secrets.env file, not hardcoded

Key takeaways:

  • Compose turns docker run commands into a declarative file. The services block defines the containers, the networks block defines the networks, and the volumes block 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 db is reachable at db from any other service on the same network. This is why the application configuration uses postgres://db:5432 instead of an IP address that changes on restart .
  • depends_on without condition is not a readiness check. It waits for the container process to exist, not for the application inside it to start accepting connections. The condition: service_healthy with a healthcheck on 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: true network 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 version field 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.

Was this helpful?


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!