# 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;"]