21 KiB
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
- Architecture & Request Flow
- Key Technical Requirements
- Production Configuration (
dashfront.conf) - Detailed Breakdown of Critical Directives
- Alternative: Remote Vite Dev Server Proxy
- Deployment & Operations Runbook
- 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
- PWA Lifecycles: A Progressive Web App checks for updates by fetching
sw.js. If NGINX cachessw.jsorindex.html, client devices can remain stuck on obsolete bundles indefinitely. - SSE Streaming: Real-time push updates at
/api/eventsrequire NGINX proxy buffering and proxy caching to be turned OFF, with a 24-hour read timeout to prevent arbitrary 60-second connection drops. - 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'sBODY_LIMIT). - Cookie Authentication: Dashback issues HTTP-only
tokensession cookies. NGINX must pass the originalHost,X-Real-IP, andX-Forwarded-Proto httpsheaders so Express sets and retains cookies correctly. - Camera Access (HTTPS): Mobile barcode scanning requires a secure origin (HTTPS) and appropriate
Permissions-Policyresponse 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):
# ==============================================================================
# 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:
sw.jsandregisterSW.js:If NGINX servesadd_header Cache-Control "no-cache, no-store, must-revalidate";sw.jswith a 30-day cache header, browser service worker checks will read from disk cache and never detect new application releases.index.html: Must also haveno-cache, no-store, must-revalidate. When a new build is run, Vite produces chunks with new hashes (e.g.,index-AbCd12.js). Ifindex.htmlis cached, users continue loading old references that may no longer exist on disk./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/kundedo not correspond to files on disk. - The directive:
instructs NGINX to first check if a matching static file exists (e.g.
try_files $uri $uri/ /index.html;/favicon.svgor/assets/...). If not found, it internally rewrites the request to/index.htmlwithout 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:
- 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.
- NGINX will close idle proxy connections after
proxy_read_timeout(default 60s).
The dedicated /api/events block fixes both issues:
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:
allows uploads up to 100 megabytes and streams the body directly to Express without waiting for NGINX to buffer the entire file on disk.
client_max_body_size 100m; proxy_request_buffering off;
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:
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:
- 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 disablenavigator.mediaDevices. - 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:
# 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 = trueor add your domain toallowedHostsin vite.config.js.
6. Deployment & Operations Runbook
Step 1: Build Dashfront for Production
On the development machine or build server:
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
# 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
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:
# 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:
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. |