# 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 , 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; # — 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; # Optional: rate-limit requests from other PotatoMesh instances. Peers send # `User-Agent: PotatoMesh/ (+https://)` and back off on 429. # To enable, uncomment the map, limit_req_zone and limit_req_status lines here # and the limit_req line in `location /` below. Requests with any other # User-Agent are not limited. # map $http_user_agent $potatomesh_peer { # default ""; # ~^PotatoMesh/ $binary_remote_addr; # } # limit_req_zone $potatomesh_peer zone=potatomesh_peers:1m rate=30r/m; # limit_req_status 429; # Optional: rate-limit reads per client address: API requests the response # cache does not answer (any `since` or `before`, or a collection `limit` other # than 10, 200 or 1000) and /api/telemetry/aggregated. An open dashboard sends # up to 8 of these a second on a busy mesh, and visitors behind one NAT (camps, # venues) share one address: set `rate` to 8r/s per such visitor. To enable, # uncomment the map and the limit_req_zone lines here, the limit_req_status # line above (once, for either limit or both) and the second limit_req line in # `location /`. # map "$uri?$args" $potatomesh_read { # default ""; # ~^/api/telemetry/aggregated[?] $binary_remote_addr; # "~*^/api/.*[?&]((s|%73)(i|%69)(n|%6e)(c|%63)(e|%65)|(b|%62)(e|%65)(f|%66)(o|%6f)(r|%72)(e|%65))=" $binary_remote_addr; # "~*^/api/(nodes|positions|telemetry|neighbors|traces|ingestors|waypoints|messages)/?[?](.*&)?(l|%6c)(i|%69)(m|%6d)(i|%69)(t|%74)=(?!(10|200|1000)(&|$))" $binary_remote_addr; # } # limit_req_zone $potatomesh_read zone=potatomesh_reads:10m rate=40r/s; # --------------------------------------------------------------------------- # server { } context # --------------------------------------------------------------------------- server { listen 443 ssl; listen [::]:443 ssl; http2 on; server_name ; 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//fullchain.pem; ssl_certificate_key /etc/letsencrypt/live//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= and the whole module graph is # remapped via