The docker-compose.yml file is a YAML configuration file that defines how your multi-container applications should be structured and run. This file serves as the blueprint for your application's architecture, specifying everything from container configurations to resource constraints. In this article, we'll explore the anatomy of this file and understand how to leverage its capabilities effectively.
The Anatomy of docker-compose.yml
At its core, a docker-compose.yml file is organized into several top-level keys that define different aspects of your application. The file follows YAML syntax rules, where indentation matters and defines the hierarchy of configuration options.
Version Specification
While the version key was commonly used in older Compose files, modern Docker Compose (v2 and above) has made this optional. The version determined which features were available, but current implementations automatically detect and support the latest specification format.
version: '3.8' # Optional in modern Compose
Top-Level Keys
The main structure of a docker-compose.yml file includes several top-level configuration sections:
- services: Defines the containers that make up your application
- volumes: Declares named volumes for persistent data storage
- networks: Specifies custom network configurations
- configs: Manages configuration files (primarily for Swarm mode)
- secrets: Handles sensitive data securely
Service Definitions
The services section is where you define each container in your application. Each service represents a containerized component of your system.
Basic Service Structure
services:
web:
image: nginx:latest
ports:
- "8080:80"
This minimal configuration defines a service named "web" that uses the nginx image and maps port 8080 on your host to port 80 in the container.
Build Context
Instead of using a pre-built image, you can specify a build context to create an image from source:
services:
app:
build:
context: ./app
dockerfile: Dockerfile.prod
args:
BUILD_VERSION: "1.0"
The context specifies the directory containing your build files, dockerfile points to a specific Dockerfile if it's not named "Dockerfile", and args passes build-time variables.
Container Naming
You can explicitly name your containers for easier identification:
services:
database:
image: postgres:14
container_name: my_postgres_db
Without container_name, Compose generates names using the pattern: projectname_servicename_number.
Port Mapping and Exposure
Port configuration controls how your containers communicate with the host system and each other.
Publishing Ports
The ports key publishes container ports to the host:
services:
api:
image: myapi:latest
ports:
- "3000:3000" # host:container
- "127.0.0.1:3001:3001" # bind to localhost only
- "4000-4005:4000-4005" # port range
Exposing Ports
The expose key makes ports accessible to linked services without publishing them to the host:
services:
backend:
image: mybackend:latest
expose:
- "8000"
Dependency Management
The depends_on key controls the startup order of services:
services:
web:
image: webapp:latest
depends_on:
- database
- cache
database:
image: postgres:14
cache:
image: redis:alpine
This ensures that database and cache start before web. However, depends_on only waits for containers to start, not for them to be ready to accept connections.
Conditional Dependencies
For more control over startup conditions:
services:
app:
image: myapp:latest
depends_on:
db:
condition: service_healthy
db:
image: postgres:14
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
Resource Constraints
You can limit the resources available to each container to prevent any single service from consuming excessive system resources.
Memory Limits
services:
processor:
image: dataprocessor:latest
mem_limit: 512m
mem_reservation: 256m
The mem_limit sets a hard limit, while mem_reservation sets a soft limit that the container should stay within under normal conditions.
CPU Constraints
services:
compute:
image: compute-intensive:latest
cpus: '0.5'
cpu_shares: 512
The cpus value limits the container to 50% of one CPU core, while cpu_shares sets the relative CPU priority compared to other containers.
Restart Policies
Restart policies determine how containers behave when they exit:
services:
api:
image: api:latest
restart: unless-stopped
Available restart policies:
- no: Never restart (default)
- always: Always restart regardless of exit status
- on-failure: Restart only on failure (non-zero exit code)
- unless-stopped: Always restart unless explicitly stopped
You can also specify a maximum retry count:
services:
worker:
image: worker:latest
restart: on-failure:3
Command and Entrypoint Overrides
You can override the default command or entrypoint defined in the image:
services:
app:
image: myapp:latest
command: python manage.py runserver 0.0.0.0:8000
entrypoint: /custom-entrypoint.sh
Commands can also be specified as arrays:
services:
processor:
image: processor:latest
command: ["python", "processor.py", "--mode", "production"]
Working Directory and User
Specify the working directory and user context for the container:
services:
app:
image: nodeapp:latest
working_dir: /usr/src/app
user: "1000:1000"
This runs the container's process from the specified directory and as a specific user (by UID:GID).
Labels and Metadata
Labels add metadata to containers for organization and automation:
services:
web:
image: nginx:latest
labels:
com.example.description: "Frontend web server"
com.example.department: "engineering"
com.example.version: "1.0"
Labels are key-value pairs that can be queried and filtered by various tools and scripts.
Advanced Service Configurations
Privileged Mode
Some containers require elevated privileges:
services:
system_monitor:
image: monitor:latest
privileged: true
Use this cautiously as it grants the container nearly all capabilities of the host machine.
Device Mapping
Map host devices into containers:
services:
hardware_access:
image: hwcontrol:latest
devices:
- "/dev/video0:/dev/video0"
- "/dev/sda:/dev/xvda:rwm"
PID and Network Modes
Share process or network namespaces:
services:
debugger:
image: debug:latest
pid: "host" # Share host's PID namespace
nettools:
image: tools:latest
network_mode: "host" # Use host networking
Extension Fields and YAML Anchors
Reduce repetition using YAML anchors and extension fields:
x-common-variables: &common-env
LOG_LEVEL: info
APP_ENV: production
x-common-config: &common-config
restart: unless-stopped
mem_limit: 512m
services:
api:
<<: *common-config
image: api:latest
environment:
<<: *common-env
SERVICE_NAME: api
worker:
<<: *common-config
image: worker:latest
environment:
<<: *common-env
SERVICE_NAME: worker
Extension fields (starting with x-) are ignored by Compose but can be referenced using anchors (&) and merged using aliases (* and <<).
Profiles for Conditional Service Activation
Profiles allow you to selectively start services:
services:
web:
image: webapp:latest
debug_tools:
image: debug:latest
profiles:
- debug
testing_db:
image: postgres:latest
profiles:
- testing
Services with profiles only start when the profile is explicitly activated through the command line or environment variable.
Multiple Compose Files
You can split your configuration across multiple files for better organization:
# docker-compose.yml (base)
services:
app:
image: myapp:latest
ports:
- "8000:8000"
# docker-compose.override.yml (automatically merged)
services:
app:
environment:
DEBUG: "true"
Compose automatically merges docker-compose.override.yml with docker-compose.yml. You can also specify additional files explicitly.
Configuration Validation
The docker-compose.yml file structure must follow specific rules. Indentation must be consistent (typically 2 spaces), and keys must be properly nested. Invalid YAML syntax or incorrect key names will cause Compose to reject the file with an error message indicating the problem location.
Best Practices for Organizing Your Compose File
Structure your docker-compose.yml file logically by grouping related services together. Place frequently modified configurations like port mappings and volume definitions near the top for easy access. Use comments to document non-obvious configurations:
services:
app:
image: myapp:latest
# Binding to localhost only for security
ports:
- "127.0.0.1:8000:8000"
# Increased memory for data processing tasks
mem_limit: 1g
Variable Interpolation
Reference shell environment variables or variables from external files:
services:
web:
image: nginx:${NGINX_VERSION:-latest}
ports:
- "${EXTERNAL_PORT:-8080}:80"
The syntax ${VARIABLE:-default} provides a fallback value if the variable isn't set.
Understanding the File's Role in Your Workflow
The docker-compose.yml file acts as infrastructure-as-code for your application. It documents your application's architecture, makes your setup reproducible across different environments, and provides a single source of truth for your application's configuration. By mastering this file's structure and capabilities, you gain precise control over how your containerized applications are configured and deployed.