diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..6d5d268 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,10 @@ +node_modules/ +dist/ +build/ +*.tsbuildinfo +coverage/ +src/**/*.test.ts +src/**/*.test.tsx +src/test/ +.git/ +.github/ diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..6035fb6 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,33 @@ +# syntax=docker/dockerfile:1.7 +# +# Cyclone frontend — React SPA built with node:20-alpine and served by +# nginx:1.27-alpine. nginx reverse-proxies /api/* to the backend service +# over the compose-managed bridge network. + +# ---------- builder ---------- +FROM node:20-alpine AS builder + +WORKDIR /build + +# Install deps first so this layer caches across source edits. +COPY package.json package-lock.json* ./ +RUN npm ci + +# Build the production bundle into dist/. +COPY . . +RUN npm run build + +# ---------- runtime ---------- +FROM nginx:1.27-alpine + +# Replace the default nginx site with ours (SPA + reverse proxy). +RUN rm -f /etc/nginx/conf.d/default.conf + +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=builder /build/dist /usr/share/nginx/html + +# wget is on busybox; nginx:alpine doesn't ship curl. +HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ + CMD wget -qO- http://localhost:8080/ >/dev/null || exit 1 + +EXPOSE 8080 diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..ee0d72d --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,41 @@ +server { + listen 8080; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + # Long-lived connections for the live-tail NDJSON streams + # (snapshot + live events can hold open for minutes at a time). + proxy_read_timeout 300s; + proxy_send_timeout 300s; + + # Claims files can be large; 50 MiB matches what FastAPI accepts. + client_max_body_size 50m; + + # API + auth: proxy to backend over the compose-managed bridge network. + location /api/ { + proxy_pass http://cyclone-backend:8000; + proxy_set_header Host $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; + # The backend sets the cookie with Path=/api/. Rewrite to / so + # the browser sends it on every subsequent request to the SPA + # (which never has a /api/ prefix on the URL). + proxy_cookie_path /api/ /; + } + + # SPA fallback: serve index.html for any non-/api route so deep-links + # to /claims/123, /admin/users, etc. round-trip through reload. + location / { + try_files $uri $uri/ /index.html; + } + + # Don't cache index.html — the SPA references hashed asset filenames, + # so index.html is the only file that needs to be revalidated. + location = /index.html { + add_header Cache-Control "no-cache, no-store, must-revalidate" always; + expires off; + } +}