Files
kjvstudy.org/nginx.conf
T
kennethreitz 2ab3dfa142 Add static site generation with nginx + hardened FastAPI sidecar
Pre-render ~1,277 high-traffic HTML pages (homepage, books, chapters)
at build time and serve them directly via nginx. All other routes
(verses, search, API, PDFs, Strong's) fall through to a FastAPI
sidecar. If the sidecar crashes, nginx continues serving static
pages and health checks.

Also harden the FastAPI app against the memory/crash issues:
- Switch from bare uvicorn to gunicorn with uvicorn workers
- Add --max-requests worker recycling to prevent memory leaks
- Add --timeout to kill hung workers
- Add per-IP rate limiting middleware (10 req/s, burst of 50)
- Add request timeout middleware (30s max per request)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 21:59:34 -05:00

185 lines
6.1 KiB
Nginx Configuration File

worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - [$time_local] "$request" $status $body_bytes_sent "$http_user_agent"';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
# Gzip
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 500;
gzip_types text/plain text/css text/xml text/javascript
application/json application/javascript application/xml
application/rss+xml image/svg+xml;
# Upstream: FastAPI sidecar for dynamic routes
upstream sidecar {
server 127.0.0.1:8001;
}
server {
listen 8000;
server_name _;
root /app/dist;
# Security headers
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options SAMEORIGIN always;
# -----------------------------------------------------------
# Dynamic routes — proxy to FastAPI sidecar
# -----------------------------------------------------------
# Search (dynamic query results)
location = /search {
proxy_pass http://sidecar;
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;
}
# All API endpoints
location /api/ {
proxy_pass http://sidecar;
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;
}
# PDF generation (on-demand)
location ~ /pdf$ {
proxy_pass http://sidecar;
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_read_timeout 30s;
}
# Verse of the day redirect (needs server-side date logic)
location = /verse-of-the-day {
proxy_pass http://sidecar;
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;
}
# OG images (dynamically generated)
location /og/ {
proxy_pass http://sidecar;
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;
}
# Family tree search (dynamic query)
location = /family-tree/search {
proxy_pass http://sidecar;
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;
}
# Family tree SVG (dynamically rendered)
location = /family-tree/lineage.svg {
proxy_pass http://sidecar;
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;
}
# OpenAPI docs
location /api/docs {
proxy_pass http://sidecar;
proxy_set_header Host $host;
}
location /api/redoc {
proxy_pass http://sidecar;
proxy_set_header Host $host;
}
location /api/openapi.json {
proxy_pass http://sidecar;
proxy_set_header Host $host;
}
# -----------------------------------------------------------
# Health check — static (no sidecar dependency)
# -----------------------------------------------------------
location = /health {
default_type application/json;
return 200 '{"status":"healthy","service":"kjv-study"}';
}
# -----------------------------------------------------------
# Static assets — aggressive caching
# -----------------------------------------------------------
location /static/ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
# -----------------------------------------------------------
# Robots / sitemaps
# -----------------------------------------------------------
location = /robots.txt {
default_type text/plain;
expires 1d;
}
location ~ ^/sitemap.*\.xml$ {
default_type application/xml;
expires 1d;
}
# Random verse list JSON
location = /random-verse-list.json {
default_type application/json;
expires 7d;
}
# -----------------------------------------------------------
# Default — serve pre-rendered HTML with clean URLs
# -----------------------------------------------------------
location / {
try_files $uri $uri/index.html $uri/ @sidecar;
expires 7d;
add_header Cache-Control "public";
}
# Fallback: if no static file exists, proxy to sidecar
location @sidecar {
proxy_pass http://sidecar;
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;
}
# Custom 404
error_page 404 /404.html;
location = /404.html {
internal;
}
}
}