Docker Compose File Structure Services Volumes and Networks
The compose.yaml file is the heart of Docker Compose. It describes every container your application needs, how they connect, and where they store data. This topic walks through every important section with clear diagrams so you can write confident Compose files from scratch.
The Top-Level Structure
compose.yaml skeleton:
services: ← containers (required)
service-name-1:
...
service-name-2:
...
volumes: ← named volumes (optional)
volume-name-1:
volume-name-2:
networks: ← custom networks (optional)
network-name-1:
Every Compose file has at least a services section. Volumes and networks are only needed when you want explicit control over them — otherwise Docker Compose creates sensible defaults automatically.
The services Section — Defining Each Container
Each entry under services becomes one container. The key is the service name, which also becomes the DNS hostname other containers use to reach it.
services:
webapp: ← service name (also the hostname)
image: my-flask-app:1.0 ← image to use (pull or local)
build: ./app ← OR build from a Dockerfile here
container_name: flask-web ← custom container name (optional)
restart: unless-stopped ← restart policy
ports:
- "5000:5000" ← HOST:CONTAINER port mapping
environment:
DATABASE_URL: postgres://...
DEBUG: "false"
env_file:
- .env ← load vars from a file
volumes:
- ./logs:/app/logs ← bind mount
- appdata:/app/data ← named volume
networks:
- app-net ← attach to custom network
depends_on:
db:
condition: service_healthy ← wait for db to be ready
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
interval: 30s
timeout: 10s
retries: 3
image vs build — Two Ways to Get a Container Image
Option 1: Use an existing image
services:
db:
image: postgres:15 ← pull from Docker Hub
Option 2: Build from a Dockerfile
services:
webapp:
build: ./webapp ← Dockerfile is in ./webapp folder
Option 3: Build with more control
services:
webapp:
build:
context: ./webapp ← folder with files to send
dockerfile: Dockerfile.prod ← specific Dockerfile name
args:
APP_VERSION: "2.0" ← build arguments
When you run docker compose up --build, it rebuilds images from Dockerfiles.
When you run docker compose up, it uses cached images.
restart Policies
restart: "no" ← Never restart (default) restart: always ← Always restart, even on host reboot restart: on-failure ← Restart only if container exits with error restart: unless-stopped ← Restart always, except when manually stopped
Use unless-stopped for production services — they come back up after a server reboot but stay stopped if you intentionally stopped them with docker compose stop.
depends_on — Controlling Startup Order
services:
webapp:
image: my-app
depends_on:
db:
condition: service_healthy ← wait until db passes healthcheck
redis:
condition: service_started ← wait until redis container starts (not ready)
db:
image: postgres:15
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
Startup order with depends_on: db starts → healthcheck passes → webapp starts redis starts → webapp starts Without depends_on: All containers start at the same time → webapp crashes because db isn't ready
The volumes Section
services:
db:
image: postgres:15
volumes:
- pgdata:/var/lib/postgresql/data ← reference named volume
webapp:
image: my-app
volumes:
- ./src:/app/src ← bind mount (no declaration needed)
- appdata:/app/uploads ← reference named volume
volumes:
pgdata: ← declares the named volume (Docker manages location)
appdata:
driver: local ← explicitly set driver (local is default)
Named volumes declared at the top level persist across docker compose down. They are only deleted when you run docker compose down -v.
The networks Section
services:
webapp:
networks:
- frontend
- backend
db:
networks:
- backend ← db is only on backend, not reachable from frontend
nginx:
networks:
- frontend ← nginx only talks to webapp, not db directly
networks:
frontend: ← Docker creates this network
backend: ← Docker creates this one too
Network isolation diagram: frontend network: backend network: nginx ──────────────── webapp ──────────── db (public facing) (app logic) (data) nginx cannot directly reach db → security by design
When you do not define networks, Compose creates one default network and all services join it. For production apps, explicit networks give you fine-grained security by isolating which services can talk to which.
A Complete Realistic Example
services:
nginx:
image: nginx:1.25
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- webapp
networks:
- frontend
webapp:
build: ./app
restart: unless-stopped
env_file: .env
depends_on:
db:
condition: service_healthy
networks:
- frontend
- backend
db:
image: postgres:15
restart: unless-stopped
environment:
POSTGRES_USER: appuser
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: appdb
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U appuser"]
interval: 10s
retries: 5
networks:
- backend
volumes:
pgdata:
networks:
frontend:
backend:
Notice ${DB_PASSWORD} — Compose reads this from a .env file in the same directory automatically, keeping secrets out of the Compose file.
