docs/self-hosting/examples.mdx
Every self-hosted setup is unique. You might be running on a single VPS, a Kubernetes cluster, behind Cloudflare Tunnel, or using a specific reverse proxy like Traefik or nginx. This page provides real-world Docker Compose configurations for various deployment scenarios to help you get started faster.
These examples go beyond the basic setup in the Self-Hosting with Docker guide, showing production-ready configurations with reverse proxies, SSL termination, and other common patterns.
<Info> **Help others by sharing your setup!** If you have a working configuration that isn't covered here, I'd love to include it. Simply [open a pull request](https://github.com/amruthpillai/reactive-resume) with your example added to this page. Your contribution helps the community and makes self-hosting easier for everyone. </Info>This example uses Traefik as a reverse proxy with automatic SSL certificate management via Let's Encrypt. Only the Reactive Resume app is exposed through Traefik—Postgres and the printer remain on an internal network.
<Tip> Traefik automatically discovers services via Docker labels and handles SSL certificates, making it ideal for setups where you want minimal configuration. </Tip>services:
traefik:
image: traefik:v3.2
restart: unless-stopped
command:
- "--api.dashboard=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.letsencrypt.acme.httpchallenge=true"
- "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
- "--certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL}"
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
- "--entrypoints.web.http.redirections.entryPoint.to=websecure"
- "--entrypoints.web.http.redirections.entryPoint.scheme=https"
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- traefik_letsencrypt:/letsencrypt
networks:
- reactive_resume_network
labels:
- "traefik.enable=true"
# Dashboard (optional, remove if not needed)
- "traefik.http.routers.traefik.rule=Host(`traefik.${DOMAIN}`)"
- "traefik.http.routers.traefik.entrypoints=websecure"
- "traefik.http.routers.traefik.tls.certresolver=letsencrypt"
- "traefik.http.routers.traefik.service=api@internal"
- "traefik.http.routers.traefik.middlewares=auth"
- "traefik.http.middlewares.auth.basicauth.users=${TRAEFIK_DASHBOARD_AUTH}"
postgres:
image: postgres:latest
restart: unless-stopped
environment:
- POSTGRES_DB=postgres
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql
networks:
- reactive_resume_network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]
interval: 10s
timeout: 5s
retries: 5
printer:
image: ghcr.io/browserless/chromium:latest
restart: unless-stopped
environment:
- QUEUED=10
- HEALTH=true
- CONCURRENT=5
- TOKEN=${BROWSERLESS_TOKEN}
networks:
- reactive_resume_network
healthcheck:
test: ["CMD-SHELL", 'curl -fsS "http://localhost:3000/pressure?token=${BROWSERLESS_TOKEN}" > /dev/null']
interval: 30s
timeout: 10s
retries: 3
reactive_resume:
image: amruthpillai/reactive-resume:latest
restart: unless-stopped
environment:
- APP_URL=https://resume.${DOMAIN}
- PRINTER_APP_URL=http://reactive_resume:3000
- DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/postgres
- PRINTER_ENDPOINT=ws://printer:3000?token=${BROWSERLESS_TOKEN}
- AUTH_SECRET=${AUTH_SECRET}
# Add other optional env vars as needed (SMTP, S3, OAuth, etc.)
volumes:
- reactive_resume_data:/app/data
networks:
- reactive_resume_network
depends_on:
postgres:
condition: service_healthy
printer:
condition: service_healthy
labels:
- "traefik.enable=true"
- "traefik.http.routers.reactive-resume.rule=Host(`resume.${DOMAIN}`)"
- "traefik.http.routers.reactive-resume.entrypoints=websecure"
- "traefik.http.routers.reactive-resume.tls.certresolver=letsencrypt"
- "traefik.http.services.reactive-resume.loadbalancer.server.port=3000"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
interval: 30s
timeout: 10s
retries: 3
networks:
reactive_resume_network:
driver: bridge
volumes:
traefik_letsencrypt:
postgres_data:
reactive_resume_data:
Environment variables (.env):
DOMAIN="example.com"
ACME_EMAIL="[email protected]"
POSTGRES_PASSWORD="your-secure-postgres-password"
AUTH_SECRET="your-auth-secret-from-openssl-rand-hex-32"
BROWSERLESS_TOKEN="your-browserless-token"
# Optional: Traefik dashboard auth (generate with: htpasswd -nb admin password)
TRAEFIK_DASHBOARD_AUTH="admin:$$apr1$$..."
This example uses nginx as a reverse proxy with SSL certificates (you'll need to provide your own certificates or use certbot separately).
services:
nginx:
image: nginx:alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/nginx/certs:ro
networks:
- reactive_resume_network
postgres:
image: postgres:latest
restart: unless-stopped
environment:
POSTGRES_DB: postgres
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql
networks:
- reactive_resume_network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]
interval: 10s
timeout: 5s
retries: 5
printer:
image: ghcr.io/browserless/chromium:latest
restart: unless-stopped
environment:
- QUEUED=10
- HEALTH=true
- CONCURRENT=5
- TOKEN=${BROWSERLESS_TOKEN}
networks:
- reactive_resume_network
healthcheck:
test: ["CMD-SHELL", 'curl -fsS "http://localhost:3000/pressure?token=${BROWSERLESS_TOKEN}" > /dev/null']
interval: 30s
timeout: 10s
retries: 3
reactive_resume:
image: amruthpillai/reactive-resume:latest
restart: unless-stopped
environment:
- APP_URL=https://resume.${DOMAIN}
- PRINTER_APP_URL=http://reactive_resume:3000
- DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/postgres
- PRINTER_ENDPOINT=ws://printer:3000?token=${BROWSERLESS_TOKEN}
- AUTH_SECRET=${AUTH_SECRET}
# Add other optional env vars as needed (SMTP, S3, OAuth, etc.)
volumes:
- reactive_resume_data:/app/data
networks:
- reactive_resume_network
depends_on:
postgres:
condition: service_healthy
printer:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
interval: 30s
timeout: 10s
retries: 3
networks:
reactive_resume_network:
driver: bridge
volumes:
postgres_data:
reactive_resume_data:
nginx configuration (nginx.conf):
events {
worker_connections 1024;
}
http {
upstream reactive_resume {
server reactive_resume:3000;
}
# Redirect HTTP to HTTPS
server {
listen 80;
server_name _;
return 301 https://$host$request_uri;
}
# HTTPS server
server {
listen 443 ssl http2;
server_name resume.example.com;
ssl_certificate /etc/nginx/certs/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/privkey.pem;
# SSL configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# 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;
# Proxy settings
location / {
proxy_pass http://reactive_resume;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
# Timeouts for long-running requests (PDF generation)
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Increase max body size for resume uploads
client_max_body_size 10M;
}
}
This example demonstrates a production-grade Docker Swarm deployment with multiple replicas, health checks, rolling updates, and Traefik integration. It includes SeaweedFS for S3-compatible storage and a PostgreSQL database with custom configuration.
<Tip> Docker Swarm is great for multi-node deployments where you need high availability and easy scaling. The app service is configured with 2 replicas and rolling update strategy. </Tip>services:
postgres:
image: postgres:latest
networks:
- reactive_resume_network
volumes:
- reactive_resume_postgres_data:/var/lib/postgresql
environment:
- POSTGRES_DB=$POSTGRES_DB
- POSTGRES_USER=$POSTGRES_USER
- POSTGRES_PASSWORD=$POSTGRES_PASSWORD
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $POSTGRES_USER -d $POSTGRES_DB"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
deploy:
mode: replicated
replicas: 1
printer:
image: ghcr.io/browserless/chromium:latest
networks:
- reactive_resume_network
environment:
- QUEUED=10
- HEALTH=true
- CONCURRENT=5
- TOKEN=$BROWSERLESS_TOKEN
healthcheck:
test: ["CMD-SHELL", 'curl -fsS "http://localhost:3000/pressure?token=$BROWSERLESS_TOKEN" > /dev/null']
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
deploy:
mode: replicated
replicas: 1
seaweedfs:
image: chrislusf/seaweedfs:latest
command: server -s3 -filer -dir=/data -ip=0.0.0.0
networks:
- reactive_resume_network
volumes:
- reactive_resume_seaweedfs_data:/data
environment:
- AWS_ACCESS_KEY_ID=$S3_ACCESS_KEY_ID
- AWS_SECRET_ACCESS_KEY=$S3_SECRET_ACCESS_KEY
healthcheck:
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://localhost:8888"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
deploy:
mode: replicated
replicas: 1
seaweedfs_create_bucket:
image: quay.io/minio/mc:latest
entrypoint: >
/bin/sh -c "
until mc alias set seaweedfs http://seaweedfs:8333 $S3_ACCESS_KEY_ID $S3_SECRET_ACCESS_KEY; do
echo 'Waiting for SeaweedFS...';
sleep 2;
done;
mc mb seaweedfs/$S3_BUCKET --ignore-existing;
"
networks:
- reactive_resume_network
deploy:
mode: replicated
replicas: 1
reactive_resume:
image: ghcr.io/amruthpillai/reactive-resume:latest
networks:
- traefik_network
- reactive_resume_network
volumes:
- reactive_resume_data:/app/data
environment:
- APP_URL=$APP_URL
# If using browserless with token auth, include the token in the URL:
# PRINTER_ENDPOINT=ws://printer:3000?token=your-secret-token
- PRINTER_ENDPOINT=$PRINTER_ENDPOINT
- DATABASE_URL=$DATABASE_URL
- AUTH_SECRET=$AUTH_SECRET
- GOOGLE_CLIENT_ID=$GOOGLE_CLIENT_ID
- GOOGLE_CLIENT_SECRET=$GOOGLE_CLIENT_SECRET
- GITHUB_CLIENT_ID=$GITHUB_CLIENT_ID
- GITHUB_CLIENT_SECRET=$GITHUB_CLIENT_SECRET
- LINKEDIN_CLIENT_ID=$LINKEDIN_CLIENT_ID
- LINKEDIN_CLIENT_SECRET=$LINKEDIN_CLIENT_SECRET
- SMTP_HOST=$SMTP_HOST
- SMTP_PORT=$SMTP_PORT
- SMTP_USER=$SMTP_USER
- SMTP_PASS=$SMTP_PASS
- SMTP_FROM=$SMTP_FROM
- SMTP_SECURE=$SMTP_SECURE
- S3_ACCESS_KEY_ID=$S3_ACCESS_KEY_ID
- S3_SECRET_ACCESS_KEY=$S3_SECRET_ACCESS_KEY
- S3_REGION=$S3_REGION
- S3_ENDPOINT=$S3_ENDPOINT
- S3_BUCKET=$S3_BUCKET
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
deploy:
mode: replicated
replicas: 1
labels:
- "traefik.enable=true"
- "traefik.http.routers.app.rule=Host(`rxresu.me`)"
- "traefik.http.routers.app.entrypoints=websecure"
- "traefik.http.routers.app.tls=true"
- "traefik.http.services.app.loadbalancer.server.port=3000"
configs:
reactive_resume_postgres_config:
name: reactive_resume_postgres_config
external: true
networks:
traefik_network:
external: true
reactive_resume_network:
name: reactive_resume_network
driver: overlay
attachable: true
volumes:
reactive_resume_postgres_data:
name: reactive_resume_postgres_data
reactive_resume_seaweedfs_data:
name: reactive_resume_seaweedfs_data
reactive_resume_data:
name: reactive_resume_data
Deploy the stack:
docker stack deploy -c compose-swarm.yml reactive_resume
Useful commands:
# Check service status
docker stack services reactive_resume
# View logs for the app
docker service logs -f reactive_resume_reactive_resume
# Scale the app
docker service scale reactive_resume_reactive_resume=3
# Remove the stack
docker stack rm reactive_resume
Have a different deployment setup that works well? Consider contributing it here. Some examples include:
To contribute, open a pull request with your example added to this page. Include: