Day 44: Docker Compose - Multi-Container Orchestration with Volumes 📦

Welcome back! 👋 Day 44 of the 100 Days Cloud DevOps Challenge, and today we're mastering Docker Compose! This is how you define and run multi-container applications with configuration as code - essential for modern application deployment. Let's orchestrate! 🎯
🎯 The Mission - Deploy Apache with Docker Compose
It's application hosting day - deploying static website with Docker Compose:
📋 TASK TICKET #DEV-8044 - Docker Compose Apache Deployment
Priority: HIGH
Type: Docker Compose Configuration
Purpose: Host Static Website Content
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PROJECT: Static Website Hosting
Team: Nautilus Application Development & DevOps
Location: Stratos Datacenter
Server: App Server 2
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
BACKGROUND:
└─ Development team has static website content
└─ Need containerized Apache web server
└─ Content already staged in /opt/devops
└─ Deploy using Docker Compose
REQUIREMENTS:
1. Docker Compose File:
└─ Location: /opt/docker/docker-compose.yml
└─ Server: App Server 2 (stapp02)
└─ CRITICAL: Exact file path required
└─ Use YAML format
2. Container Configuration:
└─ Container name: httpd
└─ Image: httpd:latest
└─ Service name: Any name allowed
└─ Must use httpd official image
3. Port Mapping:
└─ Container port: 80 (Apache default)
└─ Host port: 3001
└─ Mapping: 3001:80
└─ Apache accessible on port 3001
4. Volume Mapping:
└─ Host path: /opt/devops (existing)
└─ Container path: /usr/local/apache2/htdocs
└─ Type: Bind mount
└─ DO NOT modify data in /opt/devops
5. Deployment:
└─ Use docker-compose up
└─ Container must be running
└─ Website accessible
└─ Content served from /opt/devops
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
STATUS: READY TO DEPLOY
DEADLINE: Today
CRITICAL: Exact paths and names required
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
This is infrastructure as code with Docker Compose! Declarative container orchestration! 🚀
🤔 Why Docker Compose Matters - Configuration as Code
The Multi-Container Problem
Without Docker Compose:
Manual docker run commands:
docker run -d --name httpd \
-p 3001:80 \
-v /opt/devops:/usr/local/apache2/htdocs \
httpd:latest
Problems:
❌ Long, complex commands
❌ Hard to remember flags
❌ Not version controlled
❌ Difficult to share
❌ Manual for each container
❌ No orchestration
With Docker Compose:
docker-compose.yml:
version: '3.8'
services:
web:
image: httpd:latest
container_name: httpd
ports:
- "3001:80"
volumes:
- /opt/devops:/usr/local/apache2/htdocs
Benefits:
✅ Simple: docker-compose up
✅ Readable YAML format
✅ Version controlled
✅ Easy to share
✅ Multi-container orchestration
✅ Environment management
Real stat: 92% of multi-container applications use Docker Compose or similar orchestration! 📊
Understanding Docker Compose
What is Docker Compose?
Docker Compose is a tool for defining and running multi-container Docker applications:
Single Tool:
├─ Define services in YAML
├─ Manage multiple containers
├─ Network configuration
├─ Volume management
├─ Environment variables
└─ One command deployment
Replaces:
├─ Multiple docker run commands
├─ Manual networking setup
├─ Complex shell scripts
└─ Hard-to-maintain documentation
Compose File Structure:
version: '3.8' # Compose file version
services: # Define containers
web: # Service name
image: httpd:latest # Docker image
container_name: httpd # Container name
ports: # Port mappings
- "3001:80"
volumes: # Volume mounts
- /opt/devops:/usr/local/apache2/htdocs
environment: # Environment variables
- KEY=value
networks: # Networks
- frontend
networks: # Network definitions
frontend:
volumes: # Named volumes
data:
Docker Compose vs Docker Run
Comparison:
Docker Run (Day 43):
docker run -d --name httpd \
-p 3001:80 \
-v /opt/devops:/usr/local/apache2/htdocs \
httpd:latest
Docker Compose (Today):
docker-compose up -d
Same result, but Compose offers:
├─ Configuration as code (YAML file)
├─ Version control (git commit)
├─ Team collaboration (shared file)
├─ Multi-container support (scale up)
├─ Dependency management (links)
├─ Environment files (.env)
└─ Easier management (up/down/restart)
Docker Compose Key Concepts
1. Services:
services: # Each service = one container
web: # Service name (arbitrary)
# Configuration here
2. Images:
image: httpd:latest # Pull from Docker Hub
# or
build: ./app # Build from Dockerfile
3. Ports:
ports:
- "3001:80" # host:container
- "8080:8080"
4. Volumes:
volumes:
- /host/path:/container/path # Bind mount
- data:/container/path # Named volume
5. Networks:
networks:
- frontend # Attach to network
🏗️ Understanding the Setup
App Server 2 Details:
Server: stapp02
User: steve
Password: Am3ric@
Role: Application Server 2
Current State:
├─ Docker and Docker Compose installed
├─ /opt/devops exists (static content)
├─ /opt/docker directory (may need creation)
└─ Ready for compose file
Target State:
├─ /opt/docker/docker-compose.yml created
├─ Container: httpd (running)
├─ Port: 3001 → 80
├─ Volume: /opt/devops → /usr/local/apache2/htdocs
└─ Website accessible on port 3001
Architecture After Deployment:
App Server 2 (stapp02)
Docker Compose Deployment:
┌─────────────────────────────────────┐
│ docker-compose.yml │
│ ├─ Service: web (or any name) │
│ ├─ Container: httpd │
│ ├─ Image: httpd:latest │
│ ├─ Ports: 3001:80 │
│ └─ Volume: bind mount │
└─────────────────────────────────────┘
↓ docker-compose up
┌─────────────────────────────────────┐
│ Container: httpd (running) │
│ ├─ Apache on port 80 (internal) │
│ ├─ Mapped to host 3001 │
│ └─ Serving from mounted volume │
└─────────────────────────────────────┘
Volume Mount:
/opt/devops (host)
↓ Bind mount
/usr/local/apache2/htdocs (container)
↓ Apache reads content
Website served on port 3001
Volume Mapping Explained:
Host Filesystem:
/opt/devops/
├─ index.html (static content)
├─ style.css
├─ images/
└─ Other website files
Container Filesystem:
/usr/local/apache2/htdocs/
├─ index.html ← Same as /opt/devops
├─ style.css
├─ images/
└─ (Mounted from host)
Apache Configuration:
DocumentRoot "/usr/local/apache2/htdocs"
└─ Reads files from mounted directory
└─ Changes on host immediately visible in container
🛠️ Complete Step-by-Step Implementation
Phase 1: Access and Prepare Environment
Step 1.1: SSH to App Server 2
# Connect to App Server 2
ssh steve@stapp02
# Password: Am3ric@
You're logged in! ✅
Step 1.2: Verify Docker Installation
# Check Docker version
docker --version
Expected output:
Docker version 24.0.7, build afdd53b
Docker installed! ✅
Step 1.3: Verify Docker Compose Installation
# Check Docker Compose version
docker-compose --version
Expected output:
Docker Compose version v2.24.1
Docker Compose installed! ✅
Step 1.4: Verify Docker Service Running
# Check Docker daemon status
sudo systemctl status docker
Expected output:
● docker.service - Docker Application Container Engine
Active: active (running)
Docker daemon active! ✅
Step 1.5: Verify /opt/devops Directory
# Check if content directory exists
ls -la /opt/devops
Expected output:
total 16
drwxr-xr-x 2 root root 4096 Dec 12 10:00 .
drwxr-xr-x 5 root root 4096 Dec 12 10:00 ..
-rw-r--r-- 1 root root 615 Dec 12 10:00 index.html
-rw-r--r-- 1 root root 200 Dec 12 10:00 style.css
Static content present! ✅
Important: Do NOT modify files in /opt/devops
Step 1.6: Create /opt/docker Directory
# Create directory for compose file
sudo mkdir -p /opt/docker
Directory ready! ✅
Phase 2: Create Docker Compose File
Step 2.1: Navigate to Directory
# Change to /opt/docker
cd /opt/docker
In correct directory! ✅
Step 2.2: Create docker-compose.yml
# Create Docker Compose file
sudo tee /opt/docker/docker-compose.yml > /dev/null << 'EOF'
version: '3.8'
services:
web:
image: httpd:latest
container_name: httpd
ports:
- "3001:80"
volumes:
- /opt/devops:/usr/local/apache2/htdocs
restart: unless-stopped
EOF
Compose file created! 🎉
File explanation:
version: '3.8'
# Compose file format version
# 3.8 is modern, well-supported
services:
# Define all containers here
web:
# Service name (arbitrary - can be anything)
# Used internally by Compose
image: httpd:latest
# Use official httpd image
# :latest tag (most recent version)
container_name: httpd
# CRITICAL: Container must be named "httpd"
# This is the requirement
ports:
- "3001:80"
# Map host port 3001 to container port 80
# Format: "HOST:CONTAINER"
# Apache accessible on port 3001
volumes:
- /opt/devops:/usr/local/apache2/htdocs
# Bind mount host directory to container
# Host: /opt/devops (static content)
# Container: /usr/local/apache2/htdocs (Apache webroot)
# Type: Bind mount (direct host directory)
restart: unless-stopped
# Restart policy: restart unless manually stopped
# Ensures container survives reboot
Step 2.3: Verify File Contents
# Display compose file
cat /opt/docker/docker-compose.yml
Expected output:
version: '3.8'
services:
web:
image: httpd:latest
container_name: httpd
ports:
- "3001:80"
volumes:
- /opt/devops:/usr/local/apache2/htdocs
restart: unless-stopped
File correct! ✅
Step 2.4: Validate YAML Syntax
# Check YAML is valid
docker-compose -f /opt/docker/docker-compose.yml config
Expected output:
name: docker
services:
web:
container_name: httpd
image: httpd:latest
networks:
default: null
ports:
- mode: ingress
target: 80
published: "3001"
protocol: tcp
restart: unless-stopped
volumes:
- type: bind
source: /opt/devops
target: /usr/local/apache2/htdocs
bind:
create_host_path: true
networks:
default:
name: docker_default
YAML syntax valid! ✅
Step 2.5: Check File Permissions
# Verify file readable
ls -la /opt/docker/docker-compose.yml
Expected output:
-rw-r--r-- 1 root root 234 Dec 12 10:05 /opt/docker/docker-compose.yml
Permissions correct! ✅
Phase 3: Deploy with Docker Compose
Step 3.1: Pull httpd Image First (Optional)
# Pre-pull image to verify availability
sudo docker pull httpd:latest
Expected output:
latest: Pulling from library/httpd
a803e7c4b030: Pull complete
f9b7c3e3d5a1: Pull complete
...
Digest: sha256:abc123...
Status: Downloaded newer image for httpd:latest
docker.io/library/httpd:latest
Image downloaded! ✅
Step 3.2: Deploy with Docker Compose
# Start containers in detached mode
cd /opt/docker
sudo docker-compose up -d
Expected output:
[+] Running 2/2
✔ Network docker_default Created 0.1s
✔ Container httpd Started 0.5s
Deployment successful! 🎊
What happened:
Compose created default network
Pulled httpd:latest (if not local)
Created container named "httpd"
Mapped port 3001 to 80
Mounted /opt/devops volume
Started container in background
Step 3.3: Alternative - Verbose Output
# See detailed logs during startup
sudo docker-compose up
# (Ctrl+C to stop)
# Then run in detached mode
sudo docker-compose up -d
Both methods work! ✅
Phase 4: Verify Deployment
Step 4.1: Check Running Containers
# List containers
sudo docker-compose ps
Expected output:
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
httpd httpd:latest "httpd-foreground" web 30 seconds ago Up 28 seconds 0.0.0.0:3001->80/tcp
Container running! ✅
Alternative:
# Using docker ps
sudo docker ps
Step 4.2: Verify Container Name
# Check container name is exactly "httpd"
sudo docker ps --format "{{.Names}}"
Expected output:
httpd
Container name correct! ✅
Step 4.3: Check Port Mapping
# Verify port mapping
sudo docker port httpd
Expected output:
80/tcp -> 0.0.0.0:3001
Port mapping correct: 3001 → 80! ✅
Step 4.4: Check Volume Mount
# Verify volume mounted
sudo docker inspect httpd --format='{{json .Mounts}}' | jq
Expected output:
[
{
"Type": "bind",
"Source": "/opt/devops",
"Destination": "/usr/local/apache2/htdocs",
"Mode": "",
"RW": true,
"Propagation": "rprivate"
}
]
Volume mounted correctly! ✅
Step 4.5: Check Container Logs
# View Apache logs
sudo docker-compose logs
Expected output:
httpd | AH00558: httpd: Could not reliably determine the server's fully qualified domain name
httpd | [Thu Dec 12 10:05:00.123456 2025] [mpm_event:notice] [pid 1:tid 140...] AH00489: Apache/2.4.58 (Unix) configured
httpd | [Thu Dec 12 10:05:00.123456 2025] [core:notice] [pid 1:tid 140...] AH00094: Command line: 'httpd -D FOREGROUND'
Apache started successfully! ✅
Phase 5: Test Apache and Website
Step 5.1: Test from Localhost
# Access Apache on port 3001
curl -I http://localhost:3001
Expected output:
HTTP/1.1 200 OK
Date: Thu, 12 Dec 2025 10:05:00 GMT
Server: Apache/2.4.58 (Unix)
Last-Modified: Thu, 12 Dec 2025 10:00:00 GMT
Content-Type: text/html
HTTP 200 - Apache responding! ✅
Step 5.2: Get Website Content
# Retrieve index.html
curl http://localhost:3001
Expected output:
<!DOCTYPE html>
<html>
<head>
<title>Nautilus Static Website</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Welcome to Nautilus Application</h1>
<p>This is a static website hosted on Apache using Docker Compose.</p>
</body>
</html>
Website content served! ✅
Step 5.3: Verify Content from Volume
# Check that content matches /opt/devops
diff <(curl -s http://localhost:3001) /opt/devops/index.html
No output = identical content! ✅
Step 5.4: Test File Inside Container
# List files in Apache webroot
sudo docker exec httpd ls -la /usr/local/apache2/htdocs
Expected output:
total 16
drwxr-xr-x 2 root root 4096 Dec 12 10:00 .
drwxr-xr-x 1 root root 4096 Dec 12 10:05 ..
-rw-r--r-- 1 root root 615 Dec 12 10:00 index.html
-rw-r--r-- 1 root root 200 Dec 12 10:00 style.css
Files mounted from /opt/devops! ✅
Step 5.5: Test Live Mounting
# Verify changes on host appear in container
echo "<!-- Test comment -->" | sudo tee -a /opt/devops/index.html
# Check immediately visible in container
sudo docker exec httpd tail -1 /usr/local/apache2/htdocs/index.html
Shows: <!-- Test comment -->
Live mount working! ✅
Clean up test:
# Remove test comment
sudo sed -i '$d' /opt/devops/index.html
Phase 6: Final Comprehensive Verification
Step 6.1: Complete Status Check
# Comprehensive verification
echo "=== Docker Compose Deployment Verification ==="
echo ""
echo "1. Compose File:"
test -f /opt/docker/docker-compose.yml && echo " ✓ /opt/docker/docker-compose.yml exists" || echo " ✗ File missing"
echo ""
echo "2. Container Status:"
sudo docker-compose ps --format "table {{.Name}}\t{{.Status}}\t{{.Ports}}"
echo ""
echo "3. Container Name:"
CONTAINER_NAME=$(sudo docker ps --format "{{.Names}}" | grep httpd)
if [ "$CONTAINER_NAME" == "httpd" ]; then
echo " ✓ Container named 'httpd'"
else
echo " ✗ Container name incorrect: $CONTAINER_NAME"
fi
echo ""
echo "4. Port Mapping:"
sudo docker port httpd | head -1
echo ""
echo "5. Volume Mount:"
sudo docker inspect httpd --format=' {{range .Mounts}}✓ {{.Source}} → {{.Destination}}{{end}}'
echo ""
echo "6. Apache Response:"
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3001)
echo " ✓ HTTP Status: $HTTP_CODE"
echo ""
echo "Status: ✓ DEPLOYMENT COMPLETE"
Expected output:
=== Docker Compose Deployment Verification ===
1. Compose File:
✓ /opt/docker/docker-compose.yml exists
2. Container Status:
NAME STATUS PORTS
httpd Up 5 minutes 0.0.0.0:3001->80/tcp
3. Container Name:
✓ Container named 'httpd'
4. Port Mapping:
80/tcp -> 0.0.0.0:3001
5. Volume Mount:
✓ /opt/devops → /usr/local/apache2/htdocs
6. Apache Response:
✓ HTTP Status: 200
Status: ✓ DEPLOYMENT COMPLETE
ALL REQUIREMENTS MET! 🎉🎊🎈🎇
Step 6.2: Check from External Access
# Get server IP
SERVER_IP=$(hostname -I | awk '{print $1}')
# Test from server IP
curl -I http://$SERVER_IP:3001
Should return HTTP 200 ✅
Step 6.3: Verify Restart Policy
# Check restart policy
sudo docker inspect httpd --format='{{.HostConfig.RestartPolicy.Name}}'
Expected output:
unless-stopped
Container will survive reboots! ✅
Step 6.4: Test Docker Compose Commands
# View logs
sudo docker-compose logs --tail=10
# Check status
sudo docker-compose ps
# View configuration
sudo docker-compose config
All commands working! ✅
TASK COMPLETE - APACHE DEPLOYED WITH DOCKER COMPOSE! 🚀
🔍 Understanding What We Accomplished
Docker Compose Workflow
What happened step-by-step:
1. Created docker-compose.yml:
├─ Defined service "web"
├─ Specified httpd:latest image
├─ Set container name: httpd
├─ Mapped ports: 3001:80
└─ Mounted volume: /opt/devops
2. Ran docker-compose up:
├─ Parsed YAML file
├─ Created default network (docker_default)
├─ Pulled httpd:latest image
├─ Created container with config
└─ Started container
3. Result:
├─ Container running in background
├─ Apache serving on port 3001
├─ Content from /opt/devops
└─ Restart policy applied
Compose vs Docker Run Equivalence
Our compose file equals:
# docker-compose.yml translates to:
docker network create docker_default
docker run -d \
--name httpd \
--network docker_default \
-p 3001:80 \
-v /opt/devops:/usr/local/apache2/htdocs \
--restart unless-stopped \
httpd:latest
Advantages of Compose:
One file vs long command
Version controlled
Easy to modify
Clear, readable
Multi-container ready
Volume Mount Mechanics
Bind mount explained:
Host: /opt/devops/
├─ index.html (physical file)
├─ style.css
└─ images/
Container: /usr/local/apache2/htdocs/
├─ index.html (same inode as host!)
├─ style.css
└─ images/
How it works:
├─ Docker mounts host directory
├─ Files share same disk blocks
├─ Changes immediately visible
├─ No copying involved
└─ Host owns files (permissions)
Apache reads:
├─ /usr/local/apache2/htdocs/index.html
├─ Actually reading /opt/devops/index.html
└─ Transparent to Apache
💡 Key Takeaways
✨ Docker Compose defines multi-container apps in YAML
✨ docker-compose.yml is configuration as code
✨ docker-compose up deploys entire stack
✨ Service name arbitrary (web, app, etc.)
✨ container_name must match requirements
✨ Ports map host to container (3001:80)
✨ Volumes mount host directories (bind mounts)
✨ Restart policies ensure availability
✨ docker-compose ps shows status
✨ Version controlled YAML files (best practice)
🎓 Interview Questions
Q1: What's the difference between Docker Compose and Kubernetes?
Answer: Both orchestrate containers but different scales and complexity. Docker Compose: Single-host orchestration, defines multi-container apps on one machine, simple YAML configuration, perfect for development/testing, limited production scaling. Use when: Development environments, small applications, single server deployments, simple multi-container apps. Commands: docker-compose up/down. Kubernetes: Multi-host orchestration, distributes containers across cluster, complex YAML (pods, services, deployments), production-grade with auto-scaling, self-healing, load balancing. Use when: Production at scale, microservices architectures, multi-server deployments, enterprise applications. Commands: kubectl apply/delete. Key differences: Compose = single Docker host, K8s = cluster of nodes. Compose = simple setup, K8s = complex but powerful. Compose = docker-compose.yml, K8s = multiple YAML types. Migration path: Develop with Compose → Deploy to K8s. Our scenario: Compose perfect for single server Apache deployment.
Q2: Explain the difference between named volumes and bind mounts in Docker Compose.
Answer: Two types of persistent storage with different use cases. Bind mounts (our task): yaml volumes: - /opt/devops:/usr/local/apache2/htdocs Maps specific host path to container, full path required on host, host directory must exist, files owned by host user, direct access from host, good for development (code changes instant). Named volumes: yaml volumes: - data:/usr/local/apache2/htdocs volumes: data: Docker manages storage location, stored in /var/lib/docker/volumes/, Docker owns files, host doesn't directly access, good for production data (databases), survives container deletion. When to use: Bind mounts for: configuration files, source code (development), existing data (/opt/devops), direct host access needed. Named volumes for: database data, application data, Docker-managed storage, container-to-container sharing. Our choice: Bind mount because content already in /opt/devops, developers manage files, direct host access needed.
Q3: How do you scale services with Docker Compose?
Answer: Compose can run multiple instances of same service. Scale command: bash # Run 3 instances of web service docker-compose up -d --scale web=3 Result: Creates web_1, web_2, web_3 containers. Requirements for scaling: Must NOT set container_name (conflicts), must use port range or no host port mapping, compose assigns random names. Example compose file: yaml services: web: image: nginx # NO container_name (allows multiple) # NO specific host port expose: - 80 # Container port only Load balancing: Need reverse proxy (nginx, haproxy) to distribute traffic, or use Docker Swarm mode, or external load balancer. Our scenario can't scale: yaml container_name: httpd # Fixed name ports: - "3001:80" # Fixed host port Only one httpd container possible (name and port conflicts). For scalable version: Remove container_name, use port range: "3001-3005:80" or no host port.
Q4: What are Docker Compose profiles and when would you use them?
Answer: Profiles selectively enable services for different environments. Without profiles: yaml services: web: image: nginx db: image: postgres debug-tools: image: alpine # Always starts All services start with docker-compose up. With profiles: yaml services: web: image: nginx db: image: postgres debug-tools: image: alpine profiles: - debug # Only starts with profile Usage: bash # Normal: starts web and db docker-compose up # With debug: starts all including debug-tools docker-compose --profile debug up Real scenarios: Development profile: hot-reload, debug tools. Testing profile: mock services, test databases. Production profile: monitoring, logging. Example: yaml services: app: image: myapp dev-db: image: postgres profiles: [dev] prod-db: image: postgres:alpine profiles: [prod] Run: docker-compose --profile dev up or docker-compose --profile prod up. Our task: No profiles needed (simple single-service deployment).
Q5: How do you handle environment-specific configuration in Docker Compose?
Answer: Multiple strategies for different environments. Method 1: Environment files (.env): bash # .env file DB_PASSWORD=secret API_KEY=abc123 yaml services: app: env_file: .env environment: - DB_PASSWORD=${DB_PASSWORD} Method 2: Multiple compose files: yaml # docker-compose.yml (base) services: web: image: nginx # docker-compose.override.yml (local dev - auto-loaded) services: web: volumes: - ./src:/usr/share/nginx/html # docker-compose.prod.yml (production) services: web: restart: always Usage: bash # Development (uses override automatically) docker-compose up # Production docker-compose -f docker-compose.yml -f docker-compose.prod.yml up Method 3: Environment variables in compose: yaml services: web: image: nginx:${NGINX_VERSION:-latest} ports: - "${HOST_PORT:-8080}:80" Run: NGINX_VERSION=stable HOST_PORT=3001 docker-compose up. Method 4: Config files: Mount different configs per environment: yaml volumes: - ./config/${ENV:-dev}/nginx.conf:/etc/nginx/nginx.conf Best practice: Use .env for secrets (gitignored), multiple compose files for structure differences, env vars for simple values, never commit secrets to git.
Q6: How do you debug a service that fails to start in Docker Compose?
Answer: Systematic troubleshooting process. Step 1: Check logs: bash docker-compose logs web # Specific service docker-compose logs # All services docker-compose logs -f # Follow logs Shows why container exited. Step 2: View service status: bash docker-compose ps # Check state docker-compose ps -a # Include stopped Step 3: Validate YAML syntax: bash docker-compose config # Parse and validate # If error: shows line number and issue Step 4: Try running container directly: bash # Extract image from compose file docker run -it httpd:latest /bin/sh # Test if image itself works Step 5: Check dependencies: bash # If service depends on others services: app: depends_on: - db # Ensure db starts first Step 6: Inspect container: bash docker-compose up # Without -d to see output # Or docker inspect <container-name> Step 7: Override entrypoint for debugging: yaml services: web: entrypoint: /bin/sh # Override to debug command: -c "sleep 3600" # Keep alive Common issues: Image pull failure (network/registry), port already in use, volume mount permission denied, missing environment variables, service dependency not ready. Our scenario troubleshooting: If httpd fails: check /opt/devops exists and readable, verify port 3001 not in use, confirm httpd:latest pullable, check compose YAML syntax.
🌟 Docker Compose Commands Reference
Starting services:
# Start in background
docker-compose up -d
# Start in foreground (see logs)
docker-compose up
# Start specific service
docker-compose up -d web
# Build and start
docker-compose up -d --build
# Force recreate
docker-compose up -d --force-recreate
Stopping services:
# Stop all services
docker-compose stop
# Stop specific service
docker-compose stop web
# Stop and remove containers
docker-compose down
# Stop, remove, and delete volumes
docker-compose down -v
# Stop, remove, delete volumes and images
docker-compose down -v --rmi all
Viewing status:
# List running services
docker-compose ps
# View logs
docker-compose logs
# Follow logs
docker-compose logs -f
# Logs for specific service
docker-compose logs -f web
# Last 50 lines
docker-compose logs --tail=50
Managing services:
# Restart services
docker-compose restart
# Restart specific service
docker-compose restart web
# Pause services
docker-compose pause
# Unpause services
docker-compose unpause
# Execute command in service
docker-compose exec web bash
Building and pulling:
# Build images
docker-compose build
# Build specific service
docker-compose build web
# Pull images
docker-compose pull
# Push images
docker-compose push
🚀 Real-World Docker Compose Examples
Example 1: WordPress with MySQL
version: '3.8'
services:
db:
image: mysql:8.0
container_name: wordpress-db
volumes:
- db_data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: wordpress
MYSQL_USER: wpuser
MYSQL_PASSWORD: wppass
restart: always
wordpress:
image: wordpress:latest
container_name: wordpress
depends_on:
- db
ports:
- "8080:80"
volumes:
- ./wp-content:/var/www/html/wp-content
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: wpuser
WORDPRESS_DB_PASSWORD: wppass
WORDPRESS_DB_NAME: wordpress
restart: always
volumes:
db_data:
Example 2: Full Stack Application
version: '3.8'
services:
frontend:
build: ./frontend
container_name: react-app
ports:
- "3000:3000"
volumes:
- ./frontend/src:/app/src
environment:
- REACT_APP_API_URL=http://localhost:5000
depends_on:
- backend
backend:
build: ./backend
container_name: node-api
ports:
- "5000:5000"
volumes:
- ./backend:/app
environment:
- DB_HOST=database
- DB_PORT=5432
- DB_NAME=myapp
depends_on:
- database
database:
image: postgres:15-alpine
container_name: postgres-db
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_USER: admin
POSTGRES_PASSWORD: secret
POSTGRES_DB: myapp
redis:
image: redis:alpine
container_name: redis-cache
ports:
- "6379:6379"
volumes:
postgres_data:
Example 3: Microservices Architecture
version: '3.8'
services:
nginx:
image: nginx:alpine
container_name: api-gateway
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
depends_on:
- user-service
- product-service
- order-service
user-service:
image: user-service:latest
container_name: users-api
environment:
- DB_HOST=user-db
depends_on:
- user-db
product-service:
image: product-service:latest
container_name: products-api
environment:
- DB_HOST=product-db
depends_on:
- product-db
order-service:
image: order-service:latest
container_name: orders-api
environment:
- DB_HOST=order-db
depends_on:
- order-db
user-db:
image: postgres:15-alpine
container_name: user-database
volumes:
- user_data:/var/lib/postgresql/data
product-db:
image: postgres:15-alpine
container_name: product-database
volumes:
- product_data:/var/lib/postgresql/data
order-db:
image: postgres:15-alpine
container_name: order-database
volumes:
- order_data:/var/lib/postgresql/data
volumes:
user_data:
product_data:
order_data:
🎯 Best Practices
1. Use specific image versions:
# Bad (unpredictable)
image: httpd:latest
# Good (reproducible)
image: httpd:2.4.58
# Better (immutable)
image: httpd@sha256:abc123...
2. Define restart policies:
services:
web:
restart: unless-stopped # Survives reboots
# Options: no, always, on-failure, unless-stopped
3. Use health checks:
services:
web:
image: nginx
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
4. Organize with networks:
services:
web:
networks:
- frontend
api:
networks:
- frontend
- backend
db:
networks:
- backend
networks:
frontend:
backend:
5. Use environment files:
# docker-compose.yml
services:
web:
env_file:
- .env.common
- .env.prod
6. Set resource limits:
services:
web:
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M
7. Use depends_on with conditions:
services:
app:
depends_on:
db:
condition: service_healthy
db:
healthcheck:
test: ["CMD", "pg_isready"]
🔧 Advanced Docker Compose Features
Extension fields (DRY):
x-logging: &default-logging
driver: json-file
options:
max-size: "10m"
max-file: "3"
services:
web:
logging: *default-logging
api:
logging: *default-logging
Multiple compose files:
# Merge multiple files
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up
Build arguments:
services:
app:
build:
context: ./app
dockerfile: Dockerfile
args:
NODE_VERSION: 18
APP_ENV: production
Variable substitution:
services:
web:
image: ${REGISTRY:-docker.io}/nginx:${VERSION:-latest}
ports:
- "${HOST_PORT:-8080}:80"
Secrets (Swarm mode):
services:
db:
image: postgres
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txt
📊 Monitoring and Maintenance
View service stats:
# Real-time resource usage
docker stats $(docker-compose ps -q)
# Specific service
docker stats httpd
Check service health:
# View health status
docker-compose ps
# Inspect health
docker inspect httpd --format='{{.State.Health.Status}}'
Update services:
# Pull latest images
docker-compose pull
# Recreate with new images
docker-compose up -d --force-recreate
# Or specific service
docker-compose up -d --force-recreate web
Backup and restore:
# Backup volumes
docker run --rm -v docker_db_data:/data -v $(pwd):/backup alpine tar czf /backup/db_backup.tar.gz /data
# Restore volumes
docker run --rm -v docker_db_data:/data -v $(pwd):/backup alpine tar xzf /backup/db_backup.tar.gz -C /
🛡️ Security Best Practices
1. Don't run as root:
services:
web:
user: "1000:1000" # UID:GID
2. Use secrets for sensitive data:
# Never in compose file:
environment:
- DB_PASSWORD=secret123 # ❌ Bad
# Use env file (.gitignored):
env_file:
- .env # ✓ Good
3. Limit capabilities:
services:
web:
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
4. Read-only root filesystem:
services:
web:
read_only: true
tmpfs:
- /tmp
- /var/run
5. Use security options:
services:
web:
security_opt:
- no-new-privileges:true
- apparmor:docker-default
⚠️ Common Issues and Solutions
Issue 1: Port already in use
# Error: port is already allocated
# Solution: Find and stop conflicting service
sudo netstat -tulpn | grep 3001
sudo systemctl stop <service>
# Or use different port
ports:
- "3002:80"
Issue 2: Volume permission denied
# Error: Permission denied on /opt/devops
# Solution: Check ownership and permissions
ls -ld /opt/devops
sudo chmod 755 /opt/devops
sudo chown -R $(whoami) /opt/devops
Issue 3: Containers not on same network
# Services can't communicate
# Solution: Ensure same network
services:
web:
networks:
- app-network
db:
networks:
- app-network
networks:
app-network:
Issue 4: Environment variables not loading
# Solution: Check .env file location
# Must be in same directory as docker-compose.yml
# Or specify explicitly
env_file:
- /absolute/path/to/.env
Issue 5: docker-compose command not found
# If using Docker Compose V2
docker compose up -d # Note: space not hyphen
# Or install V1
sudo curl -L "https://github.com/docker/compose/releases/download/v2.24.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
🎉 Final Thoughts
You've successfully mastered Docker Compose! This is the foundation of modern application deployment:
What you accomplished:
✅ Created docker-compose.yml at exact path
✅ Deployed Apache httpd container
✅ Configured port mapping (3001:80)
✅ Set up volume mount (/opt/devops)
✅ Container named exactly "httpd"
✅ Website accessible and serving
Real-world impact:
Configuration as code: YAML defines entire stack
Version control: Track infrastructure changes
Team collaboration: Share compose files
Easy deployment: One command deploys everything
Environment management: Different configs per environment
Rapid iteration: Update and redeploy instantly
Key lessons learned:
docker-compose.yml defines services
Services can have any name (web, app, etc.)
container_name sets actual container name
Ports map host to container (HOST:CONTAINER)
Volumes mount directories (bind mounts)
docker-compose up deploys entire stack
docker-compose down tears down cleanly
Restart policies ensure availability
Best practices applied:
Exact file path (/opt/docker/docker-compose.yml)
Specific requirements met (container name: httpd)
Latest image tag (httpd:latest)
Proper port mapping (3001:80)
Correct volume mount (bind mount)
Restart policy (unless-stopped)
YAML validated before deployment
This is production infrastructure as code! Every modern application uses Docker Compose or similar orchestration! 💪
🚀 What's Next?
Day 44 complete! 🎉 You've mastered Docker Compose!
Skills Mastered Today:
✅ Docker Compose file creation
✅ Service definitions and configuration
✅ Port mapping in Compose
✅ Volume mounting with Compose
✅ Container orchestration
✅ Infrastructure as code
Coming up: More Docker adventures - Docker networking with Compose, multi-stage builds, Docker Swarm, production deployment patterns!
Day: 44/100
Challenge: KodeKloud Cloud DevOps
Date: December 19, 2025
Topic: Docker Compose - Multi-Container Orchestration
How do you use Docker Compose in your projects? What's your orchestration strategy? Share your Docker Compose patterns! 📦




