chore(frontend): add multi-stage Dockerfile + nginx SPA config
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
This commit is contained in:
22
frontend/.dockerignore
Normal file
22
frontend/.dockerignore
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.vite
|
||||||
|
coverage
|
||||||
|
playwright-report
|
||||||
|
test-results
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Editor / OS
|
||||||
|
.DS_Store
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
|
||||||
|
# Env (never bake into image)
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Git internals
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.gitattributes
|
||||||
37
frontend/Dockerfile
Normal file
37
frontend/Dockerfile
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# syntax=docker/dockerfile:1.7
|
||||||
|
|
||||||
|
# --- Stage 1: build --------------------------------------------------------
|
||||||
|
FROM node:22-alpine AS build
|
||||||
|
|
||||||
|
ENV CI=true \
|
||||||
|
npm_config_audit=false \
|
||||||
|
npm_config_fund=false
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Reproducible install: lockfile only, no scripts at install time.
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm ci --ignore-scripts
|
||||||
|
|
||||||
|
# App sources.
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Production build → /app/dist
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# --- Stage 2: runtime ------------------------------------------------------
|
||||||
|
# nginxinc/nginx-unprivileged is the upstream-maintained variant that runs
|
||||||
|
# nginx as a non-root user out of the box (no chown gymnastics, /var/cache
|
||||||
|
# /var/run/nginx are owned by uid 101). It already listens on 8080.
|
||||||
|
FROM docker.io/nginxinc/nginx-unprivileged:alpine
|
||||||
|
|
||||||
|
# Minimal SPA serving config: static dist + try_files SPA fallback.
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
# Static assets owned by the nginx user (uid 101 in this image).
|
||||||
|
COPY --from=build --chown=101:101 /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Foreground nginx so the container lifecycle matches.
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
30
frontend/nginx.conf
Normal file
30
frontend/nginx.conf
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
server {
|
||||||
|
listen 8080;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# SPA routing: every unknown path falls back to index.html.
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Long-cache hashed asset bundles emitted by Vite under /assets/.
|
||||||
|
location /assets/ {
|
||||||
|
access_log off;
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Health endpoint scraped by Caddy / Prometheus blackbox.
|
||||||
|
location = /healthz {
|
||||||
|
access_log off;
|
||||||
|
default_type text/plain;
|
||||||
|
return 200 "ok\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
# No directory listing, no server tokens.
|
||||||
|
autoindex off;
|
||||||
|
server_tokens off;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user