| 1 | # ──────────────────────────────────────────────────────────────────
|
|---|
| 2 | # PrutCMS v10 — nginx reverse proxy
|
|---|
| 3 | #
|
|---|
| 4 | # Place this in /etc/nginx/sites-available/prutcms and symlink to
|
|---|
| 5 | # /etc/nginx/sites-enabled/. Replace <YOUR-DOMAIN> with the real host.
|
|---|
| 6 | #
|
|---|
| 7 | # Key points:
|
|---|
| 8 | # • SSL termination here; Node listens on 127.0.0.1:3000 only.
|
|---|
| 9 | # • WebSocket upgrade headers wired through (Prutter live messaging).
|
|---|
| 10 | # • Static assets cached aggressively (Node gives them 1y maxAge anyway).
|
|---|
| 11 | # • Audio streaming endpoint /audio/stream/* must NOT be cached by nginx —
|
|---|
| 12 | # each request carries a different signed token and is byte-range based.
|
|---|
| 13 | # • HSTS + security headers added at the proxy.
|
|---|
| 14 | #
|
|---|
| 15 | # After installing:
|
|---|
| 16 | # sudo ln -s /etc/nginx/sites-available/prutcms /etc/nginx/sites-enabled/
|
|---|
| 17 | # sudo nginx -t
|
|---|
| 18 | # sudo systemctl reload nginx
|
|---|
| 19 | #
|
|---|
| 20 | # Then provision SSL:
|
|---|
| 21 | # sudo certbot --nginx -d <YOUR-DOMAIN> -d www.<YOUR-DOMAIN>
|
|---|
| 22 | # certbot will edit this file in place to insert the cert paths.
|
|---|
| 23 | # ──────────────────────────────────────────────────────────────────
|
|---|
| 24 |
|
|---|
| 25 | # WebSocket connection upgrade map (let nginx set it once globally)
|
|---|
| 26 | map $http_upgrade $connection_upgrade {
|
|---|
| 27 | default upgrade;
|
|---|
| 28 | '' close;
|
|---|
| 29 | }
|
|---|
| 30 |
|
|---|
| 31 | # Redirect HTTP → HTTPS
|
|---|
| 32 | server {
|
|---|
| 33 | listen 80;
|
|---|
| 34 | listen [::]:80;
|
|---|
| 35 | server_name <YOUR-DOMAIN> www.<YOUR-DOMAIN>;
|
|---|
| 36 |
|
|---|
| 37 | # Let's Encrypt http-01 challenge path
|
|---|
| 38 | location /.well-known/acme-challenge/ {
|
|---|
| 39 | root /var/www/letsencrypt;
|
|---|
| 40 | }
|
|---|
| 41 |
|
|---|
| 42 | location / {
|
|---|
| 43 | return 301 https://$host$request_uri;
|
|---|
| 44 | }
|
|---|
| 45 | }
|
|---|
| 46 |
|
|---|
| 47 | server {
|
|---|
| 48 | listen 443 ssl http2;
|
|---|
| 49 | listen [::]:443 ssl http2;
|
|---|
| 50 | server_name <YOUR-DOMAIN> www.<YOUR-DOMAIN>;
|
|---|
| 51 |
|
|---|
| 52 | # certbot will fill these in:
|
|---|
| 53 | # ssl_certificate /etc/letsencrypt/live/<YOUR-DOMAIN>/fullchain.pem;
|
|---|
| 54 | # ssl_certificate_key /etc/letsencrypt/live/<YOUR-DOMAIN>/privkey.pem;
|
|---|
| 55 | ssl_protocols TLSv1.2 TLSv1.3;
|
|---|
| 56 | ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
|
|---|
| 57 | ssl_prefer_server_ciphers off;
|
|---|
| 58 | ssl_session_cache shared:SSL:10m;
|
|---|
| 59 | ssl_session_timeout 1d;
|
|---|
| 60 | ssl_stapling on;
|
|---|
| 61 | ssl_stapling_verify on;
|
|---|
| 62 |
|
|---|
| 63 | # Security headers (Helmet adds them at the app too — these are belt+braces)
|
|---|
| 64 | add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
|
|---|
| 65 | add_header X-Frame-Options "SAMEORIGIN" always;
|
|---|
| 66 | add_header X-Content-Type-Options "nosniff" always;
|
|---|
| 67 | add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
|---|
| 68 |
|
|---|
| 69 | # Reasonable body size — accommodates 50 MB MP3 uploads with headroom
|
|---|
| 70 | client_max_body_size 60M;
|
|---|
| 71 |
|
|---|
| 72 | # Compression
|
|---|
| 73 | gzip on;
|
|---|
| 74 | gzip_types text/plain text/css application/json application/javascript application/xml application/atom+xml application/rss+xml image/svg+xml;
|
|---|
| 75 | gzip_min_length 256;
|
|---|
| 76 |
|
|---|
| 77 | # ── Static assets: serve via Node, but tell upstream they're long-cached.
|
|---|
| 78 | location /assets/ {
|
|---|
| 79 | proxy_pass http://127.0.0.1:3000;
|
|---|
| 80 | proxy_set_header Host $host;
|
|---|
| 81 | proxy_cache_valid 200 1y;
|
|---|
| 82 | add_header Cache-Control "public, max-age=31536000, immutable";
|
|---|
| 83 | }
|
|---|
| 84 |
|
|---|
| 85 | # ── Audio streaming: pass through, no caching, no buffering (HTML5 <audio>
|
|---|
| 86 | # needs to seek using byte-range; nginx must not slurp the whole file).
|
|---|
| 87 | location /audio/stream/ {
|
|---|
| 88 | proxy_pass http://127.0.0.1:3000;
|
|---|
| 89 | proxy_set_header Host $host;
|
|---|
| 90 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|---|
| 91 | proxy_set_header X-Forwarded-Proto $scheme;
|
|---|
| 92 | proxy_buffering off;
|
|---|
| 93 | proxy_request_buffering off;
|
|---|
| 94 | proxy_read_timeout 600s;
|
|---|
| 95 | add_header Cache-Control "private, no-store" always;
|
|---|
| 96 | }
|
|---|
| 97 |
|
|---|
| 98 | # ── WebSocket: Prutter live chat. Long-lived connection.
|
|---|
| 99 | location /ws/ {
|
|---|
| 100 | proxy_pass http://127.0.0.1:3000;
|
|---|
| 101 | proxy_http_version 1.1;
|
|---|
| 102 | proxy_set_header Upgrade $http_upgrade;
|
|---|
| 103 | proxy_set_header Connection $connection_upgrade;
|
|---|
| 104 | proxy_set_header Host $host;
|
|---|
| 105 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|---|
| 106 | proxy_set_header X-Forwarded-Proto $scheme;
|
|---|
| 107 | proxy_read_timeout 3600s; # let pings keep it alive an hour
|
|---|
| 108 | proxy_send_timeout 3600s;
|
|---|
| 109 | }
|
|---|
| 110 |
|
|---|
| 111 | # ── Everything else: standard reverse proxy
|
|---|
| 112 | location / {
|
|---|
| 113 | proxy_pass http://127.0.0.1:3000;
|
|---|
| 114 | proxy_http_version 1.1;
|
|---|
| 115 | proxy_set_header Host $host;
|
|---|
| 116 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|---|
| 117 | proxy_set_header X-Forwarded-Proto $scheme;
|
|---|
| 118 | proxy_set_header X-Real-IP $remote_addr;
|
|---|
| 119 |
|
|---|
| 120 | # Upgrade headers in case any other route ever uses WS
|
|---|
| 121 | proxy_set_header Upgrade $http_upgrade;
|
|---|
| 122 | proxy_set_header Connection $connection_upgrade;
|
|---|
| 123 |
|
|---|
| 124 | proxy_read_timeout 90s;
|
|---|
| 125 | }
|
|---|
| 126 | }
|
|---|