Skip to content

Fail the image build when the Harper install fails - #637

Merged
kriszyp merged 5 commits into
mainfrom
kris/docker-install-guard
Jul 31, 2026
Merged

Fail the image build when the Harper install fails#637
kriszyp merged 5 commits into
mainfrom
kris/docker-install-guard

Conversation

@kriszyp

@kriszyp kriszyp commented Jul 31, 2026

Copy link
Copy Markdown
Member

What happened

v5.2.0-beta.4's published image contains no Harper. Containers on it crash-loop:

chown: cannot access '/home/harperdb/.npm-global/bin': No such file or directory
setpriv: failed to execute harper: No such file or directory

The @harperfast/harper-pro directory in that image holds 22 files, all of them the uWebSockets.js tree that the later COPY at Dockerfile:59 lays down. A good image has ~34,000. The npm install --global layer is 238 MB where beta.3's is 827 MB.

Root cause

RUN <<-EOF heredocs execute under sh with no set -e, so only the last command's exit status reaches Docker. The install block ends in cleanup:

RUN <<-EOF
  npm install --global harperfast-harper-pro-*.tgz   # <- failed
  rm harperfast-harper-pro-*.tgz
  mkdir -p /home/harperdb/harper
  chown harperdb:harperdb /home/harperdb/harper      # <- exit 0, layer "succeeds"
EOF

From the beta.4 release build log (run 30602239501, job build (24, linux/amd64)):

#14 [run 6/9] RUN <<-EOF (npm install --global harperfast-harper-pro-*.tgz...)
#14 13.70 npm error code ETARGET
#14 13.70 npm error notarget No matching version found for @aws-sdk/core@^3.977.4.

The trigger was a registry race, not a code change: the build resolved @aws-sdk/core@^3.977.4 at 03:45:28Z; that version was published at 03:46:55Z — 87 seconds later. The Dockerfile is byte-identical between beta.3 and beta.4.

All four standard build legs went green and the manifest was pushed. (The run is marked failed, but only because of the unrelated build-gpu legs — the standard image published normally.)

The fix

Two layers, because the failure has two independent halves — the error was thrown away, and nothing downstream noticed the result was empty:

  • set -e in every heredoc RUN across all three Dockerfiles (6 blocks; only the pointer-compression block at Dockerfile:71 already had it). This alone turns beta.4's build red.
  • harper version as a post-install assertion, so an install that exits 0 without producing a working bin still fails the build. set -e only catches a non-zero exit; this catches a silently incomplete one.

The non-install blocks get set -e too — same masking applies there, e.g. a failed apt-get install zstd would ship an image without zstd.

Verification

Heredoc masking, reproduced minimally — failing command followed by succeeding cleanup:

build result
without set -e succeeds (the bug)
with set -e fails, as it should

The assertion, run against the real published images:

image harper version in a RUN
5.2.0-beta.3 (good) passes — prints 5.2.0-beta.3, no false positive
5.2.0-beta.4 (broken) fails the build, exit code: 127

That's the same exit 127 the crash-looping containers hit — caught at build time instead of in production.

Notes for reviewers

  • Blast radius of the bad image is beta.4 only. I pulled and checked 5.1.24, 5.1.25, 5.1.26 and 5.2.0-beta.3 — all have a working harper bin. No customer release is affected.
  • 5.2.0-beta.4 on Docker Hub is still broken and this PR does not fix it. It needs a rebuild — @aws-sdk/core@3.977.4 resolves now, so a re-run should produce a good image. Worth deciding whether the tag gets re-pushed or pulled.
  • harper version runs as root in Dockerfile-openshift (that stage is USER 0). It's placed before the chown -R 1001:0, so anything it might touch is still swept up by the existing ownership fixup.
  • Possible follow-up, deliberately not in this PR: the standard merge job in publish-docker.yaml has no post-build smoke check, while merge-pc has one (Verify pointer compression and rebuilt uWS, line 229). A docker run ... harper version there would be an outer net catching a broken image from any cause, not just this layer. Happy to add it here or separately — say which.

🤖 Generated with Claude Code (Opus 5)

The `RUN <<-EOF` heredocs run under `sh` without `set -e`, so only the last
command's exit status reaches Docker. A failed `npm install --global` was
masked by the `rm`/`mkdir`/`chown` cleanup that follows it, and the layer
exited 0.

v5.2.0-beta.4 shipped that way: `npm install --global` died with ETARGET
(`@aws-sdk/core@^3.977.4` was published 87s after the build resolved it), all
four standard build legs went green, and the pushed image contained no harper
at all — only the `uWebSockets.js` tree a later COPY laid down. Containers on
it crash-loop with `setpriv: failed to execute harper: No such file or
directory` (exit 127).

Add `set -e` to every heredoc RUN in the three Dockerfiles, and assert the
install with `harper version` so an install that exits 0 without producing a
working bin also fails the build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@kriszyp
kriszyp requested a review from sleekmountaincat July 31, 2026 13:09
@claude

claude Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Reviewed; no blockers found.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request updates Dockerfile, Dockerfile-gpu, and Dockerfile-openshift to include set -e in several RUN blocks, ensuring that failures during the build process are not masked. It also adds a harper version check immediately after the global npm installation of Harper Pro to verify that the binary resolves and runs successfully. I have no feedback to provide as there are no review comments.

@kriszyp
kriszyp requested review from DavidCockerill and cb1kenobi and removed request for sleekmountaincat July 31, 2026 13:30
@kriszyp
kriszyp marked this pull request as ready for review July 31, 2026 13:30
@kriszyp
kriszyp requested a review from a team as a code owner July 31, 2026 13:30
kriszyp added 3 commits July 31, 2026 08:49
GPU image builds have failed on every 5.2 beta since beta.2 with:

  ERROR: failed to build: failed to solve:
  base name (${CUDA_RUNTIME_IMAGE}) should not be blank

An ARG referenced by a FROM must be declared in the global scope, before any
stage. #496 added `ARG CUDA_RUNTIME_IMAGE` immediately above the `FROM
${CUDA_RUNTIME_IMAGE} AS run` line, but that position is after `FROM ... AS
build`, so the ARG belonged to the build stage and the later FROM expanded it
to an empty base name.

Moves the ARG (with its comment) above the first FROM, alongside
NODE_BUILD_VERSION and NODE_VERSION, which are already global for exactly this
reason. The default and the override contract are unchanged — a T4 host pinned
to an older driver branch still overrides with
`--build-arg CUDA_RUNTIME_IMAGE=nvidia/cuda:12.6.3-cudnn-runtime-ubuntu22.04`.

Added a note on the ordering constraint so it does not regress.

Only the GPU images were affected; standard, pointer-compression and OpenShift
builds do not use this ARG and have been publishing normally. 5.1.x is
unaffected — it has no CUDA_RUNTIME_IMAGE ARG at all.
The CUDA runtime base was ubuntu22.04 (glibc 2.35). This image bundles
uWebSockets.js, copied from the build stage, and its prebuilt Linux binaries
link against GLIBC_2.38 — so the addon could not load on that base. It is the
same failure documented at the top of ./Dockerfile, which is why the standard
image runs on Debian trixie rather than bookworm (glibc 2.36).

Switches the default to nvidia/cuda:13.3.0-cudnn-runtime-ubuntu24.04
(glibc 2.39) and updates the CUDA 12.x override example to the matching
ubuntu24.04 tag. All four tag combinations were confirmed present on Docker Hub.

Documents the glibc floor next to the ARG: the point of this ARG is that
operators override it for older driver branches, and an override back to an
ubuntu22.04 base would silently break uWS again.

Latent until now — the build has failed outright since #496, so this base was
never actually produced.

Credit: flagged by gemini-code-assist on #628.
Two problems the ubuntu24.04 base exposed.

1. Ubuntu 24.04 images ship a default `ubuntu` user at uid/gid 1000, so
   `groupadd --gid 1000` / `useradd --uid 1000` failed:

     groupadd: GID 1000 already exists
     useradd: UID 1000 is not unique

   harperdb has to own 1000 — existing deployments chown their data volumes to
   it — so rename the incumbent rather than moving harperdb to another id. This
   is what ./Dockerfile already does for the node image's `node` user. Handles
   both shapes, since CUDA_RUNTIME_IMAGE is overridable and 22.04 bases ship no
   uid-1000 user.

2. The setup block had no `set -e`, so both failures above were swallowed: the
   block continued and exited 0 on the trailing `rm -rf`, producing an image
   with no harperdb user. That surfaced much later and far from the cause, as

     unable to find user harperdb: no matching entries in passwd file

   on the pnpm step. Adding `set -e` makes the block fail at the real cause.

Verified the reclaim logic under sh with both base shapes (uid 1000 present and
absent) — the rename path and the create path each exit 0.
@cb1kenobi

Copy link
Copy Markdown
Member

Reviewed 26d48801 — no issues found. This PR looks good, nice job!

Incremental re-review since 0c453f56 (3 new commits, all in Dockerfile-gpu): fixed the CUDA_RUNTIME_IMAGE ARG global-scope bug, switched the runtime base to ubuntu24.04 to match the bundled uWS's glibc requirement, and reclaimed uid/gid 1000 from the base image's ubuntu user rather than colliding with it. The set -e + harper version assertion in the "Install Harper Pro globally" block — the core fix this PR is about — is unchanged. No || true was reintroduced around anything that matters: the two added getent ... || true guards sit in a pipeline without pipefail, so their exit status is already cut's (always 0) — they're inert, not error-masking. No unrelated changes.


Generated by Barber AI

@DavidCockerill DavidCockerill left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approving — and thanks for turning this round so fast off the #628 thread.

Checked completeness rather than the idea: all 7 heredoc RUN blocks across the three Dockerfiles now carry set -e, and the harper version assertion is positioned inside the install block — after the install, before cleanup — so a failure actually fails the build rather than deferring to container start.

One gap left in the thread, same bug class but not a heredoc so set -e can't reach it: the two wget … | bash - pnpm pipelines.

— Reviewed by DAIvid (Claude Opus 5)

Comment thread Dockerfile
# exit status of the cleanup commands that follow it, and `harper version` asserts
# the installed bin actually resolves and runs: v5.2.0-beta.4 published an image
# with no harper in it off a green build when npm install hit a transient ETARGET.
RUN <<-EOF

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Non-blocking, and about line 43 above rather than this hunk (same shape in Dockerfile-gpu:53): RUN wget -qO- https://get.pnpm.io/install.sh | ... bash - is the last uncovered instance of the bug this PR closes.

A pipeline's exit status is the RHS's, so a failed download hands bash - an empty stdin and it exits 0 — image ships with no pnpm, build green. set -e can't help here; the RUN already succeeded. There's no SHELL directive in any of the three files, so RUN is /bin/sh -c (dash, no pipefail), and nothing sets pipefail anywhere in the repo.

Narrower blast radius than the Harper case — pnpm only matters for components declaring devEngines.packageManager — but worth closing: download to a file and run it, or move it into a heredoc with an explicit bash SHELL and set -eo pipefail.

If wget fails, bash runs on empty input and exits 0, so the build
continued without pnpm. Download to a file first so each step's
failure fails the build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kriszyp

kriszyp commented Jul 31, 2026

Copy link
Copy Markdown
Member Author

Addressed the remaining gap from the review: the two wget … | bash - pnpm pipelines in Dockerfile and Dockerfile-gpu now download to a file first inside a set -e heredoc (d05b0b8), so a wget failure fails the build instead of silently running bash on empty input.

— KrAIs (Claude Fable 5)

@kriszyp
kriszyp merged commit d91de64 into main Jul 31, 2026
27 checks passed
@kriszyp
kriszyp deleted the kris/docker-install-guard branch July 31, 2026 19:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants