Docker Compose
Docker Compose Example
Deploy PHP applications with Docker Compose using Cbox Init for process management and environment-specific configuration.
Use Cases
- ✅ Local development with full stack
- ✅ Multi-environment deployments (dev/staging/prod)
- ✅ Microservices orchestration
- ✅ Testing with isolated environments
- ✅ CI/CD pipeline integration
Basic Docker Compose Setup
docker-compose.yml
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "80:80"
- "9090:9090" # Prometheus metrics
environment:
# PHP-FPM auto-tuning
PHP_FPM_AUTOTUNE_PROFILE: "medium"
# Application environment
APP_ENV: production
APP_KEY: ${APP_KEY}
DB_HOST: database
DB_DATABASE: app
DB_USERNAME: app
DB_PASSWORD: ${DB_PASSWORD}
REDIS_HOST: redis
# Cbox Init settings
CBOX_INIT_GLOBAL_METRICS_ENABLED: "true"
CBOX_INIT_GLOBAL_LOG_LEVEL: "info"
CBOX_INIT_PROCESS_QUEUE_DEFAULT_SCALE: "3"
deploy:
resources:
limits:
memory: 2G
cpus: '2'
depends_on:
database:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/health"]
interval: 10s
timeout: 5s
retries: 3
start_period: 30s
database:
image: mysql:8.0
environment:
MYSQL_DATABASE: app
MYSQL_USER: app
MYSQL_PASSWORD: ${DB_PASSWORD}
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
ports:
- "3306:3306"
volumes:
- db-data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
timeout: 3s
retries: 5
redis:
image: redis:alpine
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
volumes:
- redis-data:/data
volumes:
db-data:
redis-data:
Dockerfile
FROM php:8.3-fpm-alpine
# Install system dependencies
RUN apk add --no-cache \
nginx \
curl \
mysql-client \
redis
# Install PHP extensions
RUN docker-php-ext-install \
pdo_mysql \
opcache \
pcntl \
bcmath
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Copy application
WORKDIR /var/www/html
COPY . .
# Install dependencies
RUN composer install --no-dev --optimize-autoloader
# Copy Cbox Init
COPY --from=ghcr.io/cboxdk/init:latest /cbox-init /usr/local/bin/cbox-init
# Copy configurations
COPY docker/cbox-init.yaml /etc/cbox-init/cbox-init.yaml
COPY docker/nginx.conf /etc/nginx/nginx.conf
# Set permissions
RUN chown -R www-data:www-data /var/www/html
# Expose ports
EXPOSE 80 9090
# Run Cbox Init as PID 1
ENTRYPOINT ["/usr/local/bin/cbox-init"]
Multi-Environment Setup
Development Environment
docker-compose.dev.yml:
version: '3.8'
services:
app:
build:
target: development
environment:
APP_ENV: local
APP_DEBUG: "true"
PHP_FPM_AUTOTUNE_PROFILE: "dev"
CBOX_INIT_GLOBAL_LOG_LEVEL: "debug"
CBOX_INIT_GLOBAL_LOG_FORMAT: "text" # Human-readable
CBOX_INIT_PROCESS_HORIZON_ENABLED: "false"
CBOX_INIT_PROCESS_QUEUE_DEFAULT_SCALE: "1"
volumes:
- ./:/var/www/html # Mount source code for development
deploy:
resources:
limits:
memory: 512M
cpus: '1'
Run development:
docker-compose -f docker-compose.yml -f docker-compose.dev.yml up
Staging Environment
docker-compose.staging.yml:
version: '3.8'
services:
app:
environment:
APP_ENV: staging
APP_DEBUG: "false"
PHP_FPM_AUTOTUNE_PROFILE: "medium"
CBOX_INIT_GLOBAL_LOG_LEVEL: "info"
CBOX_INIT_GLOBAL_METRICS_ENABLED: "true"
CBOX_INIT_PROCESS_QUEUE_DEFAULT_SCALE: "3"
deploy:
resources:
limits:
memory: 2G
cpus: '2'
Production Environment
docker-compose.prod.yml:
version: '3.8'
services:
app:
environment:
APP_ENV: production
APP_DEBUG: "false"
PHP_FPM_AUTOTUNE_PROFILE: "heavy"
CBOX_INIT_GLOBAL_LOG_LEVEL: "warn"
CBOX_INIT_GLOBAL_LOG_REDACTION_ENABLED: "true"
CBOX_INIT_GLOBAL_METRICS_ENABLED: "true"
CBOX_INIT_GLOBAL_API_ENABLED: "true"
CBOX_INIT_GLOBAL_API_AUTH: ${API_TOKEN}
CBOX_INIT_PROCESS_QUEUE_DEFAULT_SCALE: "10"
deploy:
resources:
limits:
memory: 8G
cpus: '8'
replicas: 3 # Docker Swarm mode
Run production:
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
Service Dependencies
Wait for Database
services:
app:
depends_on:
database:
condition: service_healthy # Wait for MySQL health check
database:
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
retries: 10
Alternative: Custom Wait Script
services:
app:
command: ["/wait-for-db.sh", "&&", "/usr/local/bin/cbox-init"]
wait-for-db.sh:
#!/bin/bash
until php artisan db:ping; do
echo "Waiting for database..."
sleep 2
done
echo "Database is ready!"
Network Configuration
Internal Network
version: '3.8'
services:
app:
networks:
- backend
- frontend
database:
networks:
- backend # Only accessible to backend services
nginx-lb:
networks:
- frontend # Only accessible from frontend
networks:
backend:
internal: true # No external access
frontend:
driver: bridge
Service Discovery
services:
app:
environment:
# Services accessible by service name
DB_HOST: database # Resolves to database container
REDIS_HOST: redis # Resolves to redis container
MAIL_HOST: mailhog # Resolves to mailhog container
Volume Management
Named Volumes
volumes:
db-data:
driver: local
redis-data:
driver: local
app-storage:
driver: local
driver_opts:
type: nfs
o: addr=nfs-server,rw
device: ":/path/to/storage"
Bind Mounts (Development)
services:
app:
volumes:
- ./:/var/www/html # Source code
- ./docker/nginx.conf:/etc/nginx/nginx.conf
- ./docker/cbox-init.yaml:/etc/cbox-init/cbox-init.yaml
Scaling with Docker Compose
Manual Scaling
# Scale queue workers to 5 instances
docker-compose up -d --scale queue-worker=5
# Scale down to 2
docker-compose up -d --scale queue-worker=2
Swarm Mode Scaling
version: '3.8'
services:
app:
deploy:
replicas: 3
update_config:
parallelism: 1
delay: 10s
rollback_config:
parallelism: 1
resources:
limits:
memory: 2G
cpus: '2'
# Deploy to swarm
docker stack deploy -c docker-compose.yml myapp
# Scale service
docker service scale myapp_app=5
Monitoring Stack
version: '3.8'
services:
app:
# ... Laravel app with Cbox Init
prometheus:
image: prom/prometheus:latest
ports:
- "9091:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
environment:
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD}
volumes:
- grafana-data:/var/lib/grafana
volumes:
prometheus-data:
grafana-data:
prometheus.yml:
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'cbox-init'
static_configs:
- targets: ['app:9090']
Complete Multi-Service Example
version: '3.8'
services:
# PHP application with Cbox Init
app:
build: .
ports:
- "80:80"
- "9090:9090"
environment:
PHP_FPM_AUTOTUNE_PROFILE: "medium"
APP_ENV: production
APP_KEY: ${APP_KEY}
DB_HOST: database
REDIS_HOST: redis
QUEUE_CONNECTION: redis
depends_on:
database:
condition: service_healthy
redis:
condition: service_healthy
volumes:
- app-storage:/var/www/html/storage
networks:
- app-network
# MySQL database
database:
image: mysql:8.0
environment:
MYSQL_DATABASE: app
MYSQL_USER: app
MYSQL_PASSWORD: ${DB_PASSWORD}
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
ports:
- "3306:3306"
volumes:
- db-data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
retries: 10
networks:
- app-network
# Redis cache
redis:
image: redis:alpine
ports:
- "6379:6379"
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
retries: 5
networks:
- app-network
# Mailhog (email testing)
mailhog:
image: mailhog/mailhog:latest
ports:
- "1025:1025" # SMTP
- "8025:8025" # Web UI
networks:
- app-network
# Prometheus monitoring
prometheus:
image: prom/prometheus:latest
ports:
- "9091:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus
networks:
- app-network
# Grafana dashboards
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
environment:
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD}
volumes:
- grafana-data:/var/lib/grafana
networks:
- app-network
volumes:
db-data:
redis-data:
app-storage:
prometheus-data:
grafana-data:
networks:
app-network:
driver: bridge
Running the Stack
Development
# Start all services
docker-compose up -d
# View logs
docker-compose logs -f app
# Run Artisan commands
docker-compose exec app php artisan migrate
# Access services
# - App: http://localhost
# - Mailhog: http://localhost:8025
# - Prometheus: http://localhost:9091
# - Grafana: http://localhost:3000
Production
# Use production override
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
# Check health
docker-compose ps
# View metrics
curl http://localhost:9090/metrics
Environment Files
.env
# Application
APP_KEY=base64:your-key-here
APP_ENV=production
APP_DEBUG=false
# Database
DB_PASSWORD=secure-password
DB_ROOT_PASSWORD=root-password
# Monitoring
GRAFANA_PASSWORD=admin-password
# Heartbeats
BACKUP_HEARTBEAT_URL=https://hc-ping.com/uuid
.env.development
APP_ENV=local
APP_DEBUG=true
PHP_FPM_AUTOTUNE_PROFILE=dev
CBOX_INIT_GLOBAL_LOG_LEVEL=debug
CBOX_INIT_PROCESS_QUEUE_DEFAULT_SCALE=1
.env.production
APP_ENV=production
APP_DEBUG=false
PHP_FPM_AUTOTUNE_PROFILE=heavy
CBOX_INIT_GLOBAL_LOG_LEVEL=warn
CBOX_INIT_GLOBAL_LOG_REDACTION_ENABLED=true
CBOX_INIT_PROCESS_QUEUE_DEFAULT_SCALE=10
Multi-Profile Deployment
version: '3.8'
services:
# Development profile
app-dev:
build: .
environment:
PHP_FPM_AUTOTUNE_PROFILE: "dev"
deploy:
resources:
limits:
memory: 512M
cpus: '1'
# Medium traffic profile
app-medium:
build: .
environment:
PHP_FPM_AUTOTUNE_PROFILE: "medium"
deploy:
resources:
limits:
memory: 2G
cpus: '2'
# Heavy traffic profile
app-heavy:
build: .
environment:
PHP_FPM_AUTOTUNE_PROFILE: "heavy"
deploy:
resources:
limits:
memory: 8G
cpus: '8'
Service Orchestration
Startup Order
services:
# 1. Database starts first
database:
image: mysql:8.0
healthcheck:
test: ["CMD", "mysqladmin", "ping"]
# 2. Redis starts in parallel with database
redis:
image: redis:alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
# 3. App waits for both
app:
depends_on:
database:
condition: service_healthy
redis:
condition: service_healthy
Startup flow:
database + redis (parallel)
↓
Both healthy
↓
app starts
↓
Cbox Init runs pre-start hooks
↓
PHP-FPM → Nginx → Horizon → Queue
Graceful Shutdown
# Stop services gracefully
docker-compose down
# Cbox Init handles:
# 1. Stop accepting new requests
# 2. Finish current jobs (Horizon)
# 3. Terminate queue workers
# 4. Stop Nginx
# 5. Stop PHP-FPM
Volume Strategies
Development
services:
app:
volumes:
# Hot reload for development
- ./:/var/www/html
- ./docker/cbox-init.yaml:/etc/cbox-init/cbox-init.yaml
# Prevent node_modules from mounting
- /var/www/html/node_modules
Production
services:
app:
volumes:
# Only mount storage directory
- app-storage:/var/www/html/storage
# Read-only config
- ./cbox-init.yaml:/etc/cbox-init/cbox-init.yaml:ro
Monitoring Integration
Prometheus + Grafana
prometheus.yml:
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'cbox-init'
static_configs:
- targets:
- app:9090
metrics_path: /metrics
Access dashboards:
# Prometheus
http://localhost:9091
# Grafana
http://localhost:3000
# Login: admin / ${GRAFANA_PASSWORD}
Log Aggregation
services:
app:
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# Alternative: Loki driver
app:
logging:
driver: "loki"
options:
loki-url: "http://loki:3100/loki/api/v1/push"
CI/CD Integration
GitHub Actions
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build image
run: docker-compose build
- name: Run tests
run: docker-compose run --rm app php artisan test
- name: Deploy
run: |
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
- name: Health check
run: |
sleep 30
curl -f http://localhost/health
GitLab CI
deploy:
stage: deploy
script:
- docker-compose build
- docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
- sleep 30
- curl -f http://localhost/health
only:
- main
Troubleshooting
Services Not Starting in Order
Problem: App starts before database is ready
Solution: Use health check conditions
depends_on:
database:
condition: service_healthy # Don't just use 'depends_on'
Port Conflicts
Problem: Port already in use
Solution: Change port mapping
services:
app:
ports:
- "8080:80" # Map to 8080 instead of 80
Memory Limits Not Working
Problem: Docker Compose doesn't enforce limits
Solution: Enable resource constraints
# Linux: Enable cgroup v2
docker-compose --compatibility up
# Or use Docker Swarm mode
docker stack deploy -c docker-compose.yml myapp
Container OOM Killed
Problem: Container exceeds memory limit
Solution:
services:
app:
environment:
PHP_FPM_AUTOTUNE_PROFILE: "light" # Reduce workers
deploy:
resources:
limits:
memory: 4G # Increase limit
Best Practices
✅ Do
Use health checks:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/health"]
interval: 10s
retries: 3
Set resource limits:
deploy:
resources:
limits:
memory: 2G
cpus: '2'
Use named volumes:
volumes:
- db-data:/var/lib/mysql # Persists data
Separate environments:
docker-compose -f docker-compose.yml -f docker-compose.dev.yml up
❌ Don't
Don't run as root:
# Add user in Dockerfile
RUN addgroup -g 1000 www && adduser -u 1000 -G www -s /bin/sh -D www
USER www
Don't bind mount in production:
# ❌ Development only
volumes:
- ./:/var/www/html
# ✅ Production
volumes:
- app-storage:/var/www/html/storage
Don't expose unnecessary ports:
# ❌ Exposes database publicly
database:
ports:
- "3306:3306"
# ✅ Only internal access
database:
expose:
- "3306" # Only accessible to other services
See Also
- Docker Integration - Dockerfile patterns
- Kubernetes Deployment - Kubernetes alternative
- Laravel with Monitoring - Full observability stack
- Environment Variables - ENV configuration