Files
PacketStation/deploy/npm-proxy-host.md
T
spencermandClaude Fable 5.1 0b663bb2b2 Phase 0: repo skeleton, image, compose stacks, doctor, migration runner, frontend shell
New repo for the Packet Station rebuild (Python 3.12, FastAPI, PostgreSQL 16,
React, VisualKit, MFilesKit, Docker Compose on docker-1). Nothing is migrated
from the M: install.

- CLAUDE.md with the safety rules, README, CHANGELOG, docs (BRIEF, PLAN,
  SECURITY rewritten for the server with the requirement ID table, DEPLOY,
  stubs for FEED, POLICY, SCHEMA, GO-LIVE, PAPER-FALLBACK).
- packetstation CLI: app, worker (heartbeat stub), migrate, doctor, version,
  healthcheck. Settings from the environment with secret files. Stack guard
  (test never points at VISUAL production). Security headers and CSP.
  Trusted proxy resolved by name. JSON logs with redaction.
- Migration runner and 0001_base: settings, append-only audit_events with
  hash chain, ps_notify, grants for ps_app.
- Multi-stage Dockerfile (ODBC Driver 18, PESCO libraries from the Gitea
  index only), compose base, proxy overlay for npm_default, CI overlay,
  postgres initdb roles, env examples, NPM proxy host notes, Gitea Actions.
- Frontend: Vite, React, TypeScript, the PESCO dark theme ported with
  self-hosted fonts, placeholder sign-in and home.
- Verified on docker-1: image builds, CI stack comes up healthy, doctor
  database checks pass, 77 server tests pass against PostgreSQL.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-15 23:08:47 -06:00

101 lines
4.5 KiB
Markdown

# Nginx Proxy Manager proxy hosts
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:
```nginx
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).