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

21 KiB
Raw Permalink Blame History

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
  2. Key Technical Requirements
  3. Production Configuration (dashfront.conf)
  4. Detailed Breakdown of Critical Directives
  5. Alternative: Remote Vite Dev Server Proxy
  6. Deployment & Operations Runbook
  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):

# ==============================================================================
# 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:
    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:
    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:

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:
    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:

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:

# 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.


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.