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:
Docker pulled images (php:apache and mariadb:latest)
Created network: security_default (automatic)
Started mysql_web (db service first)
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! ποΈ




