Skip to main content

Overview

A reverse proxy sits between clients and your Documenso instance, providing:
  • SSL/TLS termination - Handle HTTPS encryption
  • Security headers - Add security headers to responses
  • Rate limiting - Protect against abuse
  • Compression - Reduce bandwidth usage
  • Load balancing - Distribute traffic across multiple instances

Nginx Configuration

Prerequisites

Install Nginx:
# Ubuntu/Debian
sudo apt update
sudo apt install nginx

# CentOS/RHEL
sudo yum install nginx

# macOS
brew install nginx
Verify installation:
nginx -v

Basic Configuration

Create a new site configuration:
# /etc/nginx/sites-available/documenso
server {
    listen 80;
    server_name sign.example.com;

    # Redirect HTTP to HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name sign.example.com;

    # SSL Configuration (see SSL Certificates section)
    ssl_certificate /etc/ssl/certs/documenso.crt;
    ssl_certificate_key /etc/ssl/private/documenso.key;

    # Proxy to Documenso
    location / {
        proxy_pass http://localhost:3000;
        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;
    }
}
Enable the site:
sudo ln -s /etc/nginx/sites-available/documenso /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Production Configuration

Enhanced configuration with security and performance optimizations:
# /etc/nginx/sites-available/documenso
server {
    listen 80;
    listen [::]:80;
    server_name sign.example.com;

    # Redirect all HTTP to HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name sign.example.com;

    # SSL Configuration
    ssl_certificate /etc/letsencrypt/live/sign.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/sign.example.com/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/sign.example.com/chain.pem;

    # SSL Protocols and Ciphers
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';
    ssl_prefer_server_ciphers off;

    # SSL Session Cache
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;
    ssl_session_tickets off;

    # OCSP Stapling
    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 8.8.8.8 8.8.4.4 valid=300s;
    resolver_timeout 5s;

    # Security Headers
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "no-referrer-when-downgrade" always;

    # Client Body Size (for document uploads)
    client_max_body_size 50M;

    # Timeouts
    proxy_connect_timeout 60s;
    proxy_send_timeout 300s;
    proxy_read_timeout 300s;

    # Gzip Compression
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/rss+xml;

    # Proxy Configuration
    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        
        # WebSocket support
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        
        # Standard proxy headers
        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_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Port $server_port;
        
        # Disable buffering for real-time updates
        proxy_buffering off;
        proxy_cache_bypass $http_upgrade;
    }

    # Health check endpoint (optional)
    location /api/health {
        proxy_pass http://localhost:3000/api/health;
        access_log off;
    }

    # Access and Error Logs
    access_log /var/log/nginx/documenso-access.log;
    error_log /var/log/nginx/documenso-error.log;
}

Multiple Instances (Load Balancing)

For high availability, distribute traffic across multiple Documenso instances:
# /etc/nginx/sites-available/documenso
upstream documenso_backend {
    least_conn;
    
    server localhost:3001 max_fails=3 fail_timeout=30s;
    server localhost:3002 max_fails=3 fail_timeout=30s;
    server localhost:3003 max_fails=3 fail_timeout=30s;
    
    keepalive 32;
}

server {
    listen 443 ssl http2;
    server_name sign.example.com;

    # SSL configuration...

    location / {
        proxy_pass http://documenso_backend;
        proxy_http_version 1.1;
        
        proxy_set_header Connection "";
        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;
    }
}

Rate Limiting

Protect against abuse and DoS attacks:
# Add to /etc/nginx/nginx.conf in http block
http {
    # Rate limiting zones
    limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
    limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s;
    limit_req_zone $binary_remote_addr zone=auth:10m rate=5r/m;

    # Connection limiting
    limit_conn_zone $binary_remote_addr zone=addr:10m;
}

# In your server block
server {
    # General rate limit
    limit_req zone=general burst=20 nodelay;
    limit_conn addr 10;

    # API endpoints
    location /api/ {
        limit_req zone=api burst=50 nodelay;
        proxy_pass http://localhost:3000;
    }

    # Auth endpoints (stricter limits)
    location /api/auth/ {
        limit_req zone=auth burst=5 nodelay;
        proxy_pass http://localhost:3000;
    }
}

Caddy Configuration

Prerequisites

Install Caddy:
# Ubuntu/Debian
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy

# macOS
brew install caddy
Verify installation:
caddy version

Basic Configuration

Caddy automatically handles SSL certificates via Let’s Encrypt:
# /etc/caddy/Caddyfile
sign.example.com {
    reverse_proxy localhost:3000
}
Reload configuration:
sudo systemctl reload caddy
Caddy automatically obtains and renews SSL certificates from Let’s Encrypt. No manual certificate management required!

Production Configuration

Enhanced configuration with all production features:
# /etc/caddy/Caddyfile
{
    # Global options
    email admin@example.com
    
    # Use Let's Encrypt production
    acme_ca https://acme-v02.api.letsencrypt.org/directory
    
    # Enable admin API
    admin localhost:2019
}

sign.example.com {
    # Automatic HTTPS with Let's Encrypt
    
    # Enable compression
    encode gzip zstd
    
    # Security headers
    header {
        Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "SAMEORIGIN"
        X-XSS-Protection "1; mode=block"
        Referrer-Policy "no-referrer-when-downgrade"
        -Server
    }
    
    # Request body size limit (50MB for document uploads)
    request_body {
        max_size 50MB
    }
    
    # Logging
    log {
        output file /var/log/caddy/documenso.log {
            roll_size 100mb
            roll_keep 10
            roll_keep_for 720h
        }
        format json
    }
    
    # Reverse proxy to Documenso
    reverse_proxy localhost:3000 {
        # Health check
        health_uri /api/health
        health_interval 30s
        health_timeout 5s
        
        # Timeouts
        transport http {
            dial_timeout 5s
            response_header_timeout 300s
        }
    }
}

Multiple Instances (Load Balancing)

sign.example.com {
    encode gzip zstd
    
    # Load balancing across multiple instances
    reverse_proxy localhost:3001 localhost:3002 localhost:3003 {
        # Load balancing policy
        lb_policy least_conn
        
        # Health checks
        health_uri /api/health
        health_interval 10s
        health_timeout 5s
        
        # Retry failed requests
        lb_try_duration 5s
        lb_try_interval 250ms
    }
}

Rate Limiting

Caddy rate limiting using the rate-limit plugin:
sign.example.com {
    # General rate limit
    rate_limit {
        zone general {
            key {remote_host}
            events 100
            window 1m
        }
    }
    
    # API endpoints - higher limit
    @api {
        path /api/*
    }
    route @api {
        rate_limit {
            zone api {
                key {remote_host}
                events 300
                window 1m
            }
        }
        reverse_proxy localhost:3000
    }
    
    # Auth endpoints - strict limit
    @auth {
        path /api/auth/*
    }
    route @auth {
        rate_limit {
            zone auth {
                key {remote_host}
                events 10
                window 1m
            }
        }
        reverse_proxy localhost:3000
    }
    
    # Default route
    reverse_proxy localhost:3000
}
The rate_limit directive requires the Caddy rate-limit plugin. Install it with: caddy add-package github.com/mholt/caddy-ratelimit

Custom SSL Certificates

If you want to use your own SSL certificates instead of Let’s Encrypt:
sign.example.com {
    tls /path/to/cert.pem /path/to/key.pem
    
    reverse_proxy localhost:3000
}

Traefik Configuration

Docker Compose with Traefik

Integrate Documenso with Traefik for automatic SSL:
version: '3.8'

services:
  traefik:
    image: traefik:v2.10
    container_name: traefik
    command:
      - "--api.dashboard=true"
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
      - "--certificatesresolvers.letsencrypt.acme.email=admin@example.com"
      - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - traefik-letsencrypt:/letsencrypt
    restart: unless-stopped

  database:
    image: postgres:15
    environment:
      - POSTGRES_USER=${POSTGRES_USER}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_DB=${POSTGRES_DB}
    volumes:
      - database:/var/lib/postgresql/data
    restart: unless-stopped

  documenso:
    image: documenso/documenso:latest
    depends_on:
      - database
    environment:
      - NEXT_PUBLIC_WEBAPP_URL=https://sign.example.com
      # ... other environment variables
    volumes:
      - /opt/documenso/cert.p12:/opt/documenso/cert.p12:ro
    labels:
      - "traefik.enable=true"
      # HTTP to HTTPS redirect
      - "traefik.http.routers.documenso-http.rule=Host(`sign.example.com`)"
      - "traefik.http.routers.documenso-http.entrypoints=web"
      - "traefik.http.routers.documenso-http.middlewares=redirect-to-https"
      # HTTPS router
      - "traefik.http.routers.documenso.rule=Host(`sign.example.com`)"
      - "traefik.http.routers.documenso.entrypoints=websecure"
      - "traefik.http.routers.documenso.tls=true"
      - "traefik.http.routers.documenso.tls.certresolver=letsencrypt"
      # Service configuration
      - "traefik.http.services.documenso.loadbalancer.server.port=3000"
      # Middlewares
      - "traefik.http.routers.documenso.middlewares=security-headers"
      # Redirect middleware
      - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
      - "traefik.http.middlewares.redirect-to-https.redirectscheme.permanent=true"
      # Security headers
      - "traefik.http.middlewares.security-headers.headers.stsSeconds=63072000"
      - "traefik.http.middlewares.security-headers.headers.stsIncludeSubdomains=true"
      - "traefik.http.middlewares.security-headers.headers.stsPreload=true"
      - "traefik.http.middlewares.security-headers.headers.frameDeny=true"
      - "traefik.http.middlewares.security-headers.headers.contentTypeNosniff=true"
    restart: unless-stopped

volumes:
  database:
  traefik-letsencrypt:

Testing Your Configuration

Verify Proxy is Working

Test that Documenso is accessible through the proxy:
curl -I https://sign.example.com

Check SSL Configuration

Use SSL Labs to test your SSL configuration:
https://www.ssllabs.com/ssltest/analyze.html?d=sign.example.com

Verify Security Headers

Check that security headers are present:
curl -I https://sign.example.com | grep -i "strict-transport\|x-frame\|x-content"

Test Health Endpoint

Verify the health check endpoint:
curl https://sign.example.com/api/health
Expected response:
{
  "status": "ok",
  "timestamp": "2024-03-04T12:00:00.000Z"
}

Troubleshooting

502 Bad Gateway

Causes:
  • Documenso container not running
  • Wrong proxy_pass port
  • Firewall blocking connection
Solution:
# Check Documenso is running
docker compose ps

# Test local connection
curl http://localhost:3000/api/health

# Check Nginx error logs
sudo tail -f /var/log/nginx/error.log

413 Request Entity Too Large

Increase client body size:
# Nginx
client_max_body_size 50M;
# Caddy
request_body {
    max_size 50MB
}

SSL Certificate Errors

For Nginx with Let’s Encrypt:
# Renew certificates
sudo certbot renew --nginx

# Test renewal
sudo certbot renew --dry-run
For Caddy:
# Check Caddy logs
sudo journalctl -u caddy -f

# Verify certificate
caddy trust

WebSocket Connection Failed

Ensure WebSocket headers are set:
# Nginx
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Caddy handles WebSockets automatically
reverse_proxy localhost:3000

See Also