# nginx and networking The nginx front door, how it serves each route, and the production-specific rules it enforces. See also [Production overview](/docs/production.html) and [Deploy and update](/docs/production-deploy.html). ## Build and configuration The nginx image (`nginx/Dockerfile`) renders `nginx/nginx.conf.template` at start through `nginx/start.sh`, which substitutes a small allow-list of variables (`NGINX_CACHE_CONFIG`, `NGINX_CACHE_MAX_SIZE`, `NGINX_MAX_BODY_SIZE`) and leaves nginx runtime variables such as `$http_upgrade` untouched. The host's `devplacepy/static` directory is bind-mounted read-only at `/app/static`, so served assets and uploads always match the running code without an image rebuild. ## Route map | Location | Behavior | |----------|----------| | `/static/uploads/` | Served from disk with `Content-Disposition: attachment` and `X-Content-Type-Options: nosniff`. | | `/static/` | Served from disk, immutable, 7-day cache. | | `/devii/ws` | Proxied as a WebSocket (`Upgrade`/`Connection` headers, 1h read/send timeout). | | `/avatar/` | Proxied to the app (generated SVG avatars). | | `/` | Proxied to the app; optional micro-cache. | ## Uploads are forced downloads The application mounts `/static/uploads` through `UploadStaticFiles`, which sets `Content-Disposition: attachment` so a user-supplied file downloads instead of rendering inline - a stored-XSS defense against uploaded SVG or HTML on the same origin. In production nginx serves those files directly from disk and would bypass that header, so the `/static/uploads/` location **re-applies** `Content-Disposition: attachment` and `X-Content-Type-Options: nosniff`. Keep this rule whenever the static handling changes. ## WebSockets The Devii terminal connects to `/devii/ws`. A plain reverse-proxy block drops the WebSocket handshake, so nginx maps the upgrade header: ``` map $http_upgrade $connection_upgrade { default upgrade; '' close; } ``` and the `/devii/ws` location forwards `Upgrade $http_upgrade` and `Connection $connection_upgrade` with a long read timeout. Note the app serves `/devii/ws` only from the worker holding the service lock; other workers close with code 1013 and the auto-reconnecting client lands on the owner, so a connection may take a brief reconnect to settle. See [Multi-worker and concurrency](/docs/production-concurrency.html) for the lock-owner model. ## Upload size ceiling `client_max_body_size` is set from `NGINX_MAX_BODY_SIZE` (default `50m`). It must be **>= the admin-configurable `max_upload_size_mb`** (Admin -> Settings). If the app limit exceeds the nginx limit, nginx rejects the upload with **HTTP 413** before it ever reaches the app. After raising `max_upload_size_mb`, raise `NGINX_MAX_BODY_SIZE` to match and restart nginx. ## Security headers and caching The server block sets `X-Content-Type-Options`, `X-Frame-Options: DENY`, `X-XSS-Protection`, and `Referrer-Policy` (these are inherited only by locations that declare no `add_header` of their own). gzip is enabled for text, JSON, JS, CSS, and SVG. The micro-cache is off by default; set `NGINX_CACHE_ENABLED=true` to cache proxied 200s for one minute with `X-Cache-Status` reporting. ## Troubleshooting - **Devii terminal will not connect** - confirm the `map` block and the `/devii/ws` location are present in the rendered config (`docker compose exec nginx cat /etc/nginx/conf.d/default.conf`); a connection that closes immediately with 1013 is the lock-owner reconnect, not a failure. - **Uploads fail with 413** - `NGINX_MAX_BODY_SIZE` is below `max_upload_size_mb`; raise it and restart nginx. - **Uploaded file renders inline instead of downloading** - the `/static/uploads/` location lost its `Content-Disposition` rule. - **Static assets stale after deploy** - the static bind mount is read-only and live; a hard refresh clears the 7-day browser cache.