mirror of
https://github.com/l5yth/potato-mesh.git
synced 2026-10-31 03:51:58 +01:00
110 lines
5.2 KiB
Plaintext
110 lines
5.2 KiB
Plaintext
# Copyright © 2025-26 l5yth & contributors
|
||
# Licensed under the Apache License, Version 2.0 (see LICENSE)
|
||
#
|
||
# Example nginx reverse proxy for a bare-metal PotatoMesh web deployment.
|
||
# Terminates TLS, forwards to the Puma app, and serves /assets/ straight from
|
||
# disk with SPEC AV4-aligned cache headers. Copy the relevant blocks into your
|
||
# nginx config, replace every <PLACEHOLDER>, then `nginx -t && systemctl reload nginx`.
|
||
#
|
||
# DOCKER NOTE: the container image bakes assets into /app/public with no host
|
||
# volume, so the `location /assets/` disk-serving block below does NOT apply to
|
||
# the Compose/GHCR deployment. There, get immutable asset caching from the app
|
||
# itself (see https://github.com/l5yth/potato-mesh/issues/870) or bind-mount
|
||
# web/public into an nginx sidecar. The TLS + proxy parts apply to any deployment.
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# http { } context — place alongside your other http-level directives
|
||
# ---------------------------------------------------------------------------
|
||
|
||
# Persistent connections to the app so each request doesn't reopen a TCP socket.
|
||
# Requires `proxy_http_version 1.1` + `proxy_set_header Connection ""` below.
|
||
upstream potatomesh {
|
||
server 127.0.0.1:41447; # <APP_HOST:PORT> — match the app's PORT / -p flag
|
||
keepalive 32;
|
||
}
|
||
|
||
# Silences the "could not build optimal types_hash" warning once gzip_types grows.
|
||
types_hash_max_size 2048;
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# server { } context
|
||
# ---------------------------------------------------------------------------
|
||
server {
|
||
listen 443 ssl;
|
||
listen [::]:443 ssl;
|
||
http2 on;
|
||
|
||
server_name <mesh.example.org>;
|
||
client_max_body_size 50m;
|
||
|
||
# --- TLS (Certbot-managed example) -------------------------------------
|
||
# Certbot's options-ssl-nginx.conf sets `ssl_session_tickets off` (forward
|
||
# secrecy) and its own ssl_session_cache. Do NOT add `ssl_session_tickets on;`
|
||
# in this block — the include already sets it, and a second one is a fatal
|
||
# duplicate-directive error. Leaving tickets off means TLS 1.3 can't resume
|
||
# (~1 RTT on cold connections); only enable it with rotated
|
||
# ssl_session_ticket_key files if that RTT genuinely matters to you.
|
||
ssl_certificate /etc/letsencrypt/live/<mesh.example.org>/fullchain.pem;
|
||
ssl_certificate_key /etc/letsencrypt/live/<mesh.example.org>/privkey.pem;
|
||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||
|
||
# --- Static assets straight from disk (bare-metal only — see DOCKER NOTE) --
|
||
# Versioned JS/CSS carry ?v=<APP_VERSION> and the whole module graph is
|
||
# remapped via <script type="importmap"> (SPEC AV2–AV4), so they are safe to
|
||
# cache immutably for a year. Images/icons/fonts are NOT versioned, so they
|
||
# keep a short TTL + revalidation (AV4: a stale logo is cosmetic).
|
||
#
|
||
# The nginx worker user (http / www-data) must be able to traverse to and
|
||
# read this directory. Verify with:
|
||
# namei -l <root>/assets/styles/base.css # every parent needs o+x
|
||
location /assets/ {
|
||
root /opt/potato-mesh/web/public; # <path to web/public>; /assets/x → <root>/assets/x
|
||
access_log off;
|
||
gzip on;
|
||
gzip_vary on;
|
||
gzip_comp_level 5;
|
||
gzip_types text/css text/javascript application/javascript image/svg+xml;
|
||
# Optional: precompress *.js/*.css at release time and add `gzip_static on;`
|
||
# for max compression at zero per-request CPU.
|
||
|
||
# Versioned executable/style assets → immutable for a year.
|
||
location ~* \.(?:js|mjs|css)$ {
|
||
add_header Cache-Control "public, max-age=31536000, immutable";
|
||
add_header Access-Control-Allow-Origin "*" always; # add_header is not inherited into nested locations
|
||
}
|
||
# Unversioned images/icons/fonts → short TTL, always revalidate.
|
||
location ~* \.(?:png|svg|ico|jpe?g|gif|webp|woff2?)$ {
|
||
add_header Cache-Control "public, max-age=3600, must-revalidate";
|
||
add_header Access-Control-Allow-Origin "*" always;
|
||
}
|
||
}
|
||
|
||
# --- Everything else → the Puma app ------------------------------------
|
||
location / {
|
||
proxy_pass http://potatomesh;
|
||
proxy_http_version 1.1; # required for upstream keepalive
|
||
proxy_set_header Connection ""; # clear hop-by-hop header so sockets are reused
|
||
|
||
# The app derives its public scheme/host (INSTANCE_DOMAIN, metadata,
|
||
# sitemap, federation links) from these — set them or vanity/OpenGraph
|
||
# URLs resolve to the wrong scheme/host.
|
||
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;
|
||
|
||
# Live-update SSE (GET /api/events) holds a long-lived streaming response.
|
||
proxy_read_timeout 3600;
|
||
proxy_send_timeout 3600;
|
||
}
|
||
}
|
||
|
||
# --- Redirect plain HTTP to HTTPS ------------------------------------------
|
||
server {
|
||
listen 80;
|
||
listen [::]:80;
|
||
server_name <mesh.example.org>;
|
||
return 301 https://$host$request_uri;
|
||
}
|