From 27a29ee54e93e98fea5f542d5ab393d42b2ca196 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 27 May 2026 16:10:34 +1000 Subject: [PATCH] feat(docker): upgrade Node to 22 LTS via multi-stage from node:22-bookworm-slim (#4977) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Debian trixie's bundled `nodejs` package is pinned to 20.19.2, which reached LTS EOL in April 2026. Trixie won't upgrade in place; Debian 14 (forky) — where the apt nodejs is 24.x — isn't released until ~mid-2027. To stay on a supported LTS without waiting for Debian 14, copy node + npm + corepack from the upstream `node:22-bookworm-slim` image as a multi-stage source, matching the existing `uv_source` and `gosu_source` patterns in the Dockerfile. Bookworm-based slim image is used so the produced binary links against glibc 2.36, which runs cleanly on Debian 13 (trixie, glibc 2.41). Changes: - Add `FROM node:22-bookworm-slim@sha256:... AS node_source` stage - Remove `nodejs npm` from `apt-get install` (now sourced from node_source) - Add `ca-certificates` explicitly to apt install (was a transitive of the apt nodejs package; removing nodejs broke the chain and curl inside the build failed with "error setting certificate file") - COPY node binary + npm + corepack from node_source; recreate the symlinks at /usr/local/bin/{npm,npx,corepack} - Update the npm_config_install_links=false comment block — npm 10's default is already `install-links=false`, but we keep the env as defense-in-depth against future Node-source-version regressions Future bumps to Node 24/26 are a one-line ARG change. Validation: - Built --no-cache against current origin/main; build succeeds in 1m42s - Image size: 3.27 GB (pre-salvage-1 baseline) → 3.14 GB (this PR); net 130 MiB savings (60 MiB from this change alone vs current main — removing apt nodejs+transitive deps that duplicated what node bundles) - Node 22.22.3 / npm 10.9.8 / esbuild 0.27.7 all run cleanly under trixie's glibc 2.41 - Standard image smoke (6/6), Node-version E2E (8/8), chown E2E from #19788 (6/6), TUI UID-remap E2E from #28851 (4/4) — 24 checks total Co-authored-by: Prithvi Monangi <8312237+Prithvi1994@users.noreply.github.com> --- Dockerfile | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index 52f9e22a220..d335f848325 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,12 @@ FROM ghcr.io/astral-sh/uv:0.11.6-python3.13-trixie@sha256:b3c543b6c4f23a5f2df22866bd7857e5d304b67a564f4feab6ac22044dde719b AS uv_source +# Node 22 LTS source stage. Debian trixie's bundled nodejs is pinned to 20.x +# which reached EOL in April 2026 — we copy node + npm + corepack from the +# upstream node:22 image instead so we can stay on a supported LTS without +# waiting for Debian 14 (forky, ~mid-2027). Bookworm-based slim image used +# so the produced binary links against glibc 2.36, which runs cleanly on +# our Debian 13 (trixie, glibc 2.41) runtime. Bumping to a new Node major +# is a one-line ARG change; see #4977. +FROM node:22-bookworm-slim@sha256:7af03b14a13c8cdd38e45058fd957bf00a72bbe17feac43b1c15a689c029c732 AS node_source FROM debian:13.4 # Disable Python stdout buffering to ensure logs are printed immediately @@ -17,7 +25,7 @@ ENV PLAYWRIGHT_BROWSERS_PATH=/opt/hermes/.playwright # hermes process, the dashboard, and per-profile gateways. RUN apt-get update && \ apt-get install -y --no-install-recommends \ - curl nodejs npm python3 ripgrep ffmpeg gcc python3-dev libffi-dev procps git openssh-client docker-cli xz-utils && \ + ca-certificates curl python3 ripgrep ffmpeg gcc python3-dev libffi-dev procps git openssh-client docker-cli xz-utils && \ rm -rf /var/lib/apt/lists/* # ---------- s6-overlay install ---------- @@ -72,6 +80,18 @@ RUN useradd -u 10000 -m -d /opt/data hermes COPY --chmod=0755 --from=uv_source /usr/local/bin/uv /usr/local/bin/uvx /usr/local/bin/ +# Node 22 LTS: copy the node binary plus the bundled npm + corepack JS +# installs from the upstream image. npm and npx are recreated as symlinks +# because they're symlinks in the source image (and need to live on PATH). +# See node_source stage at the top of the file for the version-bump +# rationale (#4977). +COPY --chmod=0755 --from=node_source /usr/local/bin/node /usr/local/bin/ +COPY --from=node_source /usr/local/lib/node_modules/npm /usr/local/lib/node_modules/npm +COPY --from=node_source /usr/local/lib/node_modules/corepack /usr/local/lib/node_modules/corepack +RUN ln -sf /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm && \ + ln -sf /usr/local/lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx && \ + ln -sf /usr/local/lib/node_modules/corepack/dist/corepack.js /usr/local/bin/corepack + WORKDIR /opt/hermes # ---------- Layer-cached dependency install ---------- @@ -88,14 +108,15 @@ COPY ui-tui/package.json ui-tui/package-lock.json ui-tui/ COPY ui-tui/packages/hermes-ink/ ui-tui/packages/hermes-ink/ # `npm_config_install_links=false` forces npm to install `file:` deps as -# symlinks (the npm 10+ default) even on Debian's older bundled npm 9.x, -# which defaults to `install-links=true` and installs file deps as *copies*. -# The host-side package-lock.json is generated with a newer npm that uses -# symlinks, so an install-as-copy produces a hidden node_modules/.package-lock.json -# that permanently disagrees with the root lock on the @hermes/ink entry. -# That disagreement trips the TUI launcher's `_tui_need_npm_install()` -# check on every startup and triggers a runtime `npm install` that then -# fails with EACCES (node_modules/ is root-owned from build time). +# symlinks instead of copies. This is the default since npm 10+, which is +# what the image ships now (via the node:22 source stage). We set it +# explicitly anyway as defense-in-depth: the previous Debian-bundled npm +# 9.x defaulted to install-as-copy, which produced a hidden +# node_modules/.package-lock.json that permanently disagreed with the root +# lock on the @hermes/ink entry, tripped the TUI launcher's +# `_tui_need_npm_install()` check on every startup, and triggered a +# runtime `npm install` that then failed with EACCES. Keeping the env +# guards against a future regression if the source npm version changes. ENV npm_config_install_links=false RUN npm install --prefer-offline --no-audit && \