Running one container with a long docker run command is manageable. Running five containers with networks, volumes, environment variables, and port mappings by typing individual commands becomes chaotic and error-prone. Docker Compose solves this by letting you define your entire multi-container application in a single YAML file and start everything with one command.
Docker Compose is a tool for defining and running multi-container Docker applications. You describe your application's services, networks, and volumes in a docker-compose.yml file. Then you use the docker compose up command to create and start all services together. It is the standard way to manage local development environments and simple production deployments.
Let us build a real example — a web application with Nginx, Flask, and PostgreSQL:
version: '3.8'
services:
# PostgreSQL Database
db:
image: postgres:15
environment:
POSTGRES_DB: myapp
POSTGRES_USER: appuser
POSTGRES_PASSWORD: apppassword
volumes:
- postgres-data:/var/lib/postgresql/data
networks:
- app-network
# Flask Backend
backend:
build: ./backend # Build from local Dockerfile
ports:
- "5000:5000"
environment:
DATABASE_URL: postgresql://appuser:apppassword@db:5432/myapp
depends_on:
- db
networks:
- app-network
volumes:
- ./backend:/app # Bind mount for development
# Nginx Frontend Proxy
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- backend
networks:
- app-network
volumes:
postgres-data:
networks:
app-network:
driver: bridge
# Start all services (build if needed)
docker compose up
# Start in background (detached mode)
docker compose up -d
# Build images without starting
docker compose build
# Rebuild a specific service
docker compose build backend
# Stop all services
docker compose down
# Stop and remove volumes
docker compose down -v
# View logs of all services
docker compose logs
# Follow logs of a specific service
docker compose logs -f backend
# List running services
docker compose ps
# Run a command in a service container
docker compose exec backend bash
# Scale a service (run multiple instances)
docker compose up -d --scale backend=3
The depends_on option tells Docker Compose to start services in a specific order. In our example, the backend starts after the database, and Nginx starts after the backend. However, there is an important nuance: depends_on only waits for the container to start, not for the service inside it to be ready. A PostgreSQL container can be running but not yet accepting connections. For health checks and wait logic, you need additional tooling like wait-for-it scripts or Docker healthchecks.
Hardcoding passwords in docker-compose.yml is bad practice. Use a .env file instead:
POSTGRES_PASSWORD=mysecurepassword
POSTGRES_USER=appuser
POSTGRES_DB=myapp
services:
db:
image: postgres:15
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_DB: ${POSTGRES_DB}
Docker Compose automatically reads the .env file in the same directory. Add .env to your .gitignore to avoid committing secrets.
services:
db:
image: postgres:15
healthcheck:
test: ["CMD-SHELL", "pg_isready -U appuser -d myapp"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
A common pattern is to have multiple Compose files — a base file and environment-specific overrides:
# Base configuration
docker-compose.yml
# Development overrides (bind mounts, debug settings)
docker-compose.dev.yml
# Start with development overrides
docker compose -f docker-compose.yml -f docker-compose.dev.yml up
In Part 7, we will explore Docker in real-world CI/CD pipelines — how images are built, tagged, pushed to registries, and deployed automatically in modern development workflows.
Hardcoding sensitive values in docker-compose.yml is a security risk — these files often end up in version control. Docker Compose supports several approaches for managing configuration and secrets. The .env file in the same directory as docker-compose.yml is automatically loaded and its variables are available in the compose file using ${VARIABLE_NAME} syntax. This file should be in .gitignore. For even more flexibility, each service can specify an env_file path to load environment-specific configuration:
version: '3.8'
services:
db:
image: postgres:15
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
app:
build: .
env_file:
- .env
ports:
- "${APP_PORT:-3000}:3000"
depends_on:
- db
volumes:
postgres_data:
The depends_on directive in Compose only waits for a container to start, not for the service inside to be ready. Adding health checks ensures services are actually ready before dependent services start. Define a healthcheck in the service configuration: healthcheck: test: ["CMD", "pg_isready"] interval: 10s timeout: 5s retries: 5. Then use depends_on: db: condition: service_healthy to wait for the healthy state before starting the dependent service.
Build a complete three-service application with Docker Compose: a database (PostgreSQL or MySQL), a backend API (any language), and a frontend or nginx reverse proxy. Use a .env file for all credentials and configuration. Add health checks to the database service and make the API depend on it being healthy. Test the full stack with docker compose up and verify all services start correctly and can communicate.
Cloud computing is a domain where deep intuition — the ability to make good architectural decisions quickly, to diagnose problems efficiently, and to anticipate how systems will behave under load — develops through accumulated hands-on experience. Every project you build on cloud infrastructure teaches you something that cannot be learned from documentation alone. The cost surprises, the permission errors, the networking debugging sessions, the performance investigations — these are not obstacles to learning, they are the learning. The engineers who have built genuinely deep cloud intuition have usually accumulated it through many projects over several years, not from any single course or certification. Start building things, make mistakes safely in learning environments, and accumulate that experience deliberately.