Skip to content

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

bash
forge deploy:compose --env=production --ssl=letsencrypt

This generates the following files:

deploy/
├── docker-compose.prod.yaml    # Production Compose file
├── Caddyfile                   # Caddy reverse proxy config
└── .env.production             # Environment variables

Staging environment

Generate a staging configuration by changing the --env flag:

bash
forge deploy:compose --env=staging --ssl=letsencrypt

Generated 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:

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: bridge

Caddyfile (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:

bash
# 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.com

Set 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

SettingStagingProduction
DOMAINstaging.myapp.commyapp.com
RUST_LOGdebuginfo
DB_POOL_SIZE520
RATE_LIMIT_RPM1000100
VERSIONstaging-latestv1.0.0

Data Persistence

All stateful data is stored in named Docker volumes:

VolumeServiceData
postgres_dataPostgreSQLDatabase files, tables, indexes
redis_dataRedisCache, sessions, queued jobs
caddy_dataCaddyTLS certificates, OCSP responses
caddy_configCaddyRuntime 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.

bash
# 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.sql

Starting and Managing

Initial deployment

bash
# 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 seed

Common operations

bash
# 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
bash
# Restart a single service
docker compose -f docker-compose.prod.yaml restart api

# Restart all services
docker compose -f docker-compose.prod.yaml restart
bash
# 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
bash
# 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 -v

Health checks

Verify your deployment is healthy:

bash
# 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:

bash
# 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
bash
# 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.0

Next Steps

Released under the MIT License.