Nginx Reverse Proxy

Sample nginx.conf proxying the API (:3100) and React frontend (:5173) behind port 443, TLS termination with Let's Encrypt Certbot, CORS headers, and WebSocket proxy settings.

Nginx Reverse Proxy Configuration

What you'll learn

Configure Nginx as a reverse proxy in front of the Paperclip API and frontend, set up TLS termination, and apply security headers.

Architecture

Internet (HTTPS 443)
     ↓
  Nginx (TLS termination)
  ├─── /api/* → http://api:3100
  └─── /*     → http://frontend:80 (static files)

Complete nginx.conf

events {
  worker_connections 1024;
}

http {
  # Security headers
  add_header X-Content-Type-Options   nosniff;
  add_header X-Frame-Options          DENY;
  add_header X-XSS-Protection         "1; mode=block";
  add_header Referrer-Policy          strict-origin-when-cross-origin;
  add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

  # Gzip compression
  gzip on;
  gzip_types text/plain text/css application/json application/javascript;

  # Rate limiting
  limit_req_zone $binary_remote_addr zone=api:10m rate=60r/m;

  upstream api_backend {
    server api:3100;
    keepalive 32;
  }

  upstream frontend_backend {
    server frontend:80;
    keepalive 32;
  }

  server {
    listen 80;
    server_name your-domain.com;
    return 301 https://$host$request_uri;
  }

  server {
    listen 443 ssl http2;
    server_name your-domain.com;

    ssl_certificate     /etc/ssl/paperclip/fullchain.pem;
    ssl_certificate_key /etc/ssl/paperclip/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         HIGH:!aNULL:!MD5;

    # API routes
    location /api/ {
      limit_req zone=api burst=20 nodelay;
      proxy_pass         http://api_backend;
      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_http_version 1.1;
      proxy_set_header   Connection "";
      proxy_read_timeout 120s;  # allow time for LLM calls
    }

    # Webhook endpoints (higher timeout, no rate limit on burst)
    location /webhooks/ {
      proxy_pass         http://api_backend;
      proxy_set_header   Host $host;
      proxy_read_timeout 30s;
    }

    # Frontend (SPA)
    location / {
      proxy_pass         http://frontend_backend;
      proxy_set_header   Host $host;
      # SPA fallback: serve index.html for unknown routes
      proxy_intercept_errors on;
      error_page 404 = /index.html;
    }
  }
}

TLS with Let's Encrypt

# Using certbot in Docker
docker run --rm \
  -v ./ssl:/etc/letsencrypt \
  -p 80:80 \
  certbot/certbot certonly \
  --standalone \
  -d your-domain.com \
  --email admin@your-domain.com \
  --agree-tos

Renew monthly:

0 0 1 * * docker run --rm -v ./ssl:/etc/letsencrypt certbot/certbot renew

Rate Limiting the API

The limit_req_zone above limits each IP to 60 requests/minute with a burst of 20. Adjust for your expected usage pattern.

For webhook endpoints, use a shared secret instead of rate limiting:

# Verify webhook secret header
location /webhooks/ {
  if ($http_x_paperclip_secret != "your-secret") { return 403; }
  proxy_pass http://api_backend;
}

Checkpoint ✓

Sign in for progress tracking & Q&A →