Files
dashfront/NGINX.md
2026-09-22 07:20:31 +02:00

467 lines
21 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# NGINX Configuration Guide for Dashfront & Dashback
This document provides a production-tested, complete NGINX reverse-proxy and static hosting guide for **Dashfront** (Vite + React 19 PWA) and **Dashback** (Express REST + SSE API).
---
## Table of Contents
1. [Architecture & Request Flow](#1-architecture--request-flow)
2. [Key Technical Requirements](#2-key-technical-requirements)
3. [Production Configuration (`dashfront.conf`)](#3-production-configuration-dashfrontconf)
4. [Detailed Breakdown of Critical Directives](#4-detailed-breakdown-of-critical-directives)
- [PWA & Cache Invalidation Strategy](#pwa--cache-invalidation-strategy)
- [SPA Routing (`try_files`)](#spa-routing-try_files)
- [Server-Sent Events (SSE) Buffering & Timeouts](#server-sent-events-sse-buffering--timeouts)
- [File Uploads & Request Body Limits](#file-uploads--request-body-limits)
- [WebAssembly (WASM) MIME Types](#webassembly-wasm-mime-types)
- [Camera & Hardware Permissions](#camera--hardware-permissions)
5. [Alternative: Remote Vite Dev Server Proxy](#5-alternative-remote-vite-dev-server-proxy)
6. [Deployment & Operations Runbook](#6-deployment--operations-runbook)
7. [Troubleshooting Matrix](#7-troubleshooting-matrix)
---
## 1. Architecture & Request Flow
In production, NGINX acts as both the high-performance static web server for Dashfront assets and the secure reverse proxy for the Dashback Express service.
```
┌──────────────────────────────────────────────┐
│ CLIENT │
│ (Desktop Browser, Tablet, Mobile PWA) │
└──────────────────────┬───────────────────────┘
│ HTTPS (:443)
▼
┌──────────────────────────────────────────────┐
│ NGINX │
│ TLS Termination, Gzip, Caching, Auth │
└──────────────┬────────────────┬──────────────┘
│ │
/assets/*, /index.html │ │ /api/*
/sw.js, static files │ │ (REST & SSE)
▼ ▼
┌───────────────────┐ ┌───────────────────┐
│ Static Build Dir │ │ Dashback │
│ `dashfront/dist` │ │ Express API │
│ │ │ (localhost:4208) │
└───────────────────┘ └───────────────────┘
```
| Path | Handled By | Cache Policy | Notes |
|---|---|---|---|
| `/index.html` & `/` | NGINX static file | `no-cache, no-store, must-revalidate` | Always fresh so users get new script hashes |
| `/sw.js`, `/registerSW.js` | NGINX static file | `no-cache, no-store, must-revalidate` | **Must never cache**; avoids bricking PWA updates |
| `/workbox-*.js` | NGINX static file | `no-cache, no-store, must-revalidate` | Workbox runtime loader |
| `/manifest.webmanifest` | NGINX static file | `public, max-age=3600` | PWA manifest definition |
| `/assets/*` | NGINX static file | `public, max-age=31536000, immutable` | Content-hashed JS, CSS, WASM, and workers |
| `/api/events` | Dashback (`:4208`) | `no-cache, no-buffering` | Server-Sent Events (SSE) live push notifications |
| `/api/*` | Dashback (`:4208`) | Dynamic / Backend controlled | REST endpoints, auth cookies, image & PDF uploads |
| All other paths | NGINX `try_files` | Fallback to `/index.html` | React Router HTML5 History API deep linking |
---
## 2. Key Technical Requirements
1. **PWA Lifecycles**: A Progressive Web App checks for updates by fetching `sw.js`. If NGINX caches `sw.js` or `index.html`, client devices can remain stuck on obsolete bundles indefinitely.
2. **SSE Streaming**: Real-time push updates at `/api/events` require NGINX proxy buffering and proxy caching to be turned **OFF**, with a 24-hour read timeout to prevent arbitrary 60-second connection drops.
3. **Payload Limits**: Inbound delivery invoices (PDF receipts scanned client-side) and high-resolution article photos (AVIF) require a request body limit of at least `50m`–`100m` (matching Dashback's `BODY_LIMIT`).
4. **Cookie Authentication**: Dashback issues HTTP-only `token` session cookies. NGINX must pass the original `Host`, `X-Real-IP`, and `X-Forwarded-Proto https` headers so Express sets and retains cookies correctly.
5. **Camera Access (HTTPS)**: Mobile barcode scanning requires a secure origin (HTTPS) and appropriate `Permissions-Policy` response headers.
---
## 3. Production Configuration (`dashfront.conf`)
Place this configuration file at `/etc/nginx/sites-available/dashfront` (or adapt it to your existing reverse proxy structure):
```nginx
# ==============================================================================
# Dashfront + Dashback Production NGINX Server Configuration
# ==============================================================================
# Map Connection header for WebSocket upgrades (if proxying dev server or WebSockets)
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
# ------------------------------------------------------------------------------
# 1. HTTP -> HTTPS Redirect
# ------------------------------------------------------------------------------
server {
listen 80;
listen [::]:80;
server_name dash.example.com; # <-- Replace with your domain or server IP
# Let's Encrypt / ACME challenge directory
location ^~ /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
# ------------------------------------------------------------------------------
# 2. Main HTTPS Server Block
# ------------------------------------------------------------------------------
server {
# NGINX >= 1.25.1 uses 'http2 on;'. For older NGINX, use: listen 443 ssl http2;
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name dash.example.com; # <-- Replace with your domain or server IP
# --- SSL / TLS Certificates ---
ssl_certificate /etc/letsencrypt/live/dash.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/dash.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
# --- Static File Root ---
# Point directly to the dist/ directory created by `npm run build`
root /home/seb/src/dashfront/dist;
index index.html;
# --- Request Body & Uploads ---
# Supports large delivery PDF uploads and client-encoded AVIF photos
client_max_body_size 100m;
client_body_timeout 300s;
# --- Compression (Gzip) ---
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 5;
gzip_min_length 256;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/javascript
application/x-javascript
application/json
application/xml
application/manifest+json
application/wasm
image/svg+xml;
# --- Security Headers ---
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Allow camera access for the built-in barcode scanner
add_header Permissions-Policy "camera=(self), microphone=(), geolocation=()" always;
# ==========================================================================
# API & Backend Routing (Dashback on port 4208)
# ==========================================================================
# Dedicated SSE route: Real-time broadcast events
location = /api/events {
proxy_pass http://127.0.0.1:4208;
proxy_http_version 1.1;
# Clear Connection header to maintain persistent HTTP/1.1 stream
proxy_set_header Connection "";
proxy_set_header Host $http_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;
# Disable buffering and caching completely for immediate push
proxy_buffering off;
proxy_cache off;
chunked_transfer_encoding off;
# Prevent premature disconnections; keep SSE open for 24 hours
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
# General API endpoints (REST)
location /api/ {
proxy_pass http://127.0.0.1:4208;
proxy_http_version 1.1;
proxy_set_header Host $http_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;
# Retain cookies across domain/path
proxy_cookie_path / /;
# Streaming uploads (PDF receipts, AVIF photos)
proxy_request_buffering off;
proxy_connect_timeout 60s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
}
# ==========================================================================
# PWA & Service Worker Rules (Zero Caching)
# ==========================================================================
# Service worker scripts MUST NEVER be cached by HTTP cache
location ~* ^/(sw\.js|registerSW\.js|workbox-[a-f0-9]+\.js)$ {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
access_log off;
}
# PWA web manifest and browser icons
location ~* ^/(manifest\.webmanifest|favicon\.svg|icons/.*)$ {
add_header Cache-Control "public, max-age=3600";
add_header Vary "Accept-Encoding";
access_log off;
}
# ==========================================================================
# Static Assets & Hashed Bundles (Aggressive Immutable Caching)
# ==========================================================================
# Vite content-hashed assets: JS, CSS, WASM, Web Workers
# (Example: /assets/index-CutpK3vd.js, /assets/avif_enc-Co4TcJko.wasm)
location /assets/ {
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Vary "Accept-Encoding";
access_log off;
}
# Ensure WebAssembly MIME type is correctly set
location ~* \.wasm$ {
default_type application/wasm;
add_header Cache-Control "public, max-age=31536000, immutable";
access_log off;
}
# ==========================================================================
# HTML5 SPA Entry Point & Navigation Fallback
# ==========================================================================
# index.html must never be cached so new release deployments take effect immediately
location = /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}
# SPA routing: Serve existing static file or directory; otherwise fallback to /index.html
location / {
try_files $uri $uri/ /index.html;
}
}
```
---
## 4. Detailed Breakdown of Critical Directives
### PWA & Cache Invalidation Strategy
PWA update lifecycles depend on byte-for-byte comparisons of `sw.js` when the user visits the page:
1. **`sw.js` and `registerSW.js`**:
```nginx
add_header Cache-Control "no-cache, no-store, must-revalidate";
```
If NGINX serves `sw.js` with a 30-day cache header, browser service worker checks will read from disk cache and never detect new application releases.
2. **`index.html`**:
Must also have `no-cache, no-store, must-revalidate`. When a new build is run, Vite produces chunks with new hashes (e.g., `index-AbCd12.js`). If `index.html` is cached, users continue loading old references that may no longer exist on disk.
3. **`/assets/`**:
Because Vite / Rolldown generates content-addressed filenames (hashes change whenever contents change), these files are completely immutable and safe to cache for 1 year (`max-age=31536000, immutable`).
### SPA Routing (`try_files`)
Dashfront uses React Router with HTML5 History API pushState:
- Direct visits to URLs like `/artikel/10001`, `/kategorien/12`, `/lieferung/neu`, or `/kunde` do not correspond to files on disk.
- The directive:
```nginx
try_files $uri $uri/ /index.html;
```
instructs NGINX to first check if a matching static file exists (e.g. `/favicon.svg` or `/assets/...`). If not found, it internally rewrites the request to `/index.html` without returning an HTTP 404, allowing React Router to resolve the route in-browser.
### Server-Sent Events (SSE) Buffering & Timeouts
The backend provides real-time model invalidation via `GET /api/events`. By default, NGINX buffers HTTP responses. If buffering is enabled:
1. Events sent by Dashback stay in NGINX's internal memory buffer until the buffer fills up (often several kilobytes), causing clients to miss instant UI updates.
2. NGINX will close idle proxy connections after `proxy_read_timeout` (default 60s).
The dedicated `/api/events` block fixes both issues:
```nginx
proxy_buffering off;
proxy_cache off;
chunked_transfer_encoding off;
proxy_set_header Connection "";
proxy_read_timeout 86400s;
```
### File Uploads & Request Body Limits
In Dashfront:
- Inbound deliveries support multi-page PDF invoices and OCR processing (`pdfconvert-wasm`).
- Article management supports client-side AVIF image generation.
- The default NGINX limit is `1m` (`client_max_body_size 1m;`), which causes HTTP 413 (Payload Too Large) errors on invoice uploads.
- Setting:
```nginx
client_max_body_size 100m;
proxy_request_buffering off;
```
allows uploads up to 100 megabytes and streams the body directly to Express without waiting for NGINX to buffer the entire file on disk.
### WebAssembly (WASM) MIME Types
Dashfront uses WebAssembly for:
- `@jsquash/avif` (image encoding)
- `@jsquash/resize` (squoosh resizing)
- `pdfconvert-wasm` (PDF page extraction and rasterization)
Modern browsers require the HTTP header `Content-Type: application/wasm` for streaming compilation (`WebAssembly.instantiateStreaming`). If missing, NGINX may default to `application/octet-stream`. The WASM block guarantees the correct MIME header:
```nginx
location ~* \.wasm$ {
default_type application/wasm;
}
```
### Camera & Hardware Permissions
The article lookup and delivery receipt features allow scanning barcodes using device cameras. Web browsers enforce two strict rules for camera access:
1. **Secure Origin**: The page must be served over HTTPS or `localhost`. Plain HTTP over local IP addresses (e.g., `http://192.168.1.100/`) will automatically disable `navigator.mediaDevices`.
2. **Permissions Policy**: The header `Permissions-Policy "camera=(self)"` ensures the browser allows Dashfront to access connected webcams or smartphone cameras.
---
## 5. Alternative: Remote Vite Dev Server Proxy
If you want to access the live **Vite development server** (running on `localhost:4209` with HMR) through NGINX from a smartphone, tablet, or external domain:
```nginx
# Map connection upgrade for Vite HMR WebSockets
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 443 ssl;
server_name dev-dash.example.com;
ssl_certificate /etc/letsencrypt/live/dev-dash.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/dev-dash.example.com/privkey.pem;
client_max_body_size 100m;
# 1. API Calls -> Dashback (Port 4208)
location /api/ {
proxy_pass http://127.0.0.1:4208;
proxy_http_version 1.1;
proxy_set_header Host $http_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;
proxy_buffering off;
proxy_read_timeout 86400s;
}
# 2. Vite Dev Server & HMR -> Vite (Port 4209)
location / {
proxy_pass http://127.0.0.1:4209;
proxy_http_version 1.1;
# Crucial for Vite Hot Module Replacement (HMR) WebSocket
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $http_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;
proxy_read_timeout 86400s;
}
}
```
> [!NOTE]
> When proxying to Vite dev server, ensure `server.allowedHosts = true` or add your domain to `allowedHosts` in [vite.config.js](file:///home/seb/src/dashfront/vite.config.js).
---
## 6. Deployment & Operations Runbook
### Step 1: Build Dashfront for Production
On the development machine or build server:
```bash
cd /home/seb/src/dashfront
npm run build
```
This compiles optimized bundles and service worker files into `dashfront/dist`.
### Step 2: Install and Verify NGINX Configuration
```bash
# 1. Copy config file to sites-available
sudo cp deploy/nginx-dashfront.conf /etc/nginx/sites-available/dashfront
# 2. Enable site by creating a symlink
sudo ln -s /etc/nginx/sites-available/dashfront /etc/nginx/sites-enabled/
# 3. Test configuration syntax
sudo nginx -t
# 4. Reload NGINX without dropping active connections
sudo systemctl reload nginx
```
### Step 3: Obtain TLS Certificates with Certbot
```bash
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d dash.example.com
```
### Step 4: Ensure Filesystem Permissions
NGINX worker processes run as the `www-data` user by default. Ensure the directory path to `dist/` is readable:
```bash
# Allow www-data execution on parent path components
chmod 755 /home/seb /home/seb/src /home/seb/src/dashfront /home/seb/src/dashfront/dist
```
Or deploy build artifacts into a standard web directory:
```bash
sudo mkdir -p /var/www/dashfront
sudo rsync -av --delete /home/seb/src/dashfront/dist/ /var/www/dashfront/dist/
sudo chown -R www-data:www-data /var/www/dashfront
```
---
## 7. Troubleshooting Matrix
| Symptom | Cause | Solution |
|---|---|---|
| **HTTP 404 on page reload** on sub-routes (`/artikel`, `/kunde`) | Missing SPA fallback in NGINX | Ensure `try_files $uri $uri/ /index.html;` is present in `location /`. |
| **PWA does not update** after deployment | `sw.js` or `index.html` cached in browser | Verify `Cache-Control: no-cache, no-store, must-revalidate` on `sw.js`, `registerSW.js`, and `index.html`. |
| **HTTP 413 (Payload Too Large)** when uploading invoice PDF or image | Default NGINX upload limit is 1MB | Add `client_max_body_size 100m;` to the `server` or `location /api/` block. |
| **Live SSE events disconnect every 60s** | Default `proxy_read_timeout` timeout | Set `proxy_read_timeout 86400s;` and `proxy_buffering off;` on `/api/events`. |
| **Login loop / Cookie not sent** | Secure cookie rejected on plain HTTP, or missing proxy headers | Access via HTTPS; ensure `proxy_set_header Host $http_host;` and `proxy_set_header X-Forwarded-Proto $scheme;` are set. |
| **Barcode camera scanner disabled** | Page accessed over insecure HTTP or missing permissions policy | Use HTTPS; check that `Permissions-Policy "camera=(self)"` header is present. |
| **WASM compilation error** (`incorrect response MIME type`) | NGINX serving `.wasm` as `application/octet-stream` | Add `default_type application/wasm;` for `\.wasm$` extensions. |
| **Vite HMR disconnects immediately** (Dev proxy) | Missing WebSocket upgrade headers | Ensure `proxy_set_header Upgrade $http_upgrade;` and `proxy_set_header Connection $connection_upgrade;` are configured. |