Skip to main content

Command Palette

Search for a command to run...

Day 46: Multi-Service Docker Compose - PHP & MariaDB Stack πŸ—οΈ

Published
β€’7 min read
Day 46: Multi-Service Docker Compose - PHP & MariaDB Stack πŸ—οΈ

Welcome back! πŸ‘‹ Day 46 of the 100 Days Cloud DevOps Challenge, and today we're deploying a complete multi-service application stack with Docker Compose! This is real-world full-stack deployment - web server with database backend. Let's orchestrate! 🎯

🎯 The Mission - Deploy PHP/MariaDB Application Stack

πŸ“‹ TASK TICKET #DEV-8046 - Multi-Service Stack Deployment
Priority: HIGH | Type: Docker Compose Multi-Container
Server: App Server 2 (stapp02) | User: steve | Password: Am3ric@

REQUIREMENTS:
1. Compose File: /opt/security/docker-compose.yml (exact path)
2. Two Services: web (PHP) + db (MariaDB)

WEB SERVICE:
- Container: php_web
- Image: php:apache
- Port: 5004:80
- Volume: /var/www/html:/var/www/html

DB SERVICE:
- Container: mysql_web
- Image: mariadb:latest
- Port: 3306:3306
- Volume: /var/lib/mysql:/var/lib/mysql
- Environment:
  * MYSQL_DATABASE=database_web
  * MYSQL_USER=custom_user (not root)
  * MYSQL_PASSWORD=complex_password
  * MYSQL_ROOT_PASSWORD=root_password

VERIFICATION: curl <server-ip>:5004/

This is production-grade infrastructure as code! πŸš€

πŸ€” Why Multi-Service Stacks Matter

Single container limitations:

  • ❌ Web server only (no database)

  • ❌ No data persistence layer

  • ❌ Limited real-world use

Multi-service solution:

  • βœ… Complete application stack

  • βœ… Service dependencies managed

  • βœ… Automatic networking

  • βœ… Data persistence

  • βœ… One-command deployment

Architecture:

Client β†’ http://server:5004
         ↓
    php_web (PHP + Apache)
         ↓ MySQL protocol
    mysql_web (MariaDB)
         ↓
    Persistent storage

πŸ› οΈ Complete Implementation

Step 1: Access Server and Setup

# SSH to App Server 2
ssh steve@stapp02
# Password: Am3ric@

# Switch to root
sudo su -
# Password: Am3ric@

# Create directory
mkdir -p /opt/security
cd /opt/security

Step 2: Create Docker Compose File

cat > docker-compose.yml << 'EOF'
version: '3.8'

services:
  web:
    image: php:apache
    container_name: php_web
    ports:
      - "5004:80"
    volumes:
      - /var/www/html:/var/www/html
    depends_on:
      - db
    restart: unless-stopped

  db:
    image: mariadb:latest
    container_name: mysql_web
    ports:
      - "3306:3306"
    volumes:
      - /var/lib/mysql:/var/lib/mysql
    environment:
      MYSQL_DATABASE: database_web
      MYSQL_USER: app_user
      MYSQL_PASSWORD: SecureP@ss123!
      MYSQL_ROOT_PASSWORD: R00tPass!
    restart: unless-stopped
EOF

Configuration explained:

version: '3.8'  # Compose file format

services:
  web:  # Service name (arbitrary)
    image: php:apache  # PHP with Apache built-in
    container_name: php_web  # Exact container name required
    ports:
      - "5004:80"  # Host:Container port mapping
    volumes:
      - /var/www/html:/var/www/html  # Bind mount for web content
    depends_on:
      - db  # Start db before web
    restart: unless-stopped  # Auto-restart policy

  db:  # Database service
    image: mariadb:latest  # MariaDB (MySQL-compatible)
    container_name: mysql_web  # Exact container name required
    ports:
      - "3306:3306"  # MySQL standard port
    volumes:
      - /var/lib/mysql:/var/lib/mysql  # Persistent database storage
    environment:
      MYSQL_DATABASE: database_web  # Create this database
      MYSQL_USER: app_user  # Custom user (not root)
      MYSQL_PASSWORD: SecureP@ss123!  # Complex password
      MYSQL_ROOT_PASSWORD: R00tPass!  # Root password required
    restart: unless-stopped

Key concepts:

  • depends_on: Ensures db starts before web

  • Service names: Enable DNS resolution (web can use 'db' as hostname)

  • Volumes: Persist data across container restarts

  • restart policy: Containers survive reboots

Step 3: Verify Configuration

# Check file contents
cat docker-compose.yml

# Validate YAML syntax
docker compose config

Step 4: Deploy the Stack

# Start all services
docker compose up -d

Expected output:

[+] Running 24/24
 βœ” db Pulled                                                            30.2s
 βœ” web Pulled                                                           77.1s
[+] Running 3/3
 βœ” Network security_default  Created                                    0.1s
 βœ” Container mysql_web       Started                                   15.5s
 βœ” Container php_web         Started                                   10.0s

What happened:

  1. Docker pulled images (php:apache and mariadb:latest)

  2. Created network: security_default (automatic)

  3. Started mysql_web (db service first)

  4. Started php_web (web service after db)

Step 5: Verification

# Check running containers
docker ps

Expected output:

CONTAINER ID   IMAGE            COMMAND                  STATUS          PORTS                    NAMES
551fc20b2bc7   php:apache       "docker-php-entrypoi…"   Up 24 seconds   0.0.0.0:5004->80/tcp     php_web
c00c2335fc43   mariadb:latest   "docker-entrypoint.s…"   Up 24 seconds   0.0.0.0:3306->3306/tcp   mysql_web

Verify services:

# Check compose services
docker compose ps

# View logs
docker compose logs

# Test web server
curl -I http://localhost:5004

# Test database
docker exec mysql_web mysql -u app_user -pSecureP@ss123! -e "SHOW DATABASES;"

Expected database output:

+--------------------+
| Database           |
+--------------------+
| database_web       |
| information_schema |
+--------------------+

Step 6: Test Application Access

# Create test PHP file
mkdir -p /var/www/html
cat > /var/www/html/index.php << 'EOF'
<?php
phpinfo();
?>
EOF

# Test from localhost
curl http://localhost:5004/index.php | head -20

# Test from server IP
SERVER_IP=$(hostname -I | awk '{print $1}')
curl http://$SERVER_IP:5004/

Complete verification:

echo "=== Stack Verification ==="
echo "1. Compose file: $(ls -lh /opt/security/docker-compose.yml)"
echo "2. Services running:"
docker compose ps --format "   {{.Name}}: {{.Status}}"
echo "3. Web server: $(curl -s -o /dev/null -w "%{http_code}" http://localhost:5004)"
echo "4. Database: $(docker exec mysql_web mysql -u app_user -pSecureP@ss123! -e "SELECT 1;" &>/dev/null && echo "Accessible")"
echo "βœ“ STACK DEPLOYED SUCCESSFULLY"

πŸ” Understanding the Stack

Service Communication

How services find each other:

PHP code:
$host = 'db';  // Service name from compose!
$db = new PDO("mysql:host=$host");

Docker magic:
- 'db' resolves to mysql_web container IP
- Automatic DNS within security_default network
- No hard-coded IPs needed

Startup Order

Why depends_on matters:

Without depends_on:
- Both start simultaneously
- Web might start before DB
- Connection errors

With depends_on:
1. mysql_web starts (DB initializes)
2. php_web starts (can connect immediately)
3. No race conditions

Data Persistence

Volume mounts:

Host: /var/lib/mysql
  ↓ Bind mount
Container: /var/lib/mysql
  ↓ MariaDB writes here
Data persists:
  βœ“ Container restart: Data safe
  βœ“ Container recreate: Data safe
  βœ“ Host reboot: Data safe

πŸ’‘ Key Takeaways

✨ docker-compose.yml defines complete stacks
✨ depends_on controls startup order
✨ Service names enable DNS resolution
✨ Volumes persist data across restarts
✨ Environment variables configure services
✨ One command deploys entire stack
✨ Automatic networking between services
✨ Port mapping exposes services externally

πŸŽ“ Quick Interview Questions

Q: What's the difference between depends_on and healthchecks?

A: depends_on only controls startup ORDER (db before web), doesn't wait for "ready". With healthcheck, web waits until db is actually healthy and accepting connections.

Q: How do services discover each other in Compose?

A: Automatic DNS - each service gets hostname = service name. Web container can connect to 'db' hostname, which resolves to mysql_web container IP.

Q: What happens if a container fails?

A: With restart: unless-stopped, Docker automatically restarts failed containers. For production, add healthchecks to ensure service is truly ready.

Q: Can you scale services with fixed container names?

A: No - container_name prevents scaling. Must remove it to run multiple instances: docker compose up -d --scale web=3

Q: How do you handle secrets in production?

A: Don't put passwords in compose files! Use: .env files (gitignored), Docker secrets (Swarm), or external secret managers (Vault, AWS Secrets).

🌟 Docker Compose Commands

Stack management:

docker compose up -d              # Start stack
docker compose down               # Stop and remove
docker compose ps                 # View services
docker compose logs -f            # Follow logs
docker compose restart            # Restart all
docker compose stop               # Stop (keep containers)

Service operations:

docker compose exec web bash     # Shell into web
docker compose logs db           # DB logs only
docker compose restart web       # Restart web only
docker compose pull              # Update images

πŸš€ Real-World Example - WordPress Stack

version: '3.8'

services:
  wordpress:
    image: wordpress:latest
    container_name: wp_web
    ports:
      - "8080:80"
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: wpuser
      WORDPRESS_DB_PASSWORD: wppass
      WORDPRESS_DB_NAME: wordpress
    volumes:
      - ./wp-content:/var/www/html/wp-content
    depends_on:
      - db

  db:
    image: mysql:8.0
    container_name: wp_db
    environment:
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wpuser
      MYSQL_PASSWORD: wppass
      MYSQL_ROOT_PASSWORD: rootpass
    volumes:
      - db_data:/var/lib/mysql

volumes:
  db_data:

🎯 Production Best Practices

1. Use specific versions:

image: php:8.2-apache  # Not php:latest
image: mariadb:10.11   # Not mariadb:latest

2. Add health checks:

db:
  healthcheck:
    test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
    interval: 10s
    timeout: 5s
    retries: 5

3. Resource limits:

deploy:
  resources:
    limits:
      cpus: '1.0'
      memory: 512M

4. Use named volumes for production:

volumes:
  db_data:
    driver: local

5. Separate networks:

networks:
  frontend:  # For web
  backend:   # For db

πŸ”§ Troubleshooting

Services can't communicate:

# Check network
docker network inspect security_default

# Test connectivity
docker compose exec web ping db

Database connection fails:

# Check DB logs
docker compose logs db

# Verify environment variables
docker inspect mysql_web --format='{{.Config.Env}}'

Port already in use:

# Find what's using port
sudo netstat -tulpn | grep 5004

# Change compose port
ports:
  - "5005:80"

πŸŽ‰ Final Thoughts

You've mastered multi-service Docker Compose! This is how production applications are deployed.

What you built: βœ… Complete PHP + MariaDB stack
βœ… Service dependency management
βœ… Persistent data storage
βœ… Automatic service discovery
βœ… One-command deployment

Real-world impact:

  • Infrastructure as code (version controlled)

  • Reproducible environments (dev = prod)

  • Team collaboration (shared config)

  • Rapid deployment (docker compose up)

  • Easy updates (docker compose pull)

This is modern DevOps! πŸ’ͺ


Day: 46/100
Challenge: KodeKloud Cloud DevOps
Date: December 22, 2025
Topic: Multi-Service Docker Compose Stack

Share your Docker Compose patterns and multi-service architectures! πŸ—οΈ

More from this blog

πŸš€ DevOps Challenge- KodeKloud Solutions

73 posts