375bc64f2f
Backend: Alpine runtime without Chromium by default, opt-in Debian scraper target with Playwright. Frontend: Vite build served by nginx-unprivileged with the API upstream resolved at start. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
91 lines
4.0 KiB
Docker
91 lines
4.0 KiB
Docker
# syntax=docker/dockerfile:1.7
|
|
#
|
|
# Föräldraledigplannerare — frontend (Vite build, served by nginx-unprivileged).
|
|
#
|
|
# Build context: frontend/
|
|
#
|
|
# podman build -t foraldraledig-frontend frontend/
|
|
# podman run -p 8080:8080 -e API_UPSTREAM=http://backend:8000 foraldraledig-frontend
|
|
#
|
|
# ---------------------------------------------------------------------------
|
|
# NO BUILD-TIME API URL — AND NONE IS NEEDED
|
|
#
|
|
# src/services/api.ts uses `const BASE = '/api'`: every call is same-origin and
|
|
# relative. Nothing about the backend location is baked into the bundle by
|
|
# Vite, so the usual `import.meta.env.VITE_API_URL` placeholder-substitution
|
|
# dance does not apply here. Instead this image *is* the origin: nginx serves
|
|
# the static bundle and reverse-proxies /api to the backend, with the upstream
|
|
# taken from the ${API_UPSTREAM} env var at container start (nginx's own
|
|
# envsubst templating, /etc/nginx/templates). One knob, resolved at runtime,
|
|
# and the default points at a compose/pod service name rather than localhost.
|
|
#
|
|
# Caveat: nginx resolves the proxy_pass host once, at startup. If the backend
|
|
# name is not resolvable yet, nginx exits — start the backend first, or give
|
|
# the frontend a restart policy.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# ===========================================================================
|
|
# Stage 1 — build the static bundle
|
|
# ===========================================================================
|
|
FROM node:22-alpine AS build
|
|
|
|
WORKDIR /src
|
|
|
|
# Lockfile first: dependency installs stay cached across source edits.
|
|
COPY package.json package-lock.json ./
|
|
# `npm ci` = exact, reproducible install from package-lock.json.
|
|
RUN npm ci
|
|
|
|
COPY . .
|
|
|
|
# `npm run build` is `tsc && vite build` — the type check is part of the
|
|
# build, so a type error fails the image instead of shipping.
|
|
RUN npm run build
|
|
|
|
|
|
# ===========================================================================
|
|
# Stage 2 — runtime
|
|
# ===========================================================================
|
|
#
|
|
# Why nginxinc/nginx-unprivileged:
|
|
# * Already non-root (uid 101) and listens on 8080, so no capability juggling
|
|
# and no USER override needed to satisfy a restricted PodSecurity policy.
|
|
# * Alpine variant is ~20 MB, i.e. in the same class as Caddy/distroless
|
|
# while being the most boring, most audited static server in existence.
|
|
# * Ships the envsubst template entrypoint, which is exactly what the
|
|
# runtime ${API_UPSTREAM} substitution above needs — a distroless static
|
|
# binary would have meant hand-rolling that.
|
|
FROM nginxinc/nginx-unprivileged:1.29-alpine AS runtime
|
|
|
|
# Tag-pinned, not digest-pinned: rebuilds should pick up nginx/Alpine CVE
|
|
# fixes. The *content* served is already pinned by package-lock.json. Pin the
|
|
# digest in the deployment pipeline if immutability outranks patching.
|
|
|
|
ENV API_UPSTREAM=http://backend:8000
|
|
|
|
# Template, not a plain conf: the stock entrypoint runs envsubst over
|
|
# /etc/nginx/templates/*.template into /etc/nginx/conf.d/ on start.
|
|
COPY nginx.conf.template /etc/nginx/templates/default.conf.template
|
|
|
|
COPY --from=build --chown=101:101 /src/dist /usr/share/nginx/html
|
|
|
|
USER 101
|
|
EXPOSE 8080
|
|
|
|
# busybox wget — already present, no extra package.
|
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
|
CMD ["wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8080/healthz"]
|
|
|
|
# The base image's ENTRYPOINT (docker-entrypoint.sh) execs nginx, so nginx is
|
|
# PID 1 and gets SIGTERM/SIGQUIT directly. Inherited CMD ["nginx", "-g",
|
|
# "daemon off;"] is correct as-is.
|
|
#
|
|
# Read-only root filesystem (verified working) — run with:
|
|
# --read-only --tmpfs /tmp --tmpfs /var/cache/nginx \
|
|
# --tmpfs /etc/nginx/conf.d:rw,mode=1777
|
|
# /etc/nginx/conf.d is where envsubst renders the template, so it must be
|
|
# writable BY UID 101 — podman's default tmpfs mode is not, and the entrypoint
|
|
# then silently skips the substitution and nginx serves no server block at all.
|
|
# mode=1777 is what makes it work. /tmp holds the pid file and proxy buffers.
|