user nginx; worker_processes auto; error_log /dev/stderr warn; pid /var/run/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; access_log /dev/stdout; sendfile on; tcp_nopush on; keepalive_timeout 65; server_tokens off; # Compression: takes the main JS bundle from ~1.9 MB down to ~530 kB on the wire gzip on; gzip_vary on; gzip_proxied any; gzip_comp_level 6; gzip_min_length 1024; gzip_types text/plain text/css text/xml text/javascript application/javascript application/json application/xml image/svg+xml font/woff font/woff2; server { # 80 is the conventional port; 3000 is what Dokploy targets by default in # its domain config. Binding both means Traefik reaches the app whichever # one the dashboard's Port field is set to, so a deploy can't 502 purely # because those two numbers disagree. listen 80; listen 3000; server_name _; root /usr/share/nginx/html; index index.html; # Container liveness for Dokploy/Docker. Kept above the SPA fallback so it # answers with a real status instead of 200-plus-index.html, which would # make any health check pass even with the app completely broken. location = /health { access_log off; add_header Content-Type text/plain; return 200 "ok\n"; } # Vite writes content-hashed filenames here, so they can be cached forever. # Only add_header is used — combining it with `expires` emits two # conflicting Cache-Control headers. # # try_files ending in =404 is deliberate: without it a stale index.html # asking for an asset from a previous deploy would fall through to the # SPA rule below and receive index.html with a 200 and a text/html type. # The browser refuses to execute that as a module and the page renders # empty with only a MIME error in the console — a real 404 is far easier # to diagnose and lets a reload recover. location /assets/ { add_header Cache-Control "public, max-age=31536000, immutable"; access_log off; try_files $uri =404; } # index.html must be revalidated every time or deploys won't take effect location = /index.html { add_header Cache-Control "no-cache"; } location / { # This line forces Nginx to pass routing back to React Router try_files $uri $uri/ /index.html; # The fallback is served under the requested path, so the `= /index.html` # rule above never applies to it. Without this a proxy or browser can # cache the old HTML and keep loading assets that no longer exist. add_header Cache-Control "no-cache"; } } }