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
48 lines
1.5 KiB
Docker
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"]
|