467 lines
21 KiB
Markdown
467 lines
21 KiB
Markdown
# 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. |
|