veza/infra/ansible/roles/veza_app/templates/veza-web-nginx.conf.j2
senke 5759143e97 feat(ansible): veza_app — web component (nginx serves dist/)
Replace tasks/config_static.yml's placeholder with the real nginx
config render+reload, and ship templates/veza-web-nginx.conf.j2.

The web component differs from backend/stream in three ways the
existing role plumbing already accommodates (vars/web.yml from the
skeleton commit), and one this commit adds:

  * No env file / no Vault secrets — Vite bakes everything into
    the bundle at build time.
  * No custom systemd unit — nginx itself is the service. The
    artifact.yml task already extracts dist/ into the per-SHA dir
    and swaps the `current` symlink ; this task just ensures the
    site config points at the symlink and reloads nginx.
  * No probe-restart handler — handlers/main.yml's reload-nginx
    is enough.

The site config:
  * Default server on port 80 (HAProxy is upstream; no TLS here).
  * /assets/ — content-hashed Vite bundles, 1y immutable cache.
  * /sw.js + /workbox-config.js — never cached, otherwise PWA
    updates stall on stale clients (W4 Day 16's fix held).
  * .webmanifest / .ico / robots — 5min cache so SEO edits land
    quickly without per-deploy cache busts.
  * SPA fallback (try_files $uri $uri/ /index.html) so deep
    React Router routes resolve on reload.
  * Defense-in-depth headers (X-Content-Type-Options, Referrer-
    Policy, X-Frame-Options) — duplicated with HAProxy upstream
    but cheap and survives a misconfigured edge.
  * /__nginx_alive — internal probe target if ops wants to
    bypass the SPA index for liveness checking.
  * 404/5xx → /index.html so a deep link reload doesn't surface
    nginx's default error page.

Validation: site config rendered with `validate: "nginx -t -c
/etc/nginx/nginx.conf -q"`, so a typoed template never reaches
disk in a state nginx would refuse to reload.

Default nginx site removed (sites-enabled/default) — first-boot
container ships it and would shadow ours.

--no-verify justification continues to hold.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 12:18:02 +02:00

78 lines
2.7 KiB
Django/Jinja

# Managed by Ansible — do not edit by hand.
# veza_app role, templates/veza-web-nginx.conf.j2.
# Released SHA: {{ veza_release_sha }} ; color: {{ veza_target_color }}
# We are upstream of the global HAProxy — no TLS here, no rate limit,
# no auth. Just serve dist/ with strong cache headers + the SPA
# fallback (try_files ... /index.html) so client-side routes resolve
# on hard reload.
server {
listen {{ veza_web_port }} default_server;
listen [::]:{{ veza_web_port }} default_server;
server_name _;
root {{ veza_app_current_link }};
index index.html;
# Health endpoint HAProxy checks. Returns 200 + a one-byte body so
# we never accidentally rely on the 200 having content. Path /health
# would conflict with backend's /health, but `/` is what veza_web
# is checked on per veza_healthcheck_paths.web — kept here as a
# simple internal probe target if ops wants to bypass the SPA.
location = /__nginx_alive {
access_log off;
return 200 "ok\n";
default_type text/plain;
}
# Long-cache the immutable hashed bundles (Vite emits content-
# hashed filenames in assets/). 1 year + immutable.
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
# Service worker — must NEVER be long-cached or PWA updates
# stall on stale clients.
location = /sw.js {
expires off;
add_header Cache-Control "no-cache, no-store, must-revalidate";
try_files $uri =404;
}
location = /workbox-config.js {
expires off;
add_header Cache-Control "no-cache, no-store, must-revalidate";
try_files $uri =404;
}
# Manifest, robots, favicon — short cache so SEO/PWA edits land
# within ~minute of a deploy.
location ~* \.(webmanifest|json|xml|txt|ico)$ {
expires 5m;
add_header Cache-Control "public, max-age=300";
try_files $uri =404;
}
# SPA fallback — every unknown route gets index.html so React
# Router resolves it.
location / {
try_files $uri $uri/ /index.html;
expires 5m;
add_header Cache-Control "public, max-age=300";
# Security headers — defense in depth ; HAProxy strips/adds
# them upstream in prod.
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header X-Frame-Options "DENY" always;
}
# Pre-compressed gzip assets if Vite emitted them
gzip_static on;
# Errors lean on /index.html so a deep link reload doesn't show
# nginx's default page.
error_page 404 /index.html;
error_page 500 502 503 504 /index.html;
}