Files
PacketStation/deploy/npm-proxy-host.md
T

4.7 KiB

Nginx Proxy Manager proxy hosts

This file records the docker-1 compose setup. Once production and test run on their native VMs, the proxy hosts point at the VMs over the LAN instead: see docs/DEPLOY.md, "Native VM install (phase 10)", NPM section.

NPM runs on docker-1 as container npm_app_1 on network npm_default (compose project in /home/spencerm/npm). It terminates HTTPS for both Packet Station stacks. The stacks publish no port. Each app joins npm_default through deploy/compose.proxy.yaml with its own alias, and NPM forwards to that alias.

Make one proxy host per stack in the NPM web UI.

Production Test
Domain name packetstation.pescoinc.biz packetstation-test.pescoinc.biz
Forward hostname packetstation-app packetstation-test-app
APP_ALIAS in the stack env file packetstation-app packetstation-test-app

Details tab

  • Scheme: http
  • Forward hostname / IP: the alias from the table above. Never an IP address: container addresses change when a stack is recreated.
  • Forward port: 8000
  • Cache assets: off. The app sets its own cache headers.
  • Block common exploits: on. Confirm it does not reject uploads when the test proxy host is made (plan open question 16).
  • Websockets support: off. Live updates use server-sent events, not websockets.
  • Access list: none. Sign-in is done by the app.

SSL tab

  • SSL certificate: the PESCO certificate for the name.
  • Force SSL: on.
  • HTTP/2 support: on.
  • HSTS enabled: off. The app sends Strict-Transport-Security itself, along with CSP and the other security headers. Two HSTS headers with different values would conflict.
  • HSTS subdomains: off.

Advanced tab

Paste this block as is for both hosts:

client_max_body_size 30m;

location /api/events {
    proxy_pass $forward_scheme://$server:$port;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_buffering off;
    proxy_cache off;
    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

Why each part is there:

  • client_max_body_size 30m: drawings, MTRs and reports are uploaded through the app. NPM's default is 1 MB at the server level. The app enforces its own, smaller per-type limits.
  • location /api/events: the server-sent events stream. Each open screen holds one request open for up to an hour. Buffering must be off or events arrive late in batches. The app also sends X-Accel-Buffering: no on this response.
  • proxy_http_version 1.1 with an empty Connection header keeps the upstream connection open for the stream.
  • The three proxy_set_header lines repeat what NPM's own location / adds. A custom location does not inherit NPM's proxy include, so without them the app would see NPM's address as the client and the wrong scheme.

Why $server and not the alias by name

NPM writes set $forward_scheme http; set $server "<forward hostname>"; set $port 8000; into each proxy host's server block, and its own location / uses proxy_pass with those variables. When proxy_pass contains a variable, nginx resolves the name at request time through the resolver NPM configures (Docker's DNS at 127.0.0.11). A literal proxy_pass http://packetstation-app:8000; is resolved once when nginx loads its config. That breaks in two ways: nginx refuses to load the config if the stack is down at that moment, which takes every other proxy host down with it, and after the stack is recreated the app gets a new address while nginx keeps sending to the old one. Using $forward_scheme://$server:$port keeps the events location on the same name and the same live resolution as the rest of the host.

Trust of X-Forwarded-*

The app trusts X-Forwarded-For and X-Forwarded-Proto only from npm_app_1's own address (TRUSTED_PROXY_HOST=npm_app_1), resolved at start and re-resolved every 60 seconds so an NPM restart is followed. It does not trust the whole npm_default subnet (172.22.0.0/16): other stacks share that network, and the per-address sign-in rate limit depends on the real client address. A request from any other address has its forwarded headers ignored. uvicorn runs without --proxy-headers; the app does this check itself.

Check after saving

From a PC on the LAN:

  1. https://<name>/ loads the sign-in page with a valid certificate.
  2. The response headers show one Strict-Transport-Security header, from the app.
  3. Open a packet screen and leave it for five minutes. A change made in another browser shows up without a reload, so the events stream is not being buffered or cut.
  4. Upload a 20 MB PDF on the test stack. It is accepted (Block common exploits did not reject it).