Docker Compose turns multi-container applications into single-command deployments. This guide shows you how to go from fresh VPS to production-ready Docker environment in 15 minutes.
Why Docker Compose for Production?
Benefits
- Consistency: Same environment in development and production
- Isolation: Each service runs in its own container
- Easy updates: Pull new image, restart container
- Version control: Infrastructure as code in docker-compose.yml
- Quick rollbacks: Keep old images for instant recovery
When to Use Docker Compose
- Multi-service applications (app + database + cache + queue)
- Microservices architecture
- Running multiple isolated applications on one VPS
- Development/production parity
Prerequisites
What You Need
- VPS with Ubuntu 22.04 or 24.04
- 2GB RAM minimum (4GB recommended)
- SSH access with sudo privileges
- Domain name pointed to VPS IP (for SSL)
Quick start: Deploy a 2-core, 4GB DanubeData VPS in Frankfurt for $8.99/month. Comes with 80GB NVMe storage.
Step 1: Install Docker (3 minutes)
SSH into your VPS and run:
# Update system
sudo apt update && sudo apt upgrade -y
# Install Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
# Add your user to docker group
sudo usermod -aG docker $USER
# Install Docker Compose V2
sudo apt install docker-compose-plugin
# Verify installation
docker --version
docker compose version
# Log out and back in for group changes to take effect
exit
Expected output:
Docker version 25.0.0
Docker Compose version v2.24.0
Step 2: Create Project Structure (2 minutes)
# Create project directory
mkdir -p ~/myapp && cd ~/myapp
# Create subdirectories
mkdir -p nginx/conf.d nginx/ssl data/mysql data/redis
Your structure:
~/myapp/
├── docker-compose.yml
├── .env
├── nginx/
│ ├── conf.d/
│ │ └── app.conf
│ └── ssl/
├── data/
│ ├── mysql/
│ └── redis/
└── app/ (your application code)
Step 3: Create docker-compose.yml (3 minutes)
Create docker-compose.yml:
version: '3.8'
services:
# Nginx Reverse Proxy
nginx:
image: nginx:alpine
container_name: nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d
- ./nginx/ssl:/etc/nginx/ssl
- ./app:/var/www/html
networks:
- app-network
depends_on:
- php
# PHP Application (Laravel/Symfony/etc)
php:
image: php:8.3-fpm-alpine
container_name: php-fpm
restart: unless-stopped
working_dir: /var/www/html
volumes:
- ./app:/var/www/html
environment:
- DB_HOST=mysql
- DB_DATABASE=${DB_DATABASE}
- DB_USERNAME=${DB_USERNAME}
- DB_PASSWORD=${DB_PASSWORD}
- REDIS_HOST=redis
networks:
- app-network
depends_on:
- mysql
- redis
# MySQL Database
mysql:
image: mysql:8.0
container_name: mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${DB_DATABASE}
MYSQL_USER: ${DB_USERNAME}
MYSQL_PASSWORD: ${DB_PASSWORD}
volumes:
- ./data/mysql:/var/lib/mysql
networks:
- app-network
command: --default-authentication-plugin=mysql_native_password
# Redis Cache
redis:
image: redis:7-alpine
container_name: redis
restart: unless-stopped
volumes:
- ./data/redis:/data
networks:
- app-network
command: redis-server --appendonly yes
networks:
app-network:
driver: bridge
volumes:
mysql-data:
redis-data:
Step 4: Configure Environment Variables (1 minute)
Create .env file:
# Database Configuration
DB_DATABASE=myapp
DB_USERNAME=myapp_user
DB_PASSWORD=change_this_secure_password
MYSQL_ROOT_PASSWORD=change_this_root_password
# Application
APP_ENV=production
APP_URL=https://myapp.com
Security tip: Generate strong passwords:
openssl rand -base64 32
Step 5: Configure Nginx (3 minutes)
Create nginx/conf.d/app.conf:
server {
listen 80;
server_name myapp.com www.myapp.com;
# Redirect HTTP to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name myapp.com www.myapp.com;
root /var/www/html/public;
index index.php index.html;
# SSL Configuration (we'll add certificates next)
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Gzip compression
gzip on;
gzip_vary on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ .php$ {
fastcgi_pass php:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /.(?!well-known).* {
deny all;
}
# Cache static assets
location ~* .(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
Step 6: Setup SSL with Let's Encrypt (2 minutes)
# Install Certbot
sudo apt install certbot
# Stop containers temporarily
docker compose down
# Get SSL certificate
sudo certbot certonly --standalone -d myapp.com -d www.myapp.com
# Copy certificates to project
sudo cp /etc/letsencrypt/live/myapp.com/fullchain.pem ~/myapp/nginx/ssl/
sudo cp /etc/letsencrypt/live/myapp.com/privkey.pem ~/myapp/nginx/ssl/
# Fix permissions
sudo chown $USER:$USER ~/myapp/nginx/ssl/*
Auto-Renewal Setup
# Create renewal hook
sudo nano /etc/letsencrypt/renewal-hooks/deploy/01-docker-reload.sh
Add:
#!/bin/bash
cp /etc/letsencrypt/live/myapp.com/fullchain.pem /home/ubuntu/myapp/nginx/ssl/
cp /etc/letsencrypt/live/myapp.com/privkey.pem /home/ubuntu/myapp/nginx/ssl/
docker exec nginx nginx -s reload
# Make executable
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/01-docker-reload.sh
# Test renewal (dry run)
sudo certbot renew --dry-run
Step 7: Launch Your Stack (1 minute)
# Start all services
docker compose up -d
# Verify everything is running
docker compose ps
Expected output:
NAME SERVICE STATUS PORTS
nginx nginx running 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp
php-fpm php running 9000/tcp
mysql mysql running 3306/tcp
redis redis running 6379/tcp
Your application is now live! Visit https://myapp.com
Essential Docker Compose Commands
Daily Operations
# View logs
docker compose logs -f
# View logs for specific service
docker compose logs -f php
# Restart all services
docker compose restart
# Restart specific service
docker compose restart nginx
# Stop all services
docker compose down
# Stop and remove volumes (WARNING: deletes data)
docker compose down -v
# View resource usage
docker stats
Updating Your Application
# Pull latest code
cd ~/myapp/app
git pull origin main
# Rebuild and restart (if needed)
docker compose up -d --build
# Or just restart PHP-FPM
docker compose restart php
Database Management
# Access MySQL shell
docker compose exec mysql mysql -u root -p
# Run MySQL commands from host
docker compose exec mysql mysql -u${DB_USERNAME} -p${DB_PASSWORD} ${DB_DATABASE} -e "SHOW TABLES;"
# Backup database
docker compose exec mysql mysqldump -u${DB_USERNAME} -p${DB_PASSWORD} ${DB_DATABASE} > backup.sql
# Restore database
docker compose exec -T mysql mysql -u${DB_USERNAME} -p${DB_PASSWORD} ${DB_DATABASE} < backup.sql
Debugging
# Execute command in container
docker compose exec php php -v
# Access container shell
docker compose exec php sh
# View container details
docker inspect php-fpm
# Check network connectivity
docker compose exec php ping mysql
Production Best Practices
1. Resource Limits
Prevent containers from consuming all resources:
services:
php:
image: php:8.3-fpm-alpine
deploy:
resources:
limits:
cpus: '1.0'
memory: 1G
reservations:
cpus: '0.5'
memory: 512M
2. Health Checks
services:
mysql:
image: mysql:8.0
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 3
start_period: 30s
3. Logging Configuration
services:
nginx:
image: nginx:alpine
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
4. Security Hardening
services:
mysql:
image: mysql:8.0
security_opt:
- no-new-privileges:true
read_only: true
tmpfs:
- /tmp
- /var/run/mysqld
Real-World Example: Laravel Application
Complete docker-compose.yml for Laravel:
version: '3.8'
services:
nginx:
image: nginx:alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d
- ./nginx/ssl:/etc/nginx/ssl
- ./laravel:/var/www/html
networks:
- laravel
depends_on:
- php
php:
build:
context: ./docker/php
dockerfile: Dockerfile
restart: unless-stopped
volumes:
- ./laravel:/var/www/html
environment:
DB_HOST: mysql
REDIS_HOST: redis
networks:
- laravel
mysql:
image: mysql:8.0
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: laravel
MYSQL_USER: laravel
MYSQL_PASSWORD: ${DB_PASSWORD}
volumes:
- mysql-data:/var/lib/mysql
networks:
- laravel
redis:
image: redis:7-alpine
restart: unless-stopped
volumes:
- redis-data:/data
networks:
- laravel
queue:
build:
context: ./docker/php
dockerfile: Dockerfile
restart: unless-stopped
command: php artisan queue:work --sleep=3 --tries=3
volumes:
- ./laravel:/var/www/html
networks:
- laravel
depends_on:
- mysql
- redis
scheduler:
build:
context: ./docker/php
dockerfile: Dockerfile
restart: unless-stopped
command: /bin/sh -c "while true; do php artisan schedule:run --verbose --no-interaction & sleep 60; done"
volumes:
- ./laravel:/var/www/html
networks:
- laravel
networks:
laravel:
driver: bridge
volumes:
mysql-data:
redis-data:
Monitoring Your Docker Stack
Container Statistics
# Real-time stats
docker stats
# JSON output for parsing
docker stats --no-stream --format "{{json .}}"
Setup Automated Monitoring
Add Prometheus + Grafana to docker-compose.yml:
prometheus:
image: prom/prometheus
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus
ports:
- "9090:9090"
grafana:
image: grafana/grafana
volumes:
- grafana-data:/var/lib/grafana
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
Backup Strategy
Automated Daily Backups
Create backup.sh:
#!/bin/bash
BACKUP_DIR="/home/ubuntu/backups"
DATE=$(date +%Y%m%d_%H%M%S)
# Backup MySQL
docker compose exec -T mysql mysqldump -uroot -p${MYSQL_ROOT_PASSWORD} --all-databases > $BACKUP_DIR/mysql_$DATE.sql
# Backup Redis
docker compose exec redis redis-cli BGSAVE
cp ~/myapp/data/redis/dump.rdb $BACKUP_DIR/redis_$DATE.rdb
# Backup application files
tar -czf $BACKUP_DIR/app_$DATE.tar.gz ~/myapp/app
# Delete backups older than 7 days
find $BACKUP_DIR -name "*.sql" -mtime +7 -delete
find $BACKUP_DIR -name "*.rdb" -mtime +7 -delete
find $BACKUP_DIR -name "*.tar.gz" -mtime +7 -delete
echo "Backup completed: $DATE"
Schedule with cron:
crontab -e
# Add daily backup at 2 AM
0 2 * * * /home/ubuntu/myapp/backup.sh
Troubleshooting Common Issues
| Issue | Cause | Solution |
|---|---|---|
| Container keeps restarting | Application crash | docker compose logs [service] |
| Port already in use | Another service on port 80/443 | sudo lsof -i :80 and stop conflicting service |
| Permission denied | Volume ownership mismatch | sudo chown -R $USER:$USER ./data |
| Can't connect to MySQL | Network isolation | Use service name (mysql) not localhost |
| Out of disk space | Old images/containers | docker system prune -a |
Performance Optimization
1. Use Multi-Stage Builds
# Dockerfile for PHP
FROM composer:latest AS composer
WORKDIR /app
COPY composer.* ./
RUN composer install --no-dev --optimize-autoloader
FROM php:8.3-fpm-alpine
WORKDIR /var/www/html
COPY --from=composer /app/vendor ./vendor
COPY . .
2. Enable OPcache
# php.ini
opcache.enable=1
opcache.memory_consumption=256
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0
3. Use Alpine Images
Alpine images are 5-10x smaller:
nginx:alpine- 40MB vs 180MBphp:8.3-fpm-alpine- 80MB vs 450MBredis:alpine- 30MB vs 120MB
Why DanubeData for Docker Hosting?
| Specification | DanubeData VPS | DigitalOcean | Linode |
|---|---|---|---|
| 2 vCPU, 4GB RAM | $8.99/mo | $24/mo | $24/mo |
| Storage | 80GB NVMe | 80GB SSD | 80GB SSD |
| Transfer | 20TB | 4TB | 4TB |
| Backup Storage | Included | $8/mo extra | $5/mo extra |
| Setup Time | < 60 seconds | ~2 minutes | ~2 minutes |
| Location | Germany (GDPR) | Global | Global |
Get Started Now
- Create your DanubeData account
- Deploy a 2-core, 4GB VPS (Ubuntu 24.04)
- Follow this guide to install Docker Compose
- Launch your containerized application
- Add managed database and Redis for optimal performance
Perfect for: Web applications, APIs, microservices, WordPress, Laravel, Node.js, Python, and any multi-container setup.
Questions about Docker hosting? Contact our team or check our documentation for more guides.