Docker Compose: docker-compose.yml and Multi-Container Applications
📅 Published: Feb 2026
⏱️ Estimated Reading Time: 18 minutes
🏷️ Tags: Docker Compose, Multi-Container, docker-compose.yml, Container Orchestration, DevOps
Introduction: The Multi-Container Challenge
A modern application is rarely a single container. A typical web application might include:
A web server container (Nginx)
An application container (Node.js, Python, Ruby)
A database container (PostgreSQL, MySQL)
A cache container (Redis)
A background worker container
A message queue container
Running these containers individually with docker run commands becomes unmanageable quickly. You need to remember the correct order to start them. You need to configure networks so they can find each other. You need to mount volumes for persistent data. You need to document all these commands.
Docker Compose solves this problem. It lets you define your entire multi-container application in a single YAML file. With one command, you can start everything.
This guide covers the essential features of Docker Compose that you will use daily.
Part 1: What is Docker Compose?
The Simple Definition
Docker Compose is a tool for defining and running multi-container Docker applications. You define your application's services, networks, and volumes in a docker-compose.yml file. Then, with a single command, you create and start all the services.
Think of Docker Compose as the conductor of an orchestra. Each container is a musician. The compose file is the sheet music. The docker-compose up command is the conductor raising the baton.
What Compose Does
Defines services: What containers to run and how to configure them
Creates networks: Automatically sets up a network so services can communicate
Manages volumes: Creates and attaches volumes for persistent data
Handles dependencies: Starts services in the correct order
Orchestrates lifecycle: Start, stop, rebuild everything with one command
Part 2: docker-compose.yml Basics
The Structure
A docker-compose.yml file has three main sections:
version: '3.8' # Compose file format version services: # Define your containers web: # configuration for web service database: # configuration for database service networks: # Define custom networks (optional) app-network: volumes: # Define named volumes (optional) db-data:
Version
The version field specifies the Compose file format. Use version '3.8' for current projects. Version 3 files work with both Compose and Docker Swarm.
| Version | Features | When to Use |
|---|---|---|
| 3.8+ | Most features, Swarm compatible | New projects |
| 2.x | More features than v1 | Legacy projects |
| 1.x | Basic features | Avoid |
Part 3: Defining Services
Basic Service Definition
The simplest service definition needs only an image name:
version: '3.8' services: web: image: nginx:alpine database: image: postgres:15
Service Configuration Options
Image and Build
services: # Use an existing image web: image: nginx:alpine # Build from Dockerfile app: build: ./app # Path to Dockerfile build: context: ./app dockerfile: Dockerfile.prod args: NODE_ENV: production
Container Name
services: web: container_name: my-web-server image: nginx
Without container_name, Compose generates names like project_web_1.
Ports
services: web: ports: - "80:80" # host:container - "443:443" - "8080:80" # map host 8080 to container 80 - "127.0.0.1:3000:3000" # bind to specific host IP
Environment Variables
services: database: environment: - POSTGRES_PASSWORD=secret - POSTGRES_DB=myapp - POSTGRES_USER=myuser app: environment: NODE_ENV: production DATABASE_URL: postgresql://myuser:secret@database:5432/myapp
Environment File
services: app: env_file: - .env - .env.production
Volumes
services: database: volumes: - db-data:/var/lib/postgresql/data # named volume - ./backup:/backup # bind mount - ./config/postgres.conf:/etc/postgresql/postgresql.conf:ro # read-only volumes: db-data:
Networks
services: web: networks: - frontend - backend database: networks: - backend networks: frontend: backend:
Depends On
services: app: build: ./app depends_on: - database - redis # Waits for database and redis to start before starting app database: image: postgres redis: image: redis
Note: depends_on only waits for containers to start, not for services to be ready. For databases, you may need additional health checks.
Restart Policy
services: web: restart: always # always restart app: restart: on-failure # restart on error worker: restart: unless-stopped # restart unless manually stopped
Command Override
services: app: image: node:18 command: npm run start # Overrides the default CMD from Dockerfile database: image: postgres command: postgres -c shared_buffers=256MB
Health Check
services: web: image: nginx healthcheck: test: ["CMD", "curl", "-f", "http://localhost"] interval: 30s timeout: 10s retries: 3 start_period: 40s
Part 4: Complete Example
WordPress with MySQL
Here's a complete docker-compose.yml for WordPress:
version: '3.8' services: database: image: mysql:8.0 container_name: wordpress-db restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: rootpassword MYSQL_DATABASE: wordpress MYSQL_USER: wordpress MYSQL_PASSWORD: wordpress volumes: - db-data:/var/lib/mysql networks: - wordpress-network wordpress: image: wordpress:latest container_name: wordpress-app restart: unless-stopped depends_on: - database ports: - "8080:80" environment: WORDPRESS_DB_HOST: database:3306 WORDPRESS_DB_USER: wordpress WORDPRESS_DB_PASSWORD: wordpress WORDPRESS_DB_NAME: wordpress volumes: - wordpress-data:/var/www/html networks: - wordpress-network volumes: db-data: wordpress-data: networks: wordpress-network: driver: bridge
Run it:
docker-compose up -d
Access WordPress at http://localhost:8080
Node.js Application with PostgreSQL and Redis
version: '3.8' services: postgres: image: postgres:15 container_name: myapp-postgres environment: POSTGRES_USER: myuser POSTGRES_PASSWORD: secret POSTGRES_DB: myapp volumes: - postgres-data:/var/lib/postgresql/data ports: - "5432:5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U myuser"] interval: 10s timeout: 5s retries: 5 redis: image: redis:alpine container_name: myapp-redis volumes: - redis-data:/data ports: - "6379:6379" healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 5s retries: 5 app: build: ./app container_name: myapp-app restart: always depends_on: postgres: condition: service_healthy redis: condition: service_healthy environment: DATABASE_URL: postgresql://myuser:secret@postgres:5432/myapp REDIS_URL: redis://redis:6379 NODE_ENV: production ports: - "3000:3000" volumes: - ./app:/app - /app/node_modules command: npm start volumes: postgres-data: redis-data:
Part 5: Docker Compose Commands
Basic Commands
# Start all services (detached mode) docker-compose up -d # Start and see logs docker-compose up # Stop all services docker-compose down # Stop and remove volumes (destroys data!) docker-compose down -v # List running services docker-compose ps # View logs docker-compose logs docker-compose logs -f # Follow logs docker-compose logs web # Logs for specific service
Building and Rebuilding
# Build images before starting docker-compose up --build # Rebuild specific service docker-compose build web # Force rebuild without cache docker-compose build --no-cache
Running Commands in Containers
# Run one-off command docker-compose run app npm run migrate # Open shell in service docker-compose exec app /bin/bash # Run command with environment docker-compose run -e NODE_ENV=test app npm test
Managing Services
# Start specific service docker-compose start web # Stop specific service docker-compose stop web # Restart specific service docker-compose restart web # Pause all services docker-compose pause # Unpause docker-compose unpause
Scaling Services
# Run 3 instances of web service docker-compose up -d --scale web=3 # Requires web service to have no port conflicts # Usually used with load balancer
Part 6: Advanced Features
Profiles
Profiles allow you to selectively start services:
version: '3.8' services: web: image: nginx profiles: ["production", "staging"] app: image: myapp profiles: ["production", "staging", "development"] admin: image: admin-tool profiles: ["admin"] debug: image: debug-tool profiles: ["development"]
# Start production services docker-compose --profile production up -d # Start development services docker-compose --profile development up -d # Start multiple profiles docker-compose --profile production --profile admin up -d
Variable Substitution
Compose files support environment variables:
version: '3.8' services: database: image: postgres:${POSTGRES_VERSION:-15} environment: POSTGRES_PASSWORD: ${DB_PASSWORD}
# Set variables export DB_PASSWORD=secret export POSTGRES_VERSION=16 docker-compose up # Or use .env file echo "DB_PASSWORD=secret" > .env echo "POSTGRES_VERSION=16" >> .env docker-compose up
Extending Services
You can extend service definitions:
# docker-compose.yml version: '3.8' services: base-app: image: node:18 environment: NODE_ENV: production restart: always web: extends: service: base-app command: npm run start:web ports: - "3000:3000" worker: extends: service: base-app command: npm run start:worker
Dependency Conditions
Advanced dependency conditions:
services: app: depends_on: database: condition: service_healthy redis: condition: service_started database: image: postgres healthcheck: test: ["CMD", "pg_isready"] interval: 10s redis: image: redis
Part 7: Real-World Examples
Example 1: LAMP Stack (Linux, Apache, MySQL, PHP)
version: '3.8' services: mysql: image: mysql:8.0 container_name: lamp-mysql environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: mydb MYSQL_USER: user MYSQL_PASSWORD: password volumes: - mysql-data:/var/lib/mysql networks: - lamp-network php: build: ./php container_name: lamp-php volumes: - ./www:/var/www/html networks: - lamp-network depends_on: - mysql apache: image: httpd:latest container_name: lamp-apache ports: - "80:80" volumes: - ./www:/usr/local/apache2/htdocs - ./apache-config:/usr/local/apache2/conf networks: - lamp-network depends_on: - php volumes: mysql-data: networks: lamp-network:
Example 2: MEVN Stack (MongoDB, Express, Vue, Node.js)
version: '3.8' services: mongodb: image: mongo:6 container_name: mevn-mongo volumes: - mongo-data:/data/db ports: - "27017:27017" networks: - mevn-network backend: build: ./backend container_name: mevn-backend environment: - MONGODB_URI=mongodb://mongodb:27017/mevn - JWT_SECRET=${JWT_SECRET} ports: - "5000:5000" volumes: - ./backend:/app - /app/node_modules networks: - mevn-network depends_on: - mongodb frontend: build: ./frontend container_name: mevn-frontend ports: - "8080:8080" volumes: - ./frontend:/app - /app/node_modules networks: - mevn-network depends_on: - backend volumes: mongo-data: networks: mevn-network:
Example 3: ELK Stack (Elasticsearch, Logstash, Kibana)
version: '3.8' services: elasticsearch: image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0 container_name: elk-elasticsearch environment: - discovery.type=single-node - xpack.security.enabled=false - ES_JAVA_OPTS=-Xms512m -Xmx512m volumes: - elasticsearch-data:/usr/share/elasticsearch/data ports: - "9200:9200" networks: - elk-network logstash: image: docker.elastic.co/logstash/logstash:8.11.0 container_name: elk-logstash volumes: - ./logstash/pipeline:/usr/share/logstash/pipeline ports: - "5000:5000" networks: - elk-network depends_on: - elasticsearch kibana: image: docker.elastic.co/kibana/kibana:8.11.0 container_name: elk-kibana environment: - ELASTICSEARCH_HOSTS=http://elasticsearch:9200 ports: - "5601:5601" networks: - elk-network depends_on: - elasticsearch volumes: elasticsearch-data: networks: elk-network:
Part 8: Best Practices
Project Structure
Organize your Compose projects consistently:
myapp/
├── docker-compose.yml
├── .env
├── .env.example
├── Dockerfile
├── app/
│ └── (application code)
├── nginx/
│ └── nginx.conf
└── postgres/
└── init.sqlUse .env Files
Never hardcode secrets in docker-compose.yml:
# docker-compose.yml services: database: environment: POSTGRES_PASSWORD: ${DB_PASSWORD}
# .env (never commit this!) DB_PASSWORD=supersecret
# .env.example (commit this) DB_PASSWORD=changeme
Name Your Containers
Explicit names make management easier:
services: database: container_name: myapp-postgres
Use Health Checks
Ensure services are ready before depending services start:
services: database: healthcheck: test: ["CMD", "pg_isready", "-U", "user"] interval: 10s app: depends_on: database: condition: service_healthy
Version Control
Commit
docker-compose.ymlCommit
.env.exampleNever commit
.envNever commit local volumes or build artifacts
Resource Limits
Prevent one service from consuming all resources:
services: worker: deploy: resources: limits: cpus: '0.5' memory: 512M reservations: cpus: '0.25' memory: 256M
Summary
| Concept | Purpose | Key Command |
|---|---|---|
| Services | Define containers | services: section |
| Networks | Enable communication | networks: section |
| Volumes | Persistent data | volumes: section |
| Up | Start everything | docker-compose up -d |
| Down | Stop everything | docker-compose down |
| Logs | View output | docker-compose logs -f |
| Exec | Run commands | docker-compose exec app bash |
Docker Compose transforms complex multi-container applications into simple, reproducible, version-controlled configurations.
Practice Questions
What are the three main sections of a docker-compose.yml file?
How do you ensure a database service starts before an application that depends on it?
How do you pass environment variables to services without hardcoding them in the compose file?
What command starts all services in detached mode?
How do you view logs for a specific service?
Learn More
Practice Docker Compose with hands-on exercises in our interactive labs:
https://devops.trainwithsky.com/
Comments
Post a Comment