Skip to content

feat: distroless non-root runtime image + compiled migration entrypoint (workspace#026) - #6300

Open
valentinyanakiev wants to merge 8 commits into
developfrom
feat/026-distroless-runtime-images
Open

feat: distroless non-root runtime image + compiled migration entrypoint (workspace#026)#6300
valentinyanakiev wants to merge 8 commits into
developfrom
feat/026-distroless-runtime-images

Conversation

@valentinyanakiev

@valentinyanakiev valentinyanakiev commented Jul 23, 2026

Copy link
Copy Markdown
Member

Implements the server slice of workspace#026-distroless-runtime-images (story: alkem-io/infrastructure-operations#2153, epic infra-ops#2059).

What

  • Three-stage Dockerfile: node:22.21.1-trixie-slim builder → prod-deps stage → digest-pinned gcr.io/distroless/nodejs22-debian13:nonroot runtime. UID 65532, no shell/pkg-manager/ts-node/src/dev-deps//wait. ENV PATH includes /nodejs/bin so bare command: overrides also resolve.
  • Dual-mode migration glob (src/ via ts-node AND compiled dist/migrations/*.js); migrations run in-image via node ./node_modules/typeorm/cli.js migration:run --dataSource dist/config/migration.config.js.
  • CVE remediation to zero fixable HIGH/CRITICAL (Debian 13 pair + dependency overrides + TypeORM patch). 2 documented unreachable residuals (TypeORM CVE-2025-60542 — postgres-only deployment; @nestjs/microservices CVE-2026-40879 — RMQ-only).
  • Persisted regression harnesses: .docker/distroless-image-smoke.sh, .docker/distroless-migration-smoke.sh. Human-readable CVE/size evidence in .docker/verification/; full SBOM/scan JSONs live in the workspace spec evidence bundle.

Evidence (live on k8s-hetzner-sandbox, ephemeral namespace)

  • Image 43.7% smaller than baseline (SC-001 ≥40%). All smoke checks pass against the exact deployed digest.
  • Migration Job (compiled entrypoint) green on fresh PostgreSQL 17.5; /health/live|ready 200; GraphQL serving; full rollback drill old↔new proven.
  • Review: 12-cell adversarial panel — 0 confirmed findings; security verdict pass.

Release coupling (R-8 — release-blocking follow-up)

infrastructure-operations (prod/acc) still runs pnpm-based migration CronJob + auth-reset worker against alkemio/server release tags. The first release shipping this image MUST bundle the equivalent infra-ops manifest updates (tracked in the workspace spec + Release NN story scope).

Spec: agents-hq/specs/026-distroless-runtime-images/ (C-1 amended 2026-07-24: Debian 13 pair ratified — FR-015 zero-fixable-CVE gate over the story's literal debian12 naming).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Introduced a smaller, distroless production container running as a non-root user.
    • Improved compatibility with direct Node.js command overrides and native modules.
    • Added reliable migration execution and verification for compiled production builds.
  • Bug Fixes

    • Improved migration path resolution across source and compiled environments.
    • Added security hardening for a TypeORM-related SQL injection vulnerability.
    • Reduced image contents by removing development tools and unnecessary files.
  • Documentation

    • Added container publishing, migration, security remediation, and image-size verification guidance.

workspace#026-distroless-runtime-images (US2, R-3, FR-004)

The TypeORM CLI DataSource used by migration:run/revert/show resolved its
`migrations` glob relative to the process CWD (src/migrations/*.{ts,js}).
Run from a compiled dist/ tree — as the distroless runtime image must —
that glob silently matches zero files: migration:run exits 0 having done
nothing, and every downstream deploy that carries a schema change is
silently bricked.

Anchor the glob to __dirname instead, so the identical config resolves
src/migrations/*.ts under ts-node (developer flow, unaffected) and
dist/migrations/*.js when compiled (the distroless image's execution
mode). Add a mechanical guard asserting every migration file imports only
typeorm/node builtins (path-aliased imports would break the plain-Node
CLI path); all current migrations already comply.
Pre-existing pnpm lint failure unrelated to this feature, encountered
while bringing the exit-gate pass green for
workspace#026-distroless-runtime-images.
workspace#026-distroless-runtime-images (US1, R-2, FR-001, FR-002, FR-003, SC-001, SC-002)

Replace the single-stage node:22.21.1-alpine image (ships pnpm, ts-node,
full sources, dev dependencies, and a /wait binary into production) with
a three-stage build:

  1. builder  — node:22.21.1-bookworm-slim (glibc, matches the repo's
     Volta pin), full deps, compiles TypeScript to dist/.
  2. proddeps — the SAME glibc base, a fresh `pnpm install --prod`. Never
     copied from an Alpine/musl layer: sharp's prebuilt native binaries
     are ABI-specific to the libc they were resolved against, and a musl
     artifact silently fails to load on glibc.
  3. runtime  — gcr.io/distroless/nodejs22-debian12:nonroot, digest-pinned.
     No shell, no package manager, no /wait, no src/, no dev deps. Runs
     as UID 65532. CMD ["dist/main.js"] (the distroless node entrypoint).

Dropping /wait is verified consumer-safe: no Kubernetes manifest invokes
the image's own /wait — the server Deployment's readiness gating uses a
separate groundnuty/k8s-wait-for initContainer.

Supporting package.json changes, both required for the --prod install to
actually be production-only:

- Move ts-node and tsconfig-paths from dependencies to devDependencies.
  They were miscategorized as production dependencies despite being used
  only by dev/CI-time CLI scripts (migration:*, schema:*, start-nodemon);
  nothing in src/ requires either at runtime. Left in `dependencies`,
  `pnpm install --prod` would ship both into the "distroless" image.
- Guard the `prepare` script (`husky || true`): husky is a devDependency,
  so a --prod install has no `husky` binary at all and the bare `husky`
  prepare script hard-failed the install.
- Add `pnpm.supportedArchitectures.libc: ["glibc"]` so pnpm never
  resolves musl-variant optional dependencies (e.g. sharp's musl
  binaries) for this project going forward — encodes the "never a musl
  artifact in the Debian runtime" constraint at the package-manager
  level, not just via the build's stage topology.

.dockerignore: added the three-stage build's actual context exclusions
(test/, docs/, specs/, scripts/, tooling configs, generated schema
files) alongside the pre-existing generic template entries.

Measured: 176.7MB compressed vs a 317.6MB alkemio/server:latest
baseline — 44.35% smaller (SC-001, ≥40% required). Verified via
.docker/distroless-image-smoke.sh: non-root, no shell/pnpm/wait
reachable, no src/ or ts-node/pnpm in node_modules, sharp loads
(glibc-matched, US1-AS1/AS2).
workspace#026-distroless-runtime-images (US1, US2, US5, R-2, R-3, FR-005, FR-015, SC-001, SC-006)

Two executable, persisted regression checks (contract checks for
server-runtime-image / server-migration-entrypoint):

- .docker/distroless-image-smoke.sh <image> — asserts UID 65532,
  CMD ["dist/main.js"], no shell/apk/apt/pnpm/wait executable, no src/
  tree, no ts-node/pnpm in node_modules, sharp loads (native glibc
  sentinel), and enforces the SC-001 ≥40% size-reduction budget.
  Emits IMAGE_DIGEST=/IMAGE_SIZE_BYTES= for evidence-bundle consumption.

- .docker/distroless-migration-smoke.sh <image> — spins up a throwaway
  postgres:17.5 container, runs the compiled TypeORM CLI migration path
  with zero shell involvement, and asserts the migrations_typeorm row
  count matches the compiled dist/migrations/*.js file count (the
  regression this guards against: a CWD-relative glob run from dist/
  would match zero files and "succeed" having silently done nothing),
  then asserts migration:show reports zero pending migrations. Also
  enforces the SC-006 <5-minute budget.

Baseline size (317,579,664 bytes compressed, alkemio/server:latest
amd64) is documented inline in distroless-image-smoke.sh: the exact SHA
tag pinned in dev-orchestration's base manifest
(9b6a81e) wasn't resolvable from this
worktree (404 on Docker Hub; dev-orchestration is a separate repo/wave
not in this slice's scope), so the equivalent `latest` tag was used
instead. Re-measure against the true pinned baseline during
forge-verify if it becomes available.
…point

workspace#026-distroless-runtime-images (US5, FR-005)

Describes the three-stage build, what was removed and why (/wait,
pnpm, ts-node/tsconfig-paths), the compiled migration invocation that
dev-orchestration (and infra-ops at release time, R-8) must use, and
how to run the persisted smoke scripts locally.
Reduce trivy HIGH/CRITICAL findings on alkemio/server:026-distroless-local
from 68 to 8 via pnpm.overrides + direct dependency bumps: sharp 0.35.3,
path-to-regexp 8.4.2 (+ express's bundled 0.1.13), multer 2.2.0, picomatch
2.3.2, undici 6.27.0, validator 13.15.35, ws (three bundlers) 7.5.13/8.21.1,
@apollo/server 4.13.0, axios 1.18.1 (unified), minimatch 3.1.5/5.1.9,
brace-expansion 1.1.16/2.1.2, fast-uri 3.1.4, form-data 3.0.5/4.0.6,
immutable 4.3.9, js-yaml 4.3.0, jws 3.2.3, linkify-it 5.0.2, lodash 4.18.1.

Also fixes a pre-existing top-level package.json "overrides" block being
silently inert under pnpm 10.17.1 (only pnpm.overrides is read) by moving
fs-capacitor/jsonwebtoken/tough-cookie/@nestjs/schematics>typescript pins
into pnpm.overrides, where they now actually apply tree-wide.

Excludes erroneously-materialized -musl optional native binaries (sharp's
libvips, @napi-rs/canvas, @swc/core) that the broader lockfile resolution
exposed, keeping the image within the SC-001 40% size-reduction floor.

Evidence: .docker/verification/{sbom.cdx.json,sizes.md,trivy-before-fix.json,
trivy-after-fix.json,US5-AS1-cve-remediation.md}. Remaining 8 findings
(libssl3 x6, typeorm x1, @nestjs/microservices x1) addressed in the next
commit.
…h (US5-AS1)

Closes the remaining 8 fixable HIGH/CRITICAL trivy findings down to 2
verified-unreachable residual risks, satisfying the FR-015/US5-AS1/SC-004
zero-fixable-HIGH/CRITICAL gate:

- libssl3 (1 CRITICAL + 5 HIGH, CVE-2026-31789/28387/28388/28389/28390/45447):
  migrate the Dockerfile's builder+runtime base pair from Debian 12
  (node:22.21.1-bookworm-slim / gcr.io/distroless/nodejs22-debian12:nonroot)
  to Debian 13 (node:22.21.1-trixie-slim / gcr.io/distroless/nodejs22-debian13:nonroot).
  Debian 12 is frozen upstream at the currently-pinned patch level and
  Google's distroless project no longer builds fresh Debian-12 variants, so
  no future Debian-12 rebuild would ever clear this. This supersedes spec
  chief-decision C-1 (which pinned Debian 12 because the story named it
  verbatim) — the spec's own hard CVE gate takes precedence once a live scan
  proved no Debian-12 fix path exists. Same glibc family as Debian 12, so
  the sharp/native-module gate (R-2) is unaffected. Rebuilt + re-verified:
  both smoke gates green, size reduction 43.72% (still clears the 40%
  SC-001 floor).

- typeorm CVE-2025-60542 (GHSA-q2pj-6v73-8rgj, SQL injection via
  repository.save/update): investigation confirms this is MySQL/mysql2-
  driver-only (missing `stringifyObjects: true` on the mysql2 connection
  options). This codebase is postgres-only (every DataSource hardcodes
  `type: 'postgres'`; mysql2 is not an installed dependency), so the
  vulnerable code path is unreachable. Rather than rebase the custom CTI
  fork (unsafe — no verified newer commit exists, per the prior escalation),
  ports the exact upstream fix (typeorm commit d57fe3b) as a local
  `pnpm patch` (patches/typeorm@0.3.13.patch, wired via
  package.json:pnpm.patchedDependencies) for defense-in-depth. Verified
  present in the built image. Trivy will still report this by version
  string (its matching is version-based, blind to patch overlays) — recorded
  as residual risk pending a real fork rebase once a safe upstream commit
  exists.

- @nestjs/microservices CVE-2026-40879: unchanged disposition (documented
  residual risk — Transport.TCP/JsonSocket path confirmed unreachable, only
  Transport.RMQ is used; fixing requires a NestJS 10->11 major bump,
  disproportionate blast radius for an unreachable transport).

Evidence updated: .docker/verification/{sizes.md,US5-AS1-cve-remediation.md,
sbom.cdx.json,trivy-after-fix.json} now reflect the final 2-residual-risk
posture. Full gate pass (install/lint/test/build/docker build) and both
persisted smoke scripts (distroless-image-smoke.sh,
distroless-migration-smoke.sh) re-verified green against the rebuilt image.

Note: the shared evidence bundle at
specs/026-distroless-runtime-images/forge/verification/ (workspace root)
is intentionally NOT written from this worktree — implementer charter
confines file writes to the worktree; promoting this evidence into the
shared bundle is left to the orchestrator/a human.
…ve machine scan output to the workspace evidence bundle

The ~4MB sbom/trivy JSONs live in the workspace spec's
forge/verification/evidence/server/ (review advisories spec-server-2,
qual-server-1). Human-readable US5-AS1-cve-remediation.md and sizes.md
stay in-repo. workspace#026-distroless-runtime-images
@github-actions

Copy link
Copy Markdown

📊 PR Metrics Summary

Title: feat: distroless non-root runtime image + compiled migration entrypoint (workspace#026)
Total LOC Changed: 1692
Files Changed: 13
Proposed Review Type: HUMAN_AUGMENTED_LLM
Rationale:

  • high_risk_keyword
  • critical_path_change
  • LOC>200
  • files>10

Flags

  • High Risk Keyword
  • Critical Path Change
  • Composite High Risk Trigger

Thresholds

{
  "critical_loc": 200,
  "simple_loc": 100,
  "file_count": 10
}

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

Distroless runtime migration

Layer / File(s) Summary
Three-stage distroless image
Dockerfile, .dockerignore, package.json, patches/typeorm@0.3.13.patch
The build now creates a digest-pinned Debian 13 distroless non-root runtime with production-only dependencies and patched TypeORM configuration.
Compiled TypeORM migration execution
src/config/typeorm.cli.config.run.ts, src/config/typeorm.cli.config.run.spec.ts, .docker/distroless-migration-smoke.sh, docs/PublishingImages.md
Migration discovery supports source and compiled paths, and the smoke flow runs and verifies migrations against PostgreSQL.
Runtime contract and image-size checks
.docker/distroless-image-smoke.sh, .docker/verification/sizes.md, docs/PublishingImages.md
Runtime probes validate image metadata, prohibited tooling, native sharp, command overrides, production contents, and size reduction.
CVE remediation evidence
.docker/verification/US5-AS1-cve-remediation.md, patches/typeorm@0.3.13.patch, package.json
Dependency and base-image remediation results, the TypeORM patch, residual risks, and reproduction commands are recorded.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Operator
  participant MigrationSmokeScript
  participant DistrolessImage
  participant PostgreSQL
  Operator->>MigrationSmokeScript: provide image tag
  MigrationSmokeScript->>PostgreSQL: start container and wait for readiness
  MigrationSmokeScript->>DistrolessImage: run migration:run
  DistrolessImage->>PostgreSQL: apply compiled migrations
  MigrationSmokeScript->>PostgreSQL: count applied migrations
  MigrationSmokeScript->>DistrolessImage: count compiled migration files
  MigrationSmokeScript->>DistrolessImage: run migration:show
Loading

Suggested reviewers: techsmyth

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change: a distroless non-root runtime image plus compiled migration entrypoint support.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/026-distroless-runtime-images

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

Schema Diff Summary: No blocking changes

Category Count
breaking 0
prematureRemoval 0
invalidDeprecation 0
deprecated 0
additive 0
info 0

Baseline branch: develop
Current schema MD5: 01bd14ddff193a406eb865e40833097a (size 316176)
Previous schema MD5: 01bd14ddff193a406eb865e40833097a

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.docker/distroless-migration-smoke.sh:
- Around line 59-61: Replace the predictable PID-derived temporary paths in
.docker/distroless-migration-smoke.sh lines 59-61 and
.docker/distroless-image-smoke.sh lines 63-68 with paths allocated atomically
via mktemp. Ensure each script stores and uses its allocated log or probe path,
and removes that exact file through the existing cleanup paths.

In @.docker/verification/US5-AS1-cve-remediation.md:
- Around line 102-110: Revise the CVE explanation to avoid claiming the
vulnerability is neutralized based solely on the grep result. Either add a
focused runtime assertion proving the MySQL driver path uses merged connection
options containing stringifyObjects: true, or change “neutralizes the real
vulnerable behavior” to state only that the patched source is present.

In `@docs/PublishingImages.md`:
- Around line 13-30: Update the production image documentation in the
three-stage Dockerfile description: change the builder/proddeps base from
node:22.21.1-bookworm-slim to node:22.21.1-trixie-slim and the runtime base from
distroless nodejs22-debian12 to nodejs22-debian13:nonroot. Keep the existing
digest-pinning and stage descriptions unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cc969dec-9682-4972-9fa7-f5c41dfae234

📥 Commits

Reviewing files that changed from the base of the PR and between 0f12ba6 and 6eafd84.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (12)
  • .docker/distroless-image-smoke.sh
  • .docker/distroless-migration-smoke.sh
  • .docker/verification/US5-AS1-cve-remediation.md
  • .docker/verification/sizes.md
  • .dockerignore
  • Dockerfile
  • docs/PublishingImages.md
  • package.json
  • patches/typeorm@0.3.13.patch
  • src/config/typeorm.cli.config.run.spec.ts
  • src/config/typeorm.cli.config.run.ts
  • src/domain/access/role-set/role.set.resolver.mutations.membership.spec.ts
💤 Files with no reviewable changes (1)
  • src/domain/access/role-set/role.set.resolver.mutations.membership.spec.ts

Comment on lines +59 to +61
> /tmp/migration-run.$$.log 2>&1 \
|| { cat /tmp/migration-run.$$.log; rm -f /tmp/migration-run.$$.log; fail "migration:run (compiled, no shell) exited non-zero"; }
rm -f /tmp/migration-run.$$.log

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Use atomically created temporary files in both smoke scripts. PID-derived /tmp paths are predictable and can be pre-created or symlinked. Use mktemp and remove the allocated file through each script’s cleanup path.

  • .docker/distroless-migration-smoke.sh#L59-L61: replace migration-run.$$.log with a mktemp-created log path.
  • .docker/distroless-image-smoke.sh#L63-L68: replace shell-probe.$$ with a mktemp-created probe path.
🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 59-59: Building a temp file path in a world-writable directory from the PID ($$) or `` is predictable and racy: an attacker can pre-create or guess the name and win a symlink/race attack. Use mktemp (e.g. `f=$(mktemp)` or `f=$(mktemp /tmp/myapp.XXXXXX)`) so the kernel atomically creates a unique, unpredictable file.
Context: /tmp/migration-run.$$.log
Note: [CWE-377] Insecure Temporary File.

(tmp-file-pid-name-bash)

📍 Affects 2 files
  • .docker/distroless-migration-smoke.sh#L59-L61 (this comment)
  • .docker/distroless-image-smoke.sh#L63-L68
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.docker/distroless-migration-smoke.sh around lines 59 - 61, Replace the
predictable PID-derived temporary paths in .docker/distroless-migration-smoke.sh
lines 59-61 and .docker/distroless-image-smoke.sh lines 63-68 with paths
allocated atomically via mktemp. Ensure each script stores and uses its
allocated log or probe path, and removes that exact file through the existing
cleanup paths.

Source: Linters/SAST tools

Comment on lines +102 to +110
**Why trivy still reports this finding after the patch**: trivy's npm/yarn/
pnpm vulnerability matching is version-string-based (reads the installed
`typeorm` package's declared version, `0.3.13`, against the advisory's affected
range `< 0.3.26`) — it has no visibility into a `pnpm patch` overlay's actual
code changes. The patch neutralizes the real vulnerable *behavior* (verified:
`grep 'stringifyObjects: true' node_modules/typeorm/driver/mysql/MysqlDriver.js`
inside the built image returns a match) but cannot change what version string
the scanner reads, since that requires the fork itself to publish a build
whose `package.json.version` is bumped past `0.3.26` — which requires exactly

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the referenced document with line numbers.
file=".docker/verification/US5-AS1-cve-remediation.md"
wc -l "$file"
sed -n '90,160p' "$file" | cat -n

Repository: alkem-io/server

Length of output: 3857


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file=".docker/verification/US5-AS1-cve-remediation.md"

echo "== lines 100-110 =="
sed -n '100,110p' "$file" | cat -n

echo
echo "== lines 138-145 =="
sed -n '138,145p' "$file" | cat -n

echo
echo "== search for runtime assertion / createConnectionOptions / stringifyObjects =="
rg -n "createConnectionOptions|stringifyObjects|MysqlDriver" .docker/verification src . 2>/dev/null || true

Repository: alkem-io/server

Length of output: 3211


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="patches/typeorm@0.3.13.patch"
wc -l "$file"
sed -n '1,80p' "$file" | cat -n

Repository: alkem-io/server

Length of output: 1774


Strengthen the CVE wording. The grep only shows the patched source is present in the image; it doesn’t prove the MySQL driver path is exercised or that the merged connection options still contain stringifyObjects: true at runtime. Add a focused runtime assertion, or soften “neutralizes” to “patched source is present.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.docker/verification/US5-AS1-cve-remediation.md around lines 102 - 110,
Revise the CVE explanation to avoid claiming the vulnerability is neutralized
based solely on the grep result. Either add a focused runtime assertion proving
the MySQL driver path uses merged connection options containing
stringifyObjects: true, or change “neutralizes the real vulnerable behavior” to
state only that the patched source is present.

Comment thread docs/PublishingImages.md
Comment on lines +13 to +30
Since `workspace#026-distroless-runtime-images`, the production image is built by a
three-stage `Dockerfile`:

1. **`builder`** — `node:22.21.1-bookworm-slim` (glibc/Debian 12, matches the repo's
Volta pin). Installs the full dependency set (incl. dev) and compiles TypeScript
(`pnpm run build` → `dist/`).
2. **`proddeps`** — the **same glibc base**, a fresh `pnpm install --frozen-lockfile --prod`.
This stage is never skipped or copied from an Alpine/musl layer: native modules
(`sharp`) must be installed on the exact glibc runtime they'll execute on, or
they silently fail to load (musl artifacts are ABI-incompatible with glibc).
`package.json`'s `pnpm.supportedArchitectures.libc: ["glibc"]` reinforces this by
preventing musl-variant optional dependencies from ever being resolved.
3. **`runtime`** — `gcr.io/distroless/nodejs22-debian12:nonroot`, digest-pinned. No
shell, no package manager, no `/wait` binary, no `src/` sources, no dev
dependencies. Runs as UID 65532 (`nonroot`). Entrypoint is the distroless `node`
binary; `CMD ["dist/main.js"]`.

Both base image tags are pinned by digest in the `Dockerfile` header comment.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the documented base images to Debian 13.

The Dockerfile now uses node:22.21.1-trixie-slim and gcr.io/distroless/nodejs22-debian13:nonroot, but this section still directs operators to the superseded Bookworm/Debian 12 pair. Update Lines 16 and 25 to match the digest-pinned runtime.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/PublishingImages.md` around lines 13 - 30, Update the production image
documentation in the three-stage Dockerfile description: change the
builder/proddeps base from node:22.21.1-bookworm-slim to
node:22.21.1-trixie-slim and the runtime base from distroless nodejs22-debian12
to nodejs22-debian13:nonroot. Keep the existing digest-pinning and stage
descriptions unchanged.

Source: Path instructions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant