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.
Configure Nginx as a reverse proxy in front of the Paperclip API and frontend, set up TLS termination, and apply security headers.
Internet (HTTPS 443)
↓
Nginx (TLS termination)
├─── /api/* → http://api:3100
└─── /* → http://frontend:80 (static files)
nginx.confevents {
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;
}
}
}
# 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
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;
}
nginx.conf that handles TLS, API, and frontend routingproxy_read_timeout setting and why it needs to be high for LLM routes