Files
marktvogt.de/web/Dockerfile
vikingowl df5b0563c9 refactor(web): bundle server with esbuild, slim runtime to alpine+node
Post-process adapter-node output into a single self-contained
build/bundle.mjs (614 KB) via esbuild, then ship it on a fresh
alpine:3.21 base with just the node binary copied in. Drops the
node_modules + package manager baggage that comes with node:25-alpine.

- Add esbuild devDep + `bundle` script (scripts/bundle.mjs)
- Dockerfile: drop `deps` stage; final stage is alpine + node binary +
  bundle + static client assets
- Uncompressed image: 177 MB -> 149 MB (-16%)
- Verified: /, /healthz, static assets all respond identically;
  outbound TLS to api.marktvogt.de works via node's built-in CA bundle
2026-04-18 05:05:07 +02:00

48 lines
1.5 KiB
Docker

FROM node:25-alpine AS builder
RUN npm install -g pnpm@10
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
# PUBLIC_API_BASE_URL is baked at build time if using $env/static/public.
# If using $env/dynamic/public, remove the ARG/ENV below and pass it at runtime.
ARG PUBLIC_API_BASE_URL=https://api.marktvogt.de
ENV PUBLIC_API_BASE_URL=$PUBLIC_API_BASE_URL
ARG PUBLIC_TURNSTILE_SITE_KEY=1x00000000000000000000AA
ENV PUBLIC_TURNSTILE_SITE_KEY=$PUBLIC_TURNSTILE_SITE_KEY
RUN pnpm run build && pnpm run bundle
# ─────────────────────────────────────────────
# Fresh Alpine base + just the Node binary — avoids shipping npm/yarn
# (~19 MB) and the upstream node image's build tooling. Cuts the image
# by ~35 MB vs. using `FROM node:25-alpine` directly.
FROM alpine:3.21
RUN apk add --no-cache libstdc++
COPY --from=builder /usr/local/bin/node /usr/local/bin/node
WORKDIR /app
# Only the self-contained bundle and static client assets are needed at
# runtime — no node_modules, no package.json.
COPY --from=builder /app/build/bundle.mjs ./bundle.mjs
COPY --from=builder /app/build/client ./client
# alpine:3.21 ships `nobody` at UID 65534 — matches podSecurityContext.runAsUser
USER nobody:nobody
# ORIGIN is required by adapter-node for CSRF protection.
# Must match the public-facing URL exactly (set via k8s ConfigMap).
ENV PORT=3000 HOST=0.0.0.0 NODE_ENV=production
EXPOSE 3000
CMD ["node", "bundle.mjs"]