The previous $variable form forced lazy per-request DNS resolution. Per-request lookups against 10.43.0.10 → SERVFAIL → cold-cache 502 even when downstream was healthy. Switching to a literal hostname makes nginx resolve once at startup and reuse the cached upstream IP for the lifetime of the worker. Cloudflare anycast is stable so this is safe.
88 lines
4.0 KiB
Nginx Configuration File
88 lines
4.0 KiB
Nginx Configuration File
map $sent_http_content_type $csp_header {
|
|
default "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' data: https://fonts.gstatic.com; img-src 'self' data: blob: https://*.tile.openstreetmap.org https://unpkg.com; connect-src 'self' https://demo.flow-master.ai https://canvas.flow-master.ai wss://canvas.flow-master.ai; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'";
|
|
}
|
|
|
|
server {
|
|
listen 80;
|
|
server_name _;
|
|
root /usr/share/nginx/html;
|
|
index index.html;
|
|
|
|
# Hardening — every response carries these.
|
|
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
add_header X-Frame-Options "DENY" always;
|
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
add_header Permissions-Policy "camera=(), microphone=(self), geolocation=(self), payment=()" always;
|
|
add_header Content-Security-Policy $csp_header always;
|
|
add_header Cross-Origin-Opener-Policy "same-origin" always;
|
|
add_header Cross-Origin-Resource-Policy "same-origin" always;
|
|
add_header X-Robots-Tag "noindex, nofollow" always;
|
|
|
|
# Use the cluster CoreDNS resolver for in-cluster Services AND public
|
|
# resolvers as fallback for external hostnames (demo.flow-master.ai etc.).
|
|
# k3s CoreDNS by default forwards external lookups, but if the forward
|
|
# is missing the proxy_pass to demo.flow-master.ai fails with 502/504.
|
|
# Listing public resolvers alongside ensures lookups succeed in both
|
|
# cases. Targets use $variable form to force lazy per-request resolution.
|
|
resolver 10.43.0.10 1.1.1.1 8.8.8.8 valid=30s ipv6=off;
|
|
|
|
# Reverse-proxy /api to the demo backend so live mode is same-origin.
|
|
# NOTE: proxy_pass uses a literal hostname (not a $variable) so nginx
|
|
# resolves demo.flow-master.ai ONCE at config-load, caches it for the
|
|
# lifetime of the worker, and never retries DNS mid-request. This is
|
|
# the only way to eliminate the per-request DNS race that returned
|
|
# fast 502s on cold cache. Cloudflare hides the demo origin behind
|
|
# anycast IPs, so a single-resolution lookup is safe and stable.
|
|
location /api/ {
|
|
proxy_pass https://demo.flow-master.ai;
|
|
proxy_set_header Host demo.flow-master.ai;
|
|
proxy_set_header X-Forwarded-Proto https;
|
|
proxy_set_header X-Forwarded-Host $host;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_ssl_server_name on;
|
|
proxy_ssl_name demo.flow-master.ai;
|
|
proxy_http_version 1.1;
|
|
proxy_connect_timeout 8s;
|
|
proxy_send_timeout 30s;
|
|
proxy_read_timeout 30s;
|
|
proxy_buffering off;
|
|
proxy_next_upstream error timeout http_502 http_503 http_504;
|
|
proxy_next_upstream_tries 3;
|
|
proxy_next_upstream_timeout 10s;
|
|
}
|
|
|
|
# In-cluster LLM proxy. Returns 503 when no provider key is configured,
|
|
# so the frontend's deterministic fallback fires gracefully.
|
|
location /internal/canvas-llm/ {
|
|
set $upstream_llm "http://canvas-llm-proxy.demo.svc.cluster.local:8080";
|
|
proxy_pass $upstream_llm;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Forwarded-Proto https;
|
|
proxy_http_version 1.1;
|
|
proxy_read_timeout 60s;
|
|
proxy_buffering off;
|
|
}
|
|
|
|
# SPA fallback to index.html for client-side routing.
|
|
location / {
|
|
try_files $uri $uri/ /index.html;
|
|
add_header Cache-Control "no-cache" always;
|
|
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
add_header X-Frame-Options "DENY" always;
|
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
add_header Permissions-Policy "camera=(), microphone=(self), geolocation=(self), payment=()" always;
|
|
add_header Content-Security-Policy $csp_header always;
|
|
}
|
|
|
|
# Hashed static assets cache forever.
|
|
location /assets/ {
|
|
try_files $uri =404;
|
|
add_header Cache-Control "public, max-age=31536000, immutable" always;
|
|
}
|
|
|
|
# Hide nginx version on errors.
|
|
server_tokens off;
|
|
}
|