Production image for the frontend dist.
Stage 1 (build): node:22-alpine, `npm ci --ignore-scripts` from the
committed lockfile, `npm run build`. Output lands in /app/dist.
Stage 2 (runtime): docker.io/nginxinc/nginx-unprivileged:alpine.
- Upstream-maintained variant that runs as the nginx user (uid 101)
out of the box. /var/cache/nginx and /var/run/nginx are pre-owned,
no chown gymnastics needed in our layer. Vanilla nginx:alpine fails
at startup as non-root because client_temp mkdir is denied.
- Listens on 8080 (non-privileged port, matches the unprivileged
variant convention).
- nginx.conf serves /usr/share/nginx/html with SPA `try_files`
fallback for client-side routing, long-cache headers on
/assets/ (Vite hashed bundles), a plaintext /healthz endpoint
for Caddy / Prometheus blackbox, and server_tokens off.
.dockerignore excludes node_modules, dist, .vite, coverage,
playwright-report, .env*, .git, editor dirs. Keeps .env.example.
Smoke local validated with `podman build -t mimic-frontend:smoke .`
and `podman run -p 127.0.0.1:18080:8080`:
/healthz -> 200 "ok"
/ -> 200 index.html (508 B)
/spa/x -> 200 (SPA fallback)
/assets -> Cache-Control: max-age=31536000, public, immutable