Docker Compose Guide — Multi-Container Application Orchestration
Docker Compose lets you define and run multi-container applications with a single YAML file — specifying services, networks, volumes, and dependencies so your entire application stack starts with one command.
What You’ll Learn
- Writing
docker-compose.ymlfiles with services and configuration - Managing persistent data with volumes and bind mounts
- Connecting containers with custom networks and service discovery
- Using environment variables and
.envfiles - Implementing healthchecks for service dependencies
- Organizing configurations with profiles and multiple compose files
Why Docker Compose Matters
Running docker run commands for a web app, database, cache, and queue is tedious and error-prone — you have to specify ports, volumes, networks, and environment variables for each container separately. Docker Compose packages all of that into a single declarative YAML file. One docker compose up starts your entire stack, and docker compose down cleans everything up. DodaTech uses Docker Compose for local development of Durga Antivirus Pro’s web dashboard — the frontend dev server, API backend, PostgreSQL database, and Redis cache all start simultaneously with a single command.
flowchart LR
A[Docker & Bash Basics] --> B[Docker Compose]
B --> C[Services]
B --> D[Volumes]
B --> E[Networks]
B --> F[Profiles]
C --> G[Multi-Container App]
D --> H[Persistent Data]
E --> I[Service Discovery]
F --> J[Dev / Prod Configs]
style B fill:#2496ed,color:#fff
Core Concepts
The docker-compose.yml File
version: "3.9"
services:
web:
build:
context: ./web
dockerfile: Dockerfile.dev
ports:
- "3000:3000"
volumes:
- ./web:/app # Live code reload
- /app/node_modules # Exclude node_modules from bind mount
environment:
- NODE_ENV=development
- DATABASE_URL=postgres://user:pass@db:5432/myapp
depends_on:
db:
condition: service_healthy
networks:
- frontend
- backend
api:
build: ./api
ports:
- "8080:8080"
environment:
- DATABASE_URL=postgres://user:pass@db:5432/myapp
- REDIS_URL=redis://cache:6379
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
volumes:
- ./api:/app
networks:
- backend
db:
image: postgres:16-alpine
ports:
- "5432:5432"
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: myapp
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d myapp"]
interval: 5s
timeout: 5s
retries: 5
networks:
- backend
cache:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redisdata:/data
networks:
- backend
volumes:
pgdata:
redisdata:
networks:
frontend:
backend:Running the Stack
# Start all services
docker compose up
# Output:
# [+] Running 5/5
# ✔ Container db Healthy
# ✔ Container cache Started
# ✔ Container api Started
# ✔ Container web Started
# Start in detached mode
docker compose up -d
# View logs
docker compose logs -f web api
# List running services
docker compose ps
# Output:
# NAME SERVICE STATUS PORTS
# app-web-1 web Up 0.0.0.0:3000->3000/tcp
# app-api-1 api Up 0.0.0.0:8080->8080/tcp
# app-db-1 db Up (healthy) 0.0.0.0:5432->5432/tcp
# app-cache-1 cache Up 0.0.0.0:6379->6379/tcp
# Stop all services
docker compose down
# Stop and remove volumes (destroys data)
docker compose down -vOutput: Compose creates containers with the configured ports, volumes, and networks. The depends_on with condition: service_healthy ensures the API waits until PostgreSQL is accepting connections.
Environment Variables and .env Files
# docker-compose.yml — reference variables
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: ${DB_USER:-postgres}
POSTGRES_PASSWORD: ${DB_PASSWORD:-secret}
POSTGRES_DB: ${DB_NAME:-myapp}# .env file (in the same directory as docker-compose.yml)
DB_USER=admin
DB_PASSWORD=supersecret
DB_NAME=production_app
# Or pass inline
DB_PASSWORD=other ./up docker compose up# Use env_file for shared configurations
services:
web:
env_file:
- ./web/.env
- ./web/.env.local # Override values (gitignored)Profiles — Dev vs Prod
services:
web:
image: myapp:latest
profiles: ["production"] # Only started with --profile production
web-dev:
build: ./web
ports:
- "3000:3000"
volumes:
- ./web:/app
profiles: ["development"]
db:
image: postgres:16-alpine
# No profile — always started# Start development stack
docker compose --profile development up
# Start production stack
docker compose --profile production up
# Start multiple profiles
docker compose --profile development --profile tools upHealthchecks
Healthchecks ensure services are actually ready — not just started:
services:
db:
image: postgres:16-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d myapp"]
interval: 5s
timeout: 5s
retries: 5
start_period: 10s # Wait 10s before checking
api:
build: ./api
depends_on:
db:
condition: service_healthy # Wait for healthy db
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3Multiple Compose Files
Split configuration across files for different environments:
# docker-compose.yml — base config
# docker-compose.override.yml — local development overrides (auto-loaded)
# docker-compose.prod.yml — production overrides
# Override for production
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
# Note: docker-compose.override.yml is loaded automatically by default# docker-compose.prod.yml
services:
web:
build:
context: ./web
dockerfile: Dockerfile.prod
ports:
- "80:80"
- "443:443"
environment:
- NODE_ENV=production
restart: alwaysCommon Mistakes
Not using
condition: service_healthyfor database dependencies:depends_on: dbonly waits for the container to start, not for PostgreSQL to accept connections. Without a healthcheck, your app crashes on startup.Exposing database ports in production:
ports: "5432:5432"on the database service should be removed in production. Only the API should connect to the database — no external access.Storing production secrets in docker-compose.yml: Use
.envfiles (gitignored for secrets) or Docker secrets. Never commit passwords, API keys, or tokens to your repository.Not using volumes for persistent data: Without volumes, database data is lost when the container stops. Always define named volumes for stateful services.
Overriding the CMD for database images: Editing the
commandfor the postgres image breaks its initialization scripts. Use environment variables for configuration.
Practice Questions
What is the difference between a named volume and a bind mount? Answer: Named volumes (
pgdata:) are managed by Docker and persist across container lifecycles. Bind mounts (./web:/app) map a host directory into the container and allow live code editing.How does service discovery work in Docker Compose? Answer: Services on the same network can reach each other by service name (e.g.,
db:5432). Docker’s embedded DNS resolves service names to container IPs.What is the purpose of the
depends_ondirective? Answer:depends_oncontrols the startup order and, withcondition, waits for healthchecks. However, it doesn’t wait for the service to be ready — use healthchecks for that.How do profiles help manage different environments? Answer: Profiles let you define which services start in each context (development, production, testing), preventing disk/memory waste from running unnecessary services.
Challenge
Build a full-stack application stack: create a compose file with a React frontend (with hot reload), a Node.js API, PostgreSQL with persistent volume, and Redis for caching. Add healthchecks to all services, use profiles to exclude dev tools (pgadmin, redis-commander) from production, create a .env file for database credentials, and practice the full up/down lifecycle.
FAQ
Try It Yourself
# Create a minimal demo
mkdir compose-demo && cd compose-demo
cat > docker-compose.yml << 'EOF'
services:
web:
image: nginx:alpine
ports:
- "8080:80"
volumes:
- ./html:/usr/share/nginx/html
whoami:
image: traefik/whoami
ports:
- "8081:80"
EOF
mkdir html
echo "<h1>Hello from Compose!</h1>" > html/index.html
docker compose up -d
curl http://localhost:8080
# <h1>Hello from Compose!</h1>
curl http://localhost:8081
# Hostname: abc123...
docker compose downWhat’s Next
| Topic | Description |
|---|---|
| CI/CD with containers | |
| The container engine behind Compose |
Related topics: Docker, Bash, Linux, AWS
What’s Next
Congratulations on completing this Docker Compose tutorial! Here’s where to go from here:
- Practice daily — Consistency is more important than long study sessions
- Build a project — Apply what you learned by building something real
- Explore related topics — Check out other tutorials in the same category
- Join the community — Discuss with other learners and share your progress
Remember: every expert was once a beginner. Keep coding!
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro