Skip to main content

Command Palette

Search for a command to run...

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

Published
22 min read
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:

  1. Compose created default network

  2. Pulled httpd:latest (if not local)

  3. Created container named "httpd"

  4. Mapped port 3001 to 80

  5. Mounted /opt/devops volume

  6. 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! 📦

More from this blog

🚀 DevOps Challenge- KodeKloud Solutions

73 posts