Back to Reactive Resume

Docker Compose Examples

docs/self-hosting/examples.mdx

5.0.2015.2 KB
Original Source

Overview

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>

Docker with Traefik

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>
yaml
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):

bash
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$$..."

Docker with nginx

This example uses nginx as a reverse proxy with SSL certificates (you'll need to provide your own certificates or use certbot separately).

yaml
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):

nginx
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;
    }
}
<Tip> For automatic SSL certificates with nginx, consider using [certbot](https://certbot.eff.org/) with the `--nginx` plugin, or a companion container like [nginx-proxy-acme](https://github.com/nginx-proxy/acme-companion). </Tip>

Docker Swarm

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

bash
docker stack deploy -c compose-swarm.yml reactive_resume

Useful commands:

bash
# 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
<Note> This example assumes you have an external Traefik network already set up. Adjust the `traefik_network` reference and labels based on your Traefik configuration. </Note>

Contributing Your Setup

Have a different deployment setup that works well? Consider contributing it here. Some examples include:

  • Kubernetes / Helm charts
  • Cloudflare Tunnel
  • Caddy reverse proxy
  • Docker with Portainer
  • Podman configurations
  • Cloud-specific deployments (AWS ECS, Google Cloud Run, Azure Container Apps)

To contribute, open a pull request with your example added to this page. Include:

  1. A brief description of when/why someone would use this setup
  2. The complete Docker Compose (or equivalent) configuration
  3. Any additional configuration files (nginx.conf, etc.)
  4. Required environment variables