Docker Compose
Orchestrate multi-container applications with Docker Compose
Managing multiple containers manually can quickly become complex. Docker Compose solves this problem by allowing you to define and run multi-container applications using a single YAML file. This simplifies the process of building, running, and connecting services while maintaining their configurations in version control.
Understanding Docker Compose
Docker Compose is a tool for defining and running multi-container Docker applications. With Compose, you use a YAML file to configure your application's services, networks, and volumes. Then, with a single command, you create and start all the services from your configuration.
Installing Docker Compose
Docker Desktop for Mac and Windows includes Docker Compose by default. For Linux, you may need to install it separately:
# For the latest standalone Compose binary
sudo curl -L "https://github.com/docker/compose/releases/download/v2.18.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
Docker now also offers Compose as a plugin for the Docker CLI (newer versions):
docker compose version
vs.
docker-compose version
Both work, but we'll use docker-compose
in this guide for compatibility.
Creating a Compose File
Let's create a simple web application with a frontend, backend, and database using Docker Compose.
Create a file named docker-compose.yml
:
version: '3.8'
services:
# Frontend service
frontend:
image: nginx:alpine
ports:
- '8080:80'
volumes:
- ./frontend:/usr/share/nginx/html
depends_on:
- backend
# Backend API service
backend:
build: ./backend
ports:
- '5000:5000'
environment:
- DATABASE_URL=postgres://postgres:password@db:5432/app
depends_on:
- db
# Database service
db:
image: postgres:13
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_PASSWORD=password
- POSTGRES_DB=app
volumes:
postgres_data:
This configuration:
- Creates three services:
frontend
,backend
, anddb
- Specifies container images, port mappings, volumes, and environment variables
- Defines dependencies between services
- Creates a persistent volume for the database
Core Compose Concepts
Services
Each service in a Compose file represents a container. Services are defined with configurations similar to docker run
command parameters.
Key service options include:
image
: Specifies the image to usebuild
: Builds a custom image from a Dockerfileports
: Maps container ports to host portsvolumes
: Mounts host directories or named volumesenvironment
: Sets environment variablesdepends_on
: Establishes service dependenciesrestart
: Sets the restart policy
Volumes
Volumes in Compose work the same as Docker volumes, allowing data to persist beyond the life of a container:
volumes:
postgres_data: # Named volume definition
Services can then use these volumes:
services:
db:
volumes:
- postgres_data:/var/lib/postgresql/data # Mount the named volume
- ./init-scripts:/docker-entrypoint-initdb.d # Bind mount from host
Networks
Compose automatically creates a default network for your application, but you can define custom networks:
services:
backend:
networks:
- backend-network
- db-network
db:
networks:
- db-network
networks:
backend-network:
db-network:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/24
Basic Docker Compose Commands
Starting Your Application
To start all services defined in your Compose file:
docker-compose up
To run in detached mode (background):
docker-compose up -d
Stopping Services
To stop your running services:
docker-compose down
To stop services and remove volumes:
docker-compose down -v
Viewing Service Logs
To see logs from all services:
docker-compose logs
To follow logs from specific services:
docker-compose logs -f backend db
Listing Services
To see the status of your services:
docker-compose ps
Executing Commands in Services
To run a one-off command in a service container:
docker-compose exec backend python manage.py migrate
Or for a new container instance:
docker-compose run --rm backend python manage.py createsuperuser
Real-World Compose Example
Let's look at a more detailed example for a full-stack web application:
version: '3.8'
services:
# Reverse proxy
nginx:
image: nginx:alpine
ports:
- '80:80'
- '443:443'
volumes:
- ./nginx/conf:/etc/nginx/conf.d
- ./nginx/certs:/etc/nginx/certs
- ./frontend/build:/usr/share/nginx/html
depends_on:
- api
restart: always
# React frontend build process
frontend:
build:
context: ./frontend
dockerfile: Dockerfile.dev
volumes:
- ./frontend:/app
- /app/node_modules
environment:
- REACT_APP_API_URL=http://localhost/api
ports:
- '3000:3000'
command: npm start
# Node.js API server
api:
build: ./backend
volumes:
- ./backend:/app
- /app/node_modules
environment:
- NODE_ENV=development
- DATABASE_URL=postgres://postgres:password@db:5432/app
- REDIS_URL=redis://redis:6379
- JWT_SECRET=your_jwt_secret
depends_on:
- db
- redis
restart: always
# PostgreSQL database
db:
image: postgres:13
volumes:
- postgres_data:/var/lib/postgresql/data
- ./db/init:/docker-entrypoint-initdb.d
environment:
- POSTGRES_PASSWORD=password
- POSTGRES_DB=app
ports:
- '5432:5432'
restart: always
# Redis for caching
redis:
image: redis:6-alpine
volumes:
- redis_data:/data
ports:
- '6379:6379'
restart: always
volumes:
postgres_data:
redis_data:
This setup includes:
- Nginx as a reverse proxy serving a frontend build
- A development frontend container with hot reloading
- An API container connected to Postgres and Redis
- Persistent volumes for database and cache data
- Appropriate restart policies for services
Environment Variables in Compose
You can pass environment variables to containers in several ways:
Inline Environment Variables
services:
web:
environment:
- DEBUG=true
- API_KEY=myapikey
Using an Environment File
services:
web:
env_file:
- .env.web
Where .env.web
contains:
DEBUG=true
API_KEY=myapikey
Using Host Environment Variables
You can also use variables from your host:
services:
web:
environment:
- API_KEY=${HOST_API_KEY}
Then run with:
HOST_API_KEY=myapikey docker-compose up
Scaling Services
Docker Compose allows you to run multiple instances of a service:
docker-compose up -d --scale worker=3
This would start three instances of a service named worker
.
In your Compose file, make sure to use non-conflicting port bindings for scalable services:
services:
worker:
image: my-worker-image
# Don't bind to a fixed host port if you want to scale
# ports:
# - "8080" # Only exposes the container port without binding to host
Extending Compose Files
For different environments, you can extend your main Compose file with overrides:
docker-compose.yml (Base configuration):
version: '3.8'
services:
web:
image: myapp:latest
build: .
ports:
- '8000:8000'
docker-compose.prod.yml (Production overrides):
version: '3.8'
services:
web:
restart: always
environment:
- NODE_ENV=production
ports:
- '80:8000'
Run with:
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
Compose for Development Workflows
Docker Compose is excellent for development environments:
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile.dev
volumes:
- .:/app # Mount current directory to /app in the container
- /app/node_modules # Volume for node_modules to prevent overwriting
ports:
- '3000:3000'
environment:
- NODE_ENV=development
command: npm run dev # Use development command
Key development features:
- Mount code directories for live updates
- Override production commands with development alternatives
- Create development-specific environment variables
Compose for Production
While Docker Compose is primarily a development tool, it can be used in production for simple deployments:
version: '3.8'
services:
app:
image: mycompany/myapp:${TAG:-latest}
restart: always
ports:
- '80:8000'
environment:
- NODE_ENV=production
deploy:
replicas: 2
update_config:
parallelism: 1
delay: 10s
restart_policy:
condition: on-failure
Note: For more complex production deployments, consider Docker Swarm or Kubernetes.
Health Checks in Compose
Health checks ensure your services are running correctly:
services:
web:
image: nginx
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost']
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
Other services can depend on this health check:
services:
backend:
depends_on:
web:
condition: service_healthy
Secrets Management in Compose
For non-sensitive configurations, environment variables work well. For secrets, consider these approaches:
Local development: Use
.env
files (but don't commit them to version control)Production: Use external secret management tools and inject values at runtime
For Docker Swarm, you can use Docker secrets:
version: '3.8'
services:
web:
image: myapp
secrets:
- db_password
secrets:
db_password:
file: ./db_password.txt
Deploying with Compose to DigitalOcean
Docker Compose files work excellently when deploying to cloud providers like DigitalOcean. You can set up a Droplet with Docker installed and use your Compose file to deploy your application.
Sign up with DigitalOcean and get $200 in free credits to deploy your Compose-based applications.
Steps to deploy:
- Create a Droplet with Docker pre-installed (One-click Docker application)
- Copy your project files and Compose file to the Droplet
- Run
docker-compose up -d
on the Droplet
Compose Specification and Versions
Docker Compose files have evolved through several versions. As of 2023, version 3.x is the most common, with the latest being the Compose Specification:
# Modern Compose Specification format
name: myapp
services:
web:
image: nginx
The main versions:
- Version 1: Legacy format, no longer recommended
- Version 2: Added named networks and depends_on
- Version 3: Designed for both Compose and Swarm, with some limitations
- Compose Spec: The newest format that unifies syntax across platforms
Best Practices for Docker Compose
Use version control for your Compose files
Follow a consistent project structure:
myapp/ ├── docker-compose.yml ├── docker-compose.override.yml ├── .env ├── service1/ │ ├── Dockerfile │ └── ... └── service2/ ├── Dockerfile └── ...
Create separate Compose files for different environments
Use the
.env
file for environment-specific variablesSet restart policies for services that should be resilient
Leverage healthchecks for critical services
Always specify versions for images (never use
latest
in production)Document service dependencies using
depends_on
Limit container privileges following the principle of least privilege
Keep your Compose files DRY using YAML anchors and extensions (for complex setups)
In the next section, we'll cover Docker best practices for security, performance, and resource optimization.
Found an issue?