wip: dashboard wiring (real backend) + Docker compose for production
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
# Build-time-only exclusions for the frontend image.
|
||||
**/node_modules
|
||||
**/dist
|
||||
**/.vite
|
||||
**/.cache
|
||||
**/.eslintcache
|
||||
**/coverage
|
||||
docs/
|
||||
backend/
|
||||
tests/
|
||||
*.test.ts
|
||||
*.test.tsx
|
||||
**/*.test.*
|
||||
@@ -0,0 +1,57 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
# Cyclone frontend — Vite/React SPA built once and served by nginx.
|
||||
#
|
||||
# Multi-stage so node_modules + build cache (~hundreds of MB) don't ship
|
||||
# in the runtime image. The final image is nginx:alpine serving the
|
||||
# pre-built static bundle, with /api/* reverse-proxied to the backend
|
||||
# service over the compose network.
|
||||
#
|
||||
# Build context is the repo root (../) so package.json / vite config /
|
||||
# src/ are all reachable from the docker-compose `dockerfile:` directive.
|
||||
|
||||
# ---- build stage --------------------------------------------------------
|
||||
FROM node:20-alpine AS build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy manifests first so dependency layer caches when only source changes.
|
||||
COPY package.json package-lock.json* /app/
|
||||
RUN npm ci --no-audit --no-fund
|
||||
|
||||
# Source + build configs. VITE_API_BASE_URL is left empty so the SPA
|
||||
# calls /api/* on the same origin (handled by nginx in the runtime stage).
|
||||
COPY index.html postcss.config.js tailwind.config.js \
|
||||
tsconfig.json tsconfig.app.json tsconfig.node.json \
|
||||
vite.config.ts /app/
|
||||
COPY public /app/public
|
||||
COPY src /app/src
|
||||
|
||||
ENV NODE_ENV=production \
|
||||
VITE_API_BASE_URL=
|
||||
|
||||
RUN npm run build
|
||||
|
||||
# ---- runtime stage ------------------------------------------------------
|
||||
FROM nginx:1.27-alpine AS runtime
|
||||
|
||||
# Drop the default nginx site and ship ours (SPA fallback + /api proxy).
|
||||
RUN rm /etc/nginx/conf.d/default.conf
|
||||
COPY frontend/nginx.conf /etc/nginx/conf.d/cyclone.conf
|
||||
|
||||
# Static bundle from the build stage.
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
|
||||
# nginx:alpine ships with a non-root `nginx` user (UID 101). We stay as
|
||||
# root only long enough to write the pid + cache dirs, then exec under
|
||||
# that user. This avoids permission issues with mounted volumes.
|
||||
RUN touch /var/run/nginx.pid \
|
||||
&& chown -R nginx:nginx /var/run/nginx.pid /var/cache/nginx /usr/share/nginx/html
|
||||
|
||||
USER nginx
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
HEALTHCHECK --interval=15s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD wget --quiet --tries=1 --spider http://127.0.0.1/ || exit 1
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -0,0 +1,84 @@
|
||||
# Cyclone frontend — nginx site config.
|
||||
#
|
||||
# Two responsibilities:
|
||||
# 1. Serve the built SPA bundle from /usr/share/nginx/html with a
|
||||
# fallback to index.html for client-side routing.
|
||||
# 2. Reverse-proxy /api/* to the backend service so the SPA can call
|
||||
# relative URLs (VITE_API_BASE_URL empty) without CORS round-trips.
|
||||
#
|
||||
# `backend` resolves via Docker's user-defined network (compose service
|
||||
# name). `chunked_transfer_encoding off` keeps SSE/NDJSON streams flowing
|
||||
# instead of being buffered by nginx — uvicorn already streams, but the
|
||||
# explicit override makes the intent obvious and survives future nginx
|
||||
# default changes.
|
||||
|
||||
upstream cyclone_backend {
|
||||
server backend:8000;
|
||||
keepalive 16;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Reasonable defaults for a single-operator dashboard.
|
||||
client_max_body_size 25m;
|
||||
|
||||
# gzip JS/CSS/JSON/SVG so the initial bundle loads faster.
|
||||
gzip on;
|
||||
gzip_comp_level 5;
|
||||
gzip_min_length 256;
|
||||
gzip_proxied any;
|
||||
gzip_vary on;
|
||||
gzip_types
|
||||
application/javascript
|
||||
application/json
|
||||
application/xml
|
||||
image/svg+xml
|
||||
text/css
|
||||
text/plain;
|
||||
|
||||
# Cache fingerprinted assets aggressively; index.html stays fresh.
|
||||
location ~* \.(?:js|css|woff2?|ttf|otf|eot|svg|png|jpg|jpeg|webp|ico)$ {
|
||||
try_files $uri =404;
|
||||
expires 7d;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# /api/* → backend. Don't buffer so live-tail NDJSON streams flow.
|
||||
location /api/ {
|
||||
proxy_pass http://cyclone_backend;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
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;
|
||||
proxy_set_header Connection "";
|
||||
|
||||
# Keep idle NDJSON streams alive across nginx's default 60s.
|
||||
proxy_read_timeout 1h;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
chunked_transfer_encoding on;
|
||||
add_header X-Accel-Buffering no;
|
||||
}
|
||||
|
||||
# SPA fallback — anything that isn't a real file in / serves index.html
|
||||
# so client-side routes (/inbox, /claims/:id) don't 404 on refresh.
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Lightweight health probe for orchestrators that prefer /healthz over
|
||||
# the static index.
|
||||
location = /healthz {
|
||||
access_log off;
|
||||
add_header Content-Type text/plain;
|
||||
return 200 "ok\n";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user