# 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.