Docker Compose Deep Dive — Services, Networks, Volumes & Production Deployments
Docker Compose defines and runs multi-container Docker applications using a single YAML file. In this deep dive, you’ll learn how to orchestrate services, manage networks and volumes, implement healthchecks, use profiles for environment-specific configs, and understand when Compose fits versus full Kubernetes orchestration.
compose.yml.Learning Path
flowchart LR
A["Docker Basics<br/>Containers & Images"] --> B["Docker Compose<br/>This Tutorial"]
B --> C["Production Compose<br/>Overrides & Profiles"]
C --> D["Kubernetes<br/>Orchestration"]
style B fill:#f90,color:#fff,stroke-width:2px
Compose File Structure
A standard compose.yml has four top-level sections:
services:
web:
image: nginx:alpine
ports:
- "80:80"
networks:
- frontend
volumes:
- ./html:/usr/share/nginx/html
networks:
frontend:
driver: bridge
volumes:
data:
driver: localExpected output: docker compose up starts Nginx serving your local ./html directory on port 80.
Services Deep Dive
Build Context and Dockerfile
services:
api:
build:
context: .
dockerfile: Dockerfile.api
args:
NODE_ENV: production
image: myapp/api:latestdepends_on with Healthchecks
services:
db:
image: postgres:16-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 5
api:
build: ./api
depends_on:
db:
condition: service_healthyThe API waits for PostgreSQL to be truly ready, not just started.
Environment Variables
services:
app:
image: myapp:latest
environment:
- DATABASE_URL=postgres://user:pass@db:5432/mydb
- DEBUG=true
env_file:
- .env.productionNetworks Configuration
flowchart TD
subgraph "Frontend Network"
N["Nginx<br/>Proxy"]
end
subgraph "Backend Network"
A["API<br/>Container"]
R["Redis<br/>Cache"]
end
subgraph "Database Network"
P["PostgreSQL"]
end
N -->|"proxy_pass"| A
A --> R
A --> P
services:
nginx:
image: nginx:alpine
networks:
- frontend
- backend
api:
build: ./api
networks:
- backend
- dbnet
db:
image: postgres:16-alpine
networks:
- dbnet
networks:
frontend:
backend:
dbnet:Volumes: Named vs Bind Mounts
services:
db:
image: postgres:16-alpine
volumes:
- pgdata:/var/lib/postgresql/data
- ./backups:/backups
volumes:
pgdata:- Named volumes (
pgdata) — Docker-managed, persisted across restarts - Bind mounts (
./backups) — map host directory directly
Healthchecks and Restart Policies
services:
app:
build: ./app
healthcheck:
test: curl -f http://localhost:3000/health || exit 1
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
restart: unless-stoppedProfiles for Dev vs Production
services:
app:
image: myapp:latest
profiles:
- production
- staging
mailhog:
image: mailhog/mailhog
profiles:
- development
prometheus:
image: prom/prometheus
profiles:
- monitoringStart specific profiles:
docker compose --profile development up
docker compose --profile production upMulti-Stage Builds in Compose
services:
api:
build:
context: .
target: production
dockerfile: DockerfileThe Dockerfile:
FROM node:20-alpine AS base
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM base AS development
RUN npm install -g nodemon
CMD ["nodemon", "server.js"]
FROM base AS production
COPY . .
RUN npm run build
CMD ["node", "dist/server.js"]docker-compose.override.yml
Compose automatically merges compose.override.yml if present:
# compose.override.yml (development)
services:
app:
ports:
- "3000:3000"
volumes:
- .:/app
environment:
- DEBUG=trueYour base compose.yml stays production-ready; override only for local dev.
Production Best Practices
services:
app:
image: myapp:${TAG:-latest}
deploy:
replicas: 3
resources:
limits:
cpus: "0.5"
memory: "512M"
reservations:
cpus: "0.25"
memory: "256M"
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"Docker Compose vs Kubernetes
| Feature | Docker Compose | Kubernetes |
|---|---|---|
| Setup complexity | Minimal | High |
| Multi-host | No (single host) | Yes |
| Auto-scaling | Manual replicas | Horizontal Pod Autoscaler |
| Self-healing | Restart only | Full reconciliation |
| Service discovery | DNS within Compose | DNS + API |
| Rolling updates | Yes | Advanced strategies |
| Best for | Dev/CI/single-host | Multi-host production |
Common Errors
- Port conflicts — “port is already allocated” means another container or host process uses the same port. Change the host port:
"8080:80"instead of"80:80". - depends_on without healthcheck — Container A starts before B but B isn’t ready yet. Use
condition: service_healthyfor true readiness. - Bind mount permission denied — The container user (often root) can’t access host files owned by your user. Set user IDs in the Compose file or fix host permissions.
- Network not found — If you declare a network as external that doesn’t exist, Compose fails. Create it first:
docker network create mynet. - Environment variable interpolation —
${VAR}in Compose files references the host shell environment. IfVARis unset, Compose warns. Set defaults:${VAR:-default}. - Volume data loss on
docker compose down -v— The-vflag removes named volumes. Always back up databases before this command. - Compose file version confusion — The
version:field is obsolete since Docker Compose V2. Omit it and use the modern format. - Relative build context paths — The
context:path is relative to the Compose file location, not the working directory. Use absolute paths for CI pipelines.
Practice Questions
1. What’s the difference between depends_on with and without condition: service_healthy?
Without condition, Compose waits only for the container to start. With condition: service_healthy, it waits for the healthcheck to pass, ensuring the service is actually ready.
2. How do you share data between containers in Compose? Use named volumes mounted into multiple containers, or use a common network with shared volume mounts.
3. What happens when you run docker compose --profile monitoring up?
Only services with the monitoring profile (and services with no profile) start. Services with other profiles are excluded.
4. How does Compose resolve service hostnames?
Compose creates a DNS network where each service is reachable by its service name. The api service is reachable from web at http://api:3000.
5. Challenge: Multi-tier application stack Create a Compose file with: a React frontend (Nginx), a Node.js API, a PostgreSQL database with healthcheck, Redis cache, and a monitoring profile that adds Prometheus and Grafana. The API should wait for both DB and Redis to be healthy.
Mini Project: Compose-Based CI Pipeline
services:
test:
build: ./app
command: npm test
environment:
- DATABASE_URL=postgres://test:test@db:5432/testdb
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
healthcheck:
test: ["CMD-SHELL", "pg_isready -U test"]
interval: 2s
timeout: 2s
retries: 10Run docker compose up test --abort-on-container-exit — tests pass or fail deterministically.
FAQ
Related Tutorials
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro