How-To Guide
Docker Production Deployment
Deploy to your production server with automatic HTTPS, performance optimization, and monitoring.
Meta-awareness
This is the service manual for the website you're reading right now. The site is deployed exactly as described here—Docker containers, caddy-docker-proxy, automatic HTTPS. You're looking at the result of following this guide.
Prerequisites
Before deploying to production, ensure:
- Linux server with Docker (Ubuntu 22.04+ recommended)
- Domain name pointing to server IP (A record)
- Ports 80/443 open in firewall
- caddy-docker-proxy running on server
- SSH access to server (key-based auth recommended)
Verify prerequisites on server:
# Check Docker version
docker --version
# Check caddy network exists
docker network ls | grep caddy
# Check firewall (UFW example)
sudo ufw status
# Expected:
# 80/tcp ALLOW
# 443/tcp ALLOW Step 1: Server setup
Connect to your server and prepare the environment:
# SSH into server
ssh user@your-server.com
# Create application directory
sudo mkdir -p /opt/installations
cd /opt/installations
# Clone repository
git clone https://github.com/your-org/your-installation.git
cd your-installation/website Tip: Use /opt/installations/ for consistency across deployments. Adjust permissions as needed for your deployment user.
Step 2: Production environment
Create .env.production with production-specific settings:
# Production mode
ATELIER_MODE=prod
NODE_ENV=production
# Disable development features
ENABLE_HMR=false
DEBUG=false
# Domain configuration
PUBLIC_DOMAIN=your-domain.com
COMPOSE_PROJECT_NAME=your-installation-prod
# Ambient effects
AMBIENT_EFFECTS_ENABLED=true
AMBIENT_INTENSITY=medium
# Performance
NODE_OPTIONS="--max-old-space-size=2048"
# Logging
LOG_LEVEL=warn
# Optional: Analytics, monitoring
# ANALYTICS_ID=your-analytics-id
# SENTRY_DSN=your-sentry-dsn Security considerations:
- • Never commit
.env.productionto git - • Set restrictive file permissions:
chmod 600 .env.production - • Use environment-specific secrets (never copy from dev)
- • Rotate credentials regularly
See also in Process
Environment Variables ReferenceComplete list of production configuration options
Step 3: Caddy labels configuration
Verify docker-compose.prod.yml has correct Caddy labels:
services:
web:
build:
context: .
dockerfile: Dockerfile
target: production
environment:
- NODE_ENV=production
networks:
- caddy
restart: unless-stopped
labels:
# Domain routing
caddy: ${PUBLIC_DOMAIN}
caddy.reverse_proxy: "{{upstreams 4321}}"
# HTTPS configuration
caddy.tls.dns: cloudflare # or your DNS provider
# Performance
caddy.encode: gzip
caddy.header: "X-Frame-Options DENY"
caddy.header.Strict-Transport-Security: "max-age=31536000"
# Logging
caddy.log.output: "file /var/log/caddy/access.log"
caddy.log.format: json
networks:
caddy:
external: true Note: This assumes caddy-docker-proxy is already running. If not, set it up first.
Step 4: Initial deployment
Build and start production containers:
# Build production image
docker compose -f docker-compose.yml -f docker-compose.prod.yml build --no-cache
# Start services
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
# Check status
docker compose ps
# Expected:
# NAME STATUS
# your-installation-web Up (healthy) Monitor initial startup:
# Follow logs during first start
docker compose logs -f web
# Watch for:
# - "astro build" completion
# - No error messages
# - Server listening on port 4321 Verify HTTPS certificate:
# Wait 30-60 seconds for certificate issuance
curl -I https://your-domain.com
# Expected:
# HTTP/2 200
# server: Caddy
# Check certificate details
openssl s_client -connect your-domain.com:443 -servername your-domain.com < /dev/null 2>/dev/null | openssl x509 -noout -dates First deployment: Allow 2-3 minutes for DNS propagation and SSL certificate issuance.
Step 5: Configure health checks
Ensure containers have proper health checks configured:
services:
web:
# ... other config
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:4321/"]
interval: 5m
timeout: 10s
retries: 3
start_period: 30s Check health status:
# View health status
docker compose ps
# Inspect detailed health
docker inspect --format='{{json .State.Health}}' your-installation-web | jq Health check best practices:
- • Interval: 5 minutes (balance monitoring vs overhead)
- • Timeout: 10 seconds (generous for slow responses)
- • Retries: 3 (prevents false positives)
- • Start period: 30 seconds (allows initial boot)
Performance optimization
Production-specific performance tuning:
Enable compression
Already configured via Caddy label: caddy.encode: gzip
Reduces bandwidth by ~70% for text assets. Verify:
curl -I -H "Accept-Encoding: gzip" https://your-domain.com
# Look for:
# content-encoding: gzip Browser caching
Add cache headers for static assets:
# In docker-compose.prod.yml
labels:
caddy.handle_path: "/assets/*"
caddy.handle_path.0_header: "Cache-Control "public, max-age=31536000, immutable""
caddy.handle_path.1: "/*"
caddy.handle_path.1_header: "Cache-Control "public, max-age=3600"" Resource limits
Prevent memory leaks from crashing the server:
services:
web:
deploy:
resources:
limits:
memory: 2G
cpus: '1.0'
reservations:
memory: 512M
cpus: '0.5' Deployment automation
Create a deployment script for consistent updates:
#!/bin/bash
# deploy-prod.sh - Production deployment script
set -e # Exit on error
echo "🚀 Starting production deployment..."
# Pull latest code
echo "📥 Pulling latest code..."
git pull origin main
# Backup current state
echo "💾 Creating backup..."
docker compose ps > deployment-backup-$(date +%Y%m%d-%H%M%S).txt
# Build new image
echo "🏗️ Building production image..."
docker compose -f docker-compose.yml -f docker-compose.prod.yml build --no-cache
# Stop old containers gracefully
echo "⏸️ Stopping old containers..."
docker compose -f docker-compose.yml -f docker-compose.prod.yml down
# Start new containers
echo "▶️ Starting new containers..."
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
# Wait for health check
echo "🏥 Waiting for health check..."
sleep 30
# Verify deployment
echo "✅ Verifying deployment..."
if docker compose ps | grep -q "Up (healthy)"; then
echo "✨ Deployment successful!"
curl -I https://$(grep PUBLIC_DOMAIN .env.production | cut -d '=' -f2)
else
echo "❌ Deployment failed - check logs"
docker compose logs --tail=50
exit 1
fi
# Cleanup old images
echo "🧹 Cleaning up old images..."
docker image prune -f
echo "🎉 Deployment complete!" Make executable and run:
chmod +x deploy-prod.sh
./deploy-prod.sh Monitoring and logging
Set up basic monitoring for production:
Container logs
# View recent logs
docker compose logs --tail=100
# Follow logs in real-time
docker compose logs -f
# Export logs to file
docker compose logs --since 24h > logs-$(date +%Y%m%d).txt Disk usage monitoring
# Check Docker disk usage
docker system df
# Clean up unused resources
docker system prune -a --volumes
# Schedule weekly cleanup (cron)
echo "0 2 * * 0 docker system prune -af > /dev/null 2>&1" | crontab - Uptime monitoring
Use external service (UptimeRobot, Pingdom, etc.) to monitor:
- • HTTPS availability (every 5 minutes)
- • Response time (< 2 seconds expected)
- • SSL certificate expiration
- • Status code 200 responses
Rollback procedure
If a deployment fails, roll back to previous version:
# Stop current containers
docker compose down
# Revert code to previous commit
git log --oneline -5 # Find previous commit hash
git checkout <previous-commit-hash>
# Rebuild and restart
docker compose -f docker-compose.yml -f docker-compose.prod.yml build
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
# Verify rollback
docker compose ps
curl -I https://your-domain.com Prevention strategies:
- • Test deployments on staging first
- • Use git tags for production releases
- • Keep previous Docker images (set image retention policy)
- • Automate rollback in deployment script
- • Monitor for 10 minutes after deployment
Production security checklist
- HTTPS enabled and certificate auto-renews
- Security headers set (HSTS, X-Frame-Options)
- Firewall configured (ports 80, 443, 22 only)
- SSH key-based authentication (password auth disabled)
- Environment secrets not committed to git
- Regular system updates (unattended-upgrades enabled)
- Docker daemon secured (non-root user access)
- Backups configured and tested
Next steps
Production deployment complete. Consider: