Docker Compose Deployment
FORGE generates a production-ready Docker Compose configuration with Caddy as a reverse proxy, automatic HTTPS via Let's Encrypt, and all application services containerized. This is the recommended deployment method for single-server setups and small-to-medium applications.
Generate Production Configuration
forge deploy:compose --env=production --ssl=letsencryptThis generates the following files:
deploy/
├── docker-compose.prod.yaml # Production Compose file
├── Caddyfile # Caddy reverse proxy config
└── .env.production # Environment variablesStaging environment
Generate a staging configuration by changing the --env flag:
forge deploy:compose --env=staging --ssl=letsencryptGenerated Services
The production Compose file defines six services:
┌──────────────────────────────────────────────────────────┐
│ DOCKER COMPOSE STACK │
├──────────────────────────────────────────────────────────┤
│ │
│ Internet │
│ │ │
│ ▼ │
│ ┌────────────────┐ │
│ │ Caddy │ :80, :443 │
│ │ (reverse │ Auto HTTPS via Let's Encrypt │
│ │ proxy) │ │
│ └──────┬─────────┘ │
│ │ │
│ ┌─────┼────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────┐ ┌──────┐ ┌──────────┐ │
│ │ web │ │ api │ │ admin │ │
│ │:3000 │ │:8080 │ │ :3001 │ │
│ └──────┘ └──┬───┘ └──────────┘ │
│ │ │
│ ┌─────┴─────┐ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────┐ ┌────────┐ │
│ │ postgres │ │ redis │ │
│ │ :5432 │ │ :6379 │ │
│ └──────────┘ └────────┘ │
│ │
└──────────────────────────────────────────────────────────┘Docker Compose File
The generated docker-compose.prod.yaml:
version: '3.8'
services:
api:
image: ${REGISTRY:-docker.io}/${APP_NAME}-api:${VERSION:-latest}
restart: always
environment:
- DATABASE_URL=postgres://${DB_USER}:${DB_PASSWORD}@postgres:5432/${DB_NAME}
- REDIS_URL=redis://redis:6379
- FORGE_ENCRYPTION_KEY=${FORGE_ENCRYPTION_KEY}
- RUST_LOG=info
- JWT_SECRET=${JWT_SECRET}
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
networks:
- internal
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 15s
web:
image: ${REGISTRY:-docker.io}/${APP_NAME}-web:${VERSION:-latest}
restart: always
environment:
- NEXT_PUBLIC_API_URL=https://api.${DOMAIN}
- API_URL=http://api:8080
depends_on:
- api
networks:
- internal
admin:
image: ${REGISTRY:-docker.io}/${APP_NAME}-admin:${VERSION:-latest}
restart: always
environment:
- NEXT_PUBLIC_API_URL=https://api.${DOMAIN}
- API_URL=http://api:8080
depends_on:
- api
networks:
- internal
caddy:
image: caddy:2-alpine
restart: always
ports:
- "80:80"
- "443:443"
- "443:443/udp" # HTTP/3
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
- web
- admin
- api
networks:
- internal
- external
postgres:
image: postgres:15-alpine
restart: always
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- internal
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
restart: always
command: redis-server --appendonly yes
volumes:
- redis_data:/data
networks:
- internal
volumes:
postgres_data:
redis_data:
caddy_data:
caddy_config:
networks:
internal:
driver: bridge
external:
driver: bridgeCaddyfile (Auto-HTTPS)
Caddy automatically obtains and renews Let's Encrypt certificates. No manual SSL configuration is required.
{
email admin@example.com
}
myapp.com {
reverse_proxy web:3000
encode gzip
}
admin.myapp.com {
reverse_proxy admin:3001
encode gzip
}
api.myapp.com {
reverse_proxy api:8080
encode gzip
header {
Access-Control-Allow-Origin https://myapp.com
Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
Access-Control-Allow-Headers "Authorization, Content-Type"
}
}How Caddy handles HTTPS
Caddy automatically obtains TLS certificates from Let's Encrypt when you use real domain names. It handles certificate renewal before expiry, HTTP-to-HTTPS redirects, and OCSP stapling -- all with zero configuration. The only requirement is that your domain's DNS points to the server and ports 80 and 443 are accessible.
Environment Configuration
The generated .env.production file:
# Application
APP_NAME=myapp
DOMAIN=myapp.com
VERSION=latest
REGISTRY=docker.io
# Database
DB_NAME=myapp
DB_USER=myapp
DB_PASSWORD= # Set a strong password
DB_HOST=postgres
DB_PORT=5432
# Security
JWT_SECRET= # Generate: openssl rand -hex 32
FORGE_ENCRYPTION_KEY= # Generate: forge secrets:generate-key
# Caddy
CADDY_EMAIL=admin@myapp.comSet all secrets before deployment
Never deploy with empty or default values for DB_PASSWORD, JWT_SECRET, or FORGE_ENCRYPTION_KEY. Generate strong, unique values for each.
Staging vs Production
| Setting | Staging | Production |
|---|---|---|
DOMAIN | staging.myapp.com | myapp.com |
RUST_LOG | debug | info |
DB_POOL_SIZE | 5 | 20 |
RATE_LIMIT_RPM | 1000 | 100 |
VERSION | staging-latest | v1.0.0 |
Data Persistence
All stateful data is stored in named Docker volumes:
| Volume | Service | Data |
|---|---|---|
postgres_data | PostgreSQL | Database files, tables, indexes |
redis_data | Redis | Cache, sessions, queued jobs |
caddy_data | Caddy | TLS certificates, OCSP responses |
caddy_config | Caddy | Runtime configuration |
Back up your volumes
Docker volumes persist across container restarts and image updates, but they are not automatically backed up. Set up regular backups for postgres_data at minimum.
# Backup PostgreSQL data
docker exec myapp-postgres-1 \
pg_dump -U myapp myapp > backup_$(date +%Y%m%d).sql
# Restore from backup
docker exec -i myapp-postgres-1 \
psql -U myapp myapp < backup_20260127.sqlStarting and Managing
Initial deployment
# Navigate to deploy directory
cd deploy/
# Set environment variables
cp .env.production .env
# Edit .env and set all secrets
# Build and start all services
docker compose -f docker-compose.prod.yaml up -d
# Check that all services are running
docker compose -f docker-compose.prod.yaml ps
# Run database migrations
docker compose -f docker-compose.prod.yaml exec api \
./forge-cli migrate
# Seed default data
docker compose -f docker-compose.prod.yaml exec api \
./forge-cli seedCommon operations
# All services
docker compose -f docker-compose.prod.yaml logs -f
# Specific service
docker compose -f docker-compose.prod.yaml logs -f api
docker compose -f docker-compose.prod.yaml logs -f caddy# Restart a single service
docker compose -f docker-compose.prod.yaml restart api
# Restart all services
docker compose -f docker-compose.prod.yaml restart# Pull new images
docker compose -f docker-compose.prod.yaml pull
# Recreate containers with new images (zero-downtime)
docker compose -f docker-compose.prod.yaml up -d --no-deps api
docker compose -f docker-compose.prod.yaml up -d --no-deps web
docker compose -f docker-compose.prod.yaml up -d --no-deps admin
# Run migrations after update
docker compose -f docker-compose.prod.yaml exec api \
./forge-cli migrate# Stop all services (preserves data)
docker compose -f docker-compose.prod.yaml down
# Stop and remove volumes (DESTROYS DATA)
docker compose -f docker-compose.prod.yaml down -vHealth checks
Verify your deployment is healthy:
# Check service health
docker compose -f docker-compose.prod.yaml ps
# Test API health endpoint
curl https://api.myapp.com/health
# Test web app
curl -I https://myapp.com
# Test admin app
curl -I https://admin.myapp.com
# Check SSL certificate
curl -vI https://myapp.com 2>&1 | grep "SSL certificate"Building Docker Images
FORGE generates Dockerfiles for each service. Build and push images before deploying:
# Build all images
docker build -t myapp-api:latest ./apps/api
docker build -t myapp-web:latest ./apps/web
docker build -t myapp-admin:latest ./apps/admin# Tag and push to your registry
docker tag myapp-api:latest registry.example.com/myapp-api:v1.0.0
docker push registry.example.com/myapp-api:v1.0.0
docker tag myapp-web:latest registry.example.com/myapp-web:v1.0.0
docker push registry.example.com/myapp-web:v1.0.0
docker tag myapp-admin:latest registry.example.com/myapp-admin:v1.0.0
docker push registry.example.com/myapp-admin:v1.0.0Next Steps
- Kubernetes Deployment -- Scale beyond a single server
- SSH Deployment -- Deploy without containers
- SSL Certificates -- Understand SSL options in detail