Build production-ready multi-stage Dockerfile for the frontend #135

Open
opened 2026-03-28 08:51:51 +01:00 by marcel · 0 comments
Owner

Why

frontend/Dockerfile currently runs npm run dev (Vite hot-reload dev server) against a bind-mounted source tree. This means:

  • The container cannot run without the host filesystem — it has no built output inside it.
  • It serves the app via Vite's development server, which is not designed for production traffic (no caching, no compression, no stability guarantees under load).
  • The image includes all devDependencies (~400 MB) rather than just the production runtime.
  • It is unsuitable for any deployment beyond local dev.

What to do

Replace frontend/Dockerfile with a two-stage build. The project uses SvelteKit with @sveltejs/adapter-node, so the build output is a Node.js server that can be run with node build/index.js.

Stage 1 — Build (Node + all dependencies): install deps, run npm run build, produce the build/ directory.

Stage 2 — Runtime (Node slim): copy only build/ and the production node_modules, nothing else.

# Stage 1: build
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci                  # installs dev + prod deps needed for build
COPY . .
RUN npm run build

# Stage 2: runtime
FROM node:20-alpine AS runtime
WORKDIR /app
COPY --from=build /app/build ./build
COPY --from=build /app/package*.json ./
RUN npm ci --omit=dev       # install only production deps
EXPOSE 3000
ENV PORT=3000
ENV HOST=0.0.0.0
ENTRYPOINT ["node", "build/index.js"]

Environment variables at runtime

SvelteKit's Node adapter reads these at startup — they must be injected via Docker environment, not baked into the image:

Variable Purpose
API_INTERNAL_URL SSR calls from server to backend (e.g. http://backend:8080)
PORT Port the Node server listens on (default 3000)
HOST Bind address (must be 0.0.0.0 in Docker)
ORIGIN Full public URL (e.g. https://familienarchiv.example.com) — required by SvelteKit for CSRF protection

ORIGIN is easy to forget and causes cryptic CSRF errors if missing. Document it in .env.example.

Additional hardening

  • Run as non-root: add RUN addgroup -S app && adduser -S app -G app and USER app in the runtime stage.
  • Do not copy .env or any secrets into the image — all config comes from the runtime environment.

Acceptance criteria

  • docker build -t familienarchiv-frontend . from frontend/ produces a self-contained image.
  • docker run -e API_INTERNAL_URL=... -e ORIGIN=... familienarchiv-frontend starts and serves the app.
  • Final image size is under 200 MB.
  • No source files or devDependencies in the final image layers.
## Why `frontend/Dockerfile` currently runs `npm run dev` (Vite hot-reload dev server) against a bind-mounted source tree. This means: - The container **cannot run without the host filesystem** — it has no built output inside it. - It serves the app via Vite's development server, which is not designed for production traffic (no caching, no compression, no stability guarantees under load). - The image includes all devDependencies (~400 MB) rather than just the production runtime. - It is **unsuitable for any deployment** beyond local dev. ## What to do Replace `frontend/Dockerfile` with a two-stage build. The project uses SvelteKit with `@sveltejs/adapter-node`, so the build output is a Node.js server that can be run with `node build/index.js`. **Stage 1 — Build** (Node + all dependencies): install deps, run `npm run build`, produce the `build/` directory. **Stage 2 — Runtime** (Node slim): copy only `build/` and the production `node_modules`, nothing else. ```dockerfile # Stage 1: build FROM node:20-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm ci # installs dev + prod deps needed for build COPY . . RUN npm run build # Stage 2: runtime FROM node:20-alpine AS runtime WORKDIR /app COPY --from=build /app/build ./build COPY --from=build /app/package*.json ./ RUN npm ci --omit=dev # install only production deps EXPOSE 3000 ENV PORT=3000 ENV HOST=0.0.0.0 ENTRYPOINT ["node", "build/index.js"] ``` ## Environment variables at runtime SvelteKit's Node adapter reads these at startup — they must be injected via Docker environment, not baked into the image: | Variable | Purpose | |---|---| | `API_INTERNAL_URL` | SSR calls from server to backend (e.g. `http://backend:8080`) | | `PORT` | Port the Node server listens on (default `3000`) | | `HOST` | Bind address (must be `0.0.0.0` in Docker) | | `ORIGIN` | Full public URL (e.g. `https://familienarchiv.example.com`) — required by SvelteKit for CSRF protection | `ORIGIN` is easy to forget and causes cryptic CSRF errors if missing. Document it in `.env.example`. ## Additional hardening - Run as non-root: add `RUN addgroup -S app && adduser -S app -G app` and `USER app` in the runtime stage. - Do not copy `.env` or any secrets into the image — all config comes from the runtime environment. ## Acceptance criteria - `docker build -t familienarchiv-frontend .` from `frontend/` produces a self-contained image. - `docker run -e API_INTERNAL_URL=... -e ORIGIN=... familienarchiv-frontend` starts and serves the app. - Final image size is under 200 MB. - No source files or devDependencies in the final image layers.
marcel added the devopsphase-2: container-images labels 2026-03-28 10:46:41 +01:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#135