docker: multi-stage Dockerfile for adapter-node runtime #10

Merged
forgejo_admin merged 1 commit from 7-dockerfile-multistage into main 2026-04-28 04:35:32 +00:00

Summary

Adds a multi-stage Dockerfile + .dockerignore for westside-admin. Builder stage compiles SvelteKit (npm ci + npm run build); runtime stage copies only build/ plus production deps and runs as non-root node (UID 1000) on port 3000.

Changes

  • Dockerfile — two-stage node:22-alpine build, npm ci for reproducibility (lockfile committed in PR #9), NODE_ENV=production, drops to node user, EXPOSE 3000, CMD ["node", "build/index.js"]. No HEALTHCHECK directive — k8s probes (kustomize overlay PR #134) own /health.
  • .dockerignore — excludes node_modules, build, .svelte-kit, .git, .env*, *.md, .playwright-mcp, .claude, screenshots, etc. Includes the node_modules/.vite-temp exclusion that fixed prior Kaniko snapshot failures in westside-app.

Test Plan

Verified locally:

  • docker build -t westside-admin:test . succeeds
  • docker run -p 3000:3000 westside-admin:test -> "Listening on http://0.0.0.0:3000"
  • curl http://localhost:3000/health -> HTTP 200
  • docker run --rm westside-admin:test id -> uid=1000(node) gid=1000(node) (non-root)
  • Image size: 240MB (under 250MB target)
  • No secrets baked in (no COPY of .env*, .dockerignore enforces)

Review Checklist

  • docker build succeeds locally
  • Container runs and serves on port 3000
  • Image size under 250MB (240MB actual)
  • No secrets baked into image (verified .dockerignore excludes .env*)
  • EXPOSE 3000 declared
  • /health returns 200 (k8s probes own HEALTHCHECK semantics)
  • Runs as non-root user (UID 1000)
  • Reproducible install via npm ci + committed lockfile
  • No unrelated changes (no .woodpecker.yaml, no scaffold modifications)
  • Closes #7
  • Story: story-westside-admin-admin-row-crud
  • Arch: arch-deployment-westside-admin (Harbor project from #65)
  • Sibling: #8 (Woodpecker pipeline — out of scope), kustomize overlay PR #134
  • Reference: ~/westside-app/Dockerfile (adapter-static + nginx — different runtime, similar dockerignore pattern)
## Summary Adds a multi-stage Dockerfile + .dockerignore for westside-admin. Builder stage compiles SvelteKit (npm ci + npm run build); runtime stage copies only `build/` plus production deps and runs as non-root `node` (UID 1000) on port 3000. ## Changes - `Dockerfile` — two-stage `node:22-alpine` build, `npm ci` for reproducibility (lockfile committed in PR #9), `NODE_ENV=production`, drops to `node` user, `EXPOSE 3000`, `CMD ["node", "build/index.js"]`. No HEALTHCHECK directive — k8s probes (kustomize overlay PR #134) own `/health`. - `.dockerignore` — excludes node_modules, build, .svelte-kit, .git, .env*, *.md, .playwright-mcp, .claude, screenshots, etc. Includes the `node_modules/.vite-temp` exclusion that fixed prior Kaniko snapshot failures in westside-app. ## Test Plan Verified locally: - `docker build -t westside-admin:test .` succeeds - `docker run -p 3000:3000 westside-admin:test` -> "Listening on http://0.0.0.0:3000" - `curl http://localhost:3000/health` -> HTTP 200 - `docker run --rm westside-admin:test id` -> `uid=1000(node) gid=1000(node)` (non-root) - Image size: **240MB** (under 250MB target) - No secrets baked in (no COPY of .env*, .dockerignore enforces) ## Review Checklist - [x] `docker build` succeeds locally - [x] Container runs and serves on port 3000 - [x] Image size under 250MB (240MB actual) - [x] No secrets baked into image (verified .dockerignore excludes .env*) - [x] EXPOSE 3000 declared - [x] `/health` returns 200 (k8s probes own HEALTHCHECK semantics) - [x] Runs as non-root user (UID 1000) - [x] Reproducible install via `npm ci` + committed lockfile - [x] No unrelated changes (no .woodpecker.yaml, no scaffold modifications) ## Related Notes - Closes #7 - Story: `story-westside-admin-admin-row-crud` - Arch: `arch-deployment-westside-admin` (Harbor project from #65) - Sibling: #8 (Woodpecker pipeline — out of scope), kustomize overlay PR #134 - Reference: `~/westside-app/Dockerfile` (adapter-static + nginx — different runtime, similar dockerignore pattern)
Builder stage compiles SvelteKit (npm ci + npm run build); runtime
stage copies only build/ + production deps and runs as the non-root
'node' user (UID 1000) on port 3000. Image lands at ~240MB, under
the 250MB target. /health probe owned by the kustomize overlay.

Closes #7

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author
Owner

PR #10 Review

DOMAIN REVIEW

Stack: Dockerfile (multi-stage) + .dockerignore for SvelteKit adapter-node on node:22-alpine. Containerization / k8s deployment domain.

Multi-stage soundness: Clean separation. Builder stage runs npm ci then npm run build. Runtime stage starts from a fresh node:22-alpine, installs prod-only deps with npm ci --omit=dev, copies build/ from builder, and runs node build/index.js. No dev deps, no source files, no .svelte-kit/ cache leak into runtime. Matches the adapter-node out: 'build' convention.

Reproducibility: npm ci in BOTH stages (builder + runtime). Lockfile from PR #9 honored. No npm install anywhere. Matches Track A QA forward-flag.

Non-root: chown -R node:node /app then USER node. node:alpine ships UID 1000 node user. Dev's local id confirms uid=1000(node) gid=1000(node). Correct.

Image surface / size: 240MB reported (under 250MB target). Layering is minimal: prod deps + build output + package.json on alpine base. npm cache clean --force after the runtime install keeps the layer lean. No Tailwind/PostCSS detected (compliant with feedback_no_tailwind).

Healthcheck delegation: No HEALTHCHECK directive. Comment explicitly hands liveness/readiness to k8s probes (kustomize overlay #134). Correct per platform convention — Dockerfile shouldn't duplicate k8s probe semantics.

Base pin: node:22-alpine (specific major). Fine — a stronger pin than node:lts-alpine, and matches basketball-api and westside-app siblings. No drift risk from lts floating tag.

Secrets: No ARG/ENV carrying sensitive values. .dockerignore excludes .env and .env.*. No COPY of secret files. Clean.

.dockerignore: Excludes node_modules, build, .svelte-kit, .git, .env*, *.md, *.png, .playwright-mcp, .claude, coverage/test-results. Includes the node_modules/.vite-temp exclusion that previously broke Kaniko in westside-app — good carry-forward.

Sibling comparison: westside-app uses adapter-static + nginx (different runtime). Dev correctly noted the structural divergence in PR body. basketball-api shows the multi-stage shape this PR follows. No copy-paste mismatch.

BLOCKERS

None.

NITS

  • Line 27 COPY package.json package-lock.json ./ in the runtime stage could COPY --from=builder /app/package*.json ./ to save one copy operation, but doing it from the host is also fine and arguably more transparent. Non-blocking.
  • Builder stage does COPY . . which will pull every non-ignored file into the build context layer. Acceptable since the runtime stage is fresh, but if future scaffolding adds large fixtures, scope .dockerignore again.
  • RUN chown -R node:node /app could be avoided by using COPY --chown=node:node on each prior COPY, saving a layer. Cosmetic.

SOP COMPLIANCE

  • Branch named after issue (7-dockerfile-multistage)
  • PR body has Summary / Changes / Test Plan / Related Notes
  • Closes #7 declared
  • Story + arch trace in PR body (story-westside-admin-admin-row-crud, arch-deployment-westside-admin); repo has no labels yet — Ava is filing that separately, acknowledged
  • No secrets committed
  • Scope is exactly the Dockerfile + .dockerignore — no .woodpecker.yaml, no scaffold drift (sibling PR #11 covers CI)
  • Test Plan documents local docker build, docker run, curl /health, id non-root check, image size

PROCESS OBSERVATIONS

  • Test Plan is local-only. No CI build verification yet because .woodpecker.yaml lands in the sibling PR (#11 / issue #8). That's correct sequencing — Dockerfile must exist before pipeline can build it. Validation gate after merge should confirm Kaniko-in-CI matches the local docker build outcome.
  • Healthcheck-delegation comment is exemplary documentation; future Dockerfiles in the platform should adopt this pattern explicitly so reviewers don't ask "where's HEALTHCHECK?"
  • Image size 240MB on node:22-alpine with full prod deps is healthy. If size pressure ever rises, consider node:22-alpine + --production install in a separate deps stage and copy node_modules across to skip the second npm ci network round-trip.

VERDICT: APPROVED

## PR #10 Review ### DOMAIN REVIEW **Stack:** Dockerfile (multi-stage) + .dockerignore for SvelteKit adapter-node on `node:22-alpine`. Containerization / k8s deployment domain. **Multi-stage soundness:** Clean separation. Builder stage runs `npm ci` then `npm run build`. Runtime stage starts from a fresh `node:22-alpine`, installs prod-only deps with `npm ci --omit=dev`, copies `build/` from builder, and runs `node build/index.js`. No dev deps, no source files, no `.svelte-kit/` cache leak into runtime. Matches the adapter-node `out: 'build'` convention. **Reproducibility:** `npm ci` in BOTH stages (builder + runtime). Lockfile from PR #9 honored. No `npm install` anywhere. Matches Track A QA forward-flag. **Non-root:** `chown -R node:node /app` then `USER node`. `node:alpine` ships UID 1000 `node` user. Dev's local `id` confirms `uid=1000(node) gid=1000(node)`. Correct. **Image surface / size:** 240MB reported (under 250MB target). Layering is minimal: prod deps + build output + package.json on alpine base. `npm cache clean --force` after the runtime install keeps the layer lean. No Tailwind/PostCSS detected (compliant with `feedback_no_tailwind`). **Healthcheck delegation:** No `HEALTHCHECK` directive. Comment explicitly hands liveness/readiness to k8s probes (kustomize overlay #134). Correct per platform convention — Dockerfile shouldn't duplicate k8s probe semantics. **Base pin:** `node:22-alpine` (specific major). Fine — a stronger pin than `node:lts-alpine`, and matches `basketball-api` and `westside-app` siblings. No drift risk from `lts` floating tag. **Secrets:** No `ARG`/`ENV` carrying sensitive values. `.dockerignore` excludes `.env` and `.env.*`. No COPY of secret files. Clean. **.dockerignore:** Excludes `node_modules`, `build`, `.svelte-kit`, `.git`, `.env*`, `*.md`, `*.png`, `.playwright-mcp`, `.claude`, coverage/test-results. Includes the `node_modules/.vite-temp` exclusion that previously broke Kaniko in westside-app — good carry-forward. **Sibling comparison:** westside-app uses adapter-static + nginx (different runtime). Dev correctly noted the structural divergence in PR body. basketball-api shows the multi-stage shape this PR follows. No copy-paste mismatch. ### BLOCKERS None. ### NITS - Line 27 `COPY package.json package-lock.json ./` in the runtime stage could `COPY --from=builder /app/package*.json ./` to save one copy operation, but doing it from the host is also fine and arguably more transparent. Non-blocking. - Builder stage does `COPY . .` which will pull every non-ignored file into the build context layer. Acceptable since the runtime stage is fresh, but if future scaffolding adds large fixtures, scope `.dockerignore` again. - `RUN chown -R node:node /app` could be avoided by using `COPY --chown=node:node` on each prior COPY, saving a layer. Cosmetic. ### SOP COMPLIANCE - [x] Branch named after issue (`7-dockerfile-multistage`) - [x] PR body has Summary / Changes / Test Plan / Related Notes - [x] Closes #7 declared - [x] Story + arch trace in PR body (`story-westside-admin-admin-row-crud`, `arch-deployment-westside-admin`); repo has no labels yet — Ava is filing that separately, acknowledged - [x] No secrets committed - [x] Scope is exactly the Dockerfile + .dockerignore — no `.woodpecker.yaml`, no scaffold drift (sibling PR #11 covers CI) - [x] Test Plan documents local `docker build`, `docker run`, `curl /health`, `id` non-root check, image size ### PROCESS OBSERVATIONS - Test Plan is local-only. No CI build verification yet because `.woodpecker.yaml` lands in the sibling PR (#11 / issue #8). That's correct sequencing — Dockerfile must exist before pipeline can build it. Validation gate after merge should confirm Kaniko-in-CI matches the local `docker build` outcome. - Healthcheck-delegation comment is exemplary documentation; future Dockerfiles in the platform should adopt this pattern explicitly so reviewers don't ask "where's HEALTHCHECK?" - Image size 240MB on `node:22-alpine` with full prod deps is healthy. If size pressure ever rises, consider `node:22-alpine` + `--production` install in a separate `deps` stage and copy `node_modules` across to skip the second `npm ci` network round-trip. ### VERDICT: APPROVED
forgejo_admin deleted branch 7-dockerfile-multistage 2026-04-28 04:35:32 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
forgejo_admin/westside-admin!10
No description provided.