Skip to content

fix(docker): trixie base so arm64 native prebuilds load; execute bindings per-arch at build time - #384

Merged
aliasunder merged 2 commits into
mainfrom
fix/arm64-image-glibc
Jul 30, 2026
Merged

fix(docker): trixie base so arm64 native prebuilds load; execute bindings per-arch at build time#384
aliasunder merged 2 commits into
mainfrom
fix/arm64-image-glibc

Conversation

@aliasunder

Copy link
Copy Markdown
Owner

Problem

Every arm64 image since v0.33.0 (July 25) crash-loops at startup — all Apple Silicon local-mode users are affected:

/lib/aarch64-linux-gnu/libm.so.6: version 'GLIBC_2.38' not found
(required by /app/node_modules/better-sqlite3/prebuilds/linux-arm64.node)

better-sqlite3 v13 (Dependabot #369) bundles its prebuilds in the npm tarball, and the linux-arm64 prebuild requires glibc >= 2.38 while the bookworm base ships 2.36. amd64 was unaffected only because its prebuild's glibc floor happens to fit — which is why prod (Lightsail, amd64) never surfaced it and no CI check caught it: nothing ever executes the arm64 artifact.

Why the existing defense failed

The deps stage ran npm ci --ignore-scripts && npm rebuild better-sqlite3 onnxruntime-node precisely to compile bindings against the image's libc. Two independent failures made it dead weight:

  1. npm's allow-scripts gating silently no-ops npm rebuild — it prints a warning, reports "rebuilt dependencies successfully", and exits 0 without building anything. Reproduced in a clean container at the exact pinned base digest.
  2. v13 tarballs no longer produce a working binding from source — even with scripts approved and build tools present, the compile completes without linking a .node (upstream moved to bundled prebuilds; 13.0.2 sets gypfile: false, making source builds a permanent no-op).

Both shipped image arches contain no compiled binding at all — they run on bundled prebuilds, so amd64 is also one upstream prebuild-toolchain bump away from the same crash.

Fix

  • Base image → node:24-trixie-slim (glibc 2.41 ≥ 2.38), digest-pinned as before. Aligns with upstream's prebuild-only direction instead of fighting it.
  • Replace the dead rebuild with an executed assertion: the deps stage RUNs new Database(':memory:') + require('onnxruntime-node'). Release builds are linux/amd64,linux/arm64, so this executes on both architectures (arm64 under QEMU) — a glibc/arch mismatch now fails the build on the affected arch instead of shipping a crash-looping image. This makes per-arch validation a hard release gate.
  • Dropped the now-unused python3 make g++ from the deps stage (the build stage keeps its own copy).
  • Docs lockstep: AGENTS.md + ARCHITECTURE.md base-image references updated with the glibc-floor rationale.

Validation

  • arm64 local target: built with the assertion passing, booted via the CLI against a scaffolded .env — healthz ok, authenticated MCP initialize returns serverInfo, index rebuild + embedding pass complete (both native bindings exercised for real).
  • amd64 deps stage: built under --platform linux/amd64 emulation — assertion passes, proving the cross-arch gate works exactly as CI will run it.
  • remote target: builds cleanly on trixie (s6-overlay static binaries, obsidian-headless npm ci, UID-1000 user swap all unaffected).
  • Regression window confirmed by probing released images: 0.32.0 (better-sqlite3 12.x) opens a database on arm64; 0.33.2 and :latest do not. Note: bare require('better-sqlite3') passes on broken images — v13 defers the native load — so the assertion opens a real database.

🤖 Generated with Claude Code

…ings per-arch at build time

Every arm64 image since v0.33.0 crash-loops at startup:
better-sqlite3 v13's bundled linux-arm64 prebuild requires glibc >= 2.38
and the bookworm base ships 2.36. amd64 was unaffected only because its
prebuild's glibc floor happens to fit — prod never saw it.

The deps stage's `npm rebuild better-sqlite3 onnxruntime-node` defense
stopped working silently: npm's allow-scripts gating skips the build,
prints a warning, and exits 0 (reproduced at the pinned digest), and v13
tarballs no longer produce a working binding from source anyway (compile
completes without linking; upstream moved to bundled prebuilds and set
gypfile:false in 13.0.2).

Fix:
- Base image node:24-slim (bookworm, glibc 2.36) -> node:24-trixie-slim
  (glibc 2.41), digest-pinned as before.
- Replace the dead rebuild with an executed assertion: the deps stage
  RUNs new Database(':memory:') and require('onnxruntime-node'). Release
  builds are linux/amd64 + linux/arm64, so this executes on both arches
  (arm64 under QEMU) and a glibc/arch mismatch fails the build instead
  of shipping a crash-looping image.
- Drop the now-unused python3/make/g++ from the deps stage (the build
  stage keeps its own).

Validated locally: arm64 local target builds and serves (healthz, MCP
initialize, index rebuild, embedding pass — both bindings exercised);
amd64 deps stage passes under emulation; remote target builds on trixie.

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

umm-actually Bot commented Jul 30, 2026

Copy link
Copy Markdown

PR Code Suggestions ✨

No code suggestions found for the PR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@aliasunder
aliasunder merged commit 1afd2ac into main Jul 30, 2026
15 checks passed
@aliasunder
aliasunder deleted the fix/arm64-image-glibc branch July 30, 2026 21:21
aliasunder added a commit that referenced this pull request Jul 31, 2026
## Motivation

Every arm64 image from v0.33.0 through v0.33.4 crash-looped at startup
(better-sqlite3 v13's linux-arm64 prebuild requires glibc >= 2.38; the
bookworm base had 2.36) and stayed invisible for five days across four
releases: prod is amd64, and trivy-pr — the only PR-time image build —
builds amd64 only, so nothing ever *executed* an arm64 artifact before
release. PR #384 fixed the image and added the deps-stage assertion that
executes both native bindings per-arch at build time. This PR makes
executed-per-arch a PR gate.

## What the job does

New `Arch Smoke` workflow, matrix over native runners (`ubuntu-latest`
amd64, `ubuntu-24.04-arm` arm64 — no QEMU). Each leg:

1. **`docker build --target local`** — natively executes the Dockerfile
deps-stage native-binding assertion for its arch (`new
Database(':memory:')` + `require('onnxruntime-node')`).
2. **Boot smoke** — runs the image against a small inline fixture vault
(frontmatter, tags, wikilink pair, task line — real FTS inserts) with
`EMBEDDING_ENABLED=false` / `MEMORY_ENABLED=false` and a per-run
generated token. `/healthz` 200 is a true proof of the incident class:
`server.ts` opens the search DB, awaits the blocking FTS rebuild, and
opens the OAuth DB before `app.listen`, exiting 1 on any failure. Then
an authenticated MCP `initialize` round-trip is asserted (HTTP 2xx +
`mcp-session-id` header + `serverInfo` in the body — a JSON-RPC error
still returns 200, hence the body assertion).
3. **arm64 only**: `docker build --target remote` as a compile check —
the first-ever arm64 build of the remote target at PR time. Build-only:
booting remote needs Obsidian Sync credentials, and amd64 remote
coverage is already trivy-pr's job.

## Design notes

- **Check contexts are `arch-smoke (amd64)` / `arch-smoke (arm64)`** —
explicit job `name` with a friendly arch key, so the ruleset contexts
stay stable if runner labels ever change. Adding them to the branch
ruleset is a follow-up settings change after this merges.
- **Cache: explicit per-arch `scope=arch-smoke-<arch>`**, a deliberate
divergence from the repo's bare `type=gha`: trivy-pr (two targets) plus
both smoke legs writing `mode=max` into the one default scope would
evict each other — worst for arm64, which shares no blobs with the amd64
writers. `cache-from` also lists the bare default scope so the amd64 leg
piggybacks trivy-pr's layers (no-op on arm64). The `APT_UPGRADE_DATE`
cache-bust step is reused verbatim from trivy.yml — a differing
build-arg would change the apt-upgrade layer hash and break that
sharing.
- **No path filters** — a required check gated by `paths:` leaves
docs-only PRs stuck on "Expected" forever; trivy-pr also runs
unconditionally.
- **`push: main` trigger** seeds the per-arch cache into the
default-branch scope (readable by all PRs; PR-branch caches are not
shared across PRs) and acts as a post-merge canary.
- **Crash-loop fast-fail**: the healthz poll checks `docker inspect
'{{.State.Running}}'` each iteration, so the incident's exact failure
mode (startup `process.exit(1)`) fails in ~2s instead of burning the 60s
poll window. Diagnostics step (`if: failure()`) dumps `docker ps -a`,
container state JSON, and logs.
- **`timeout-minutes: 20`** is a kill-switch cap (default is 360), not
the expected runtime — expected ~3-5 min warm-cache, ~8-12 min cold,
legs parallel.
- **`fail-fast: false`** — arch failures are independent signals; one
leg must not cancel the other.

## Verification

- `actionlint`: zero findings on the new workflow; `npm run
prettier:check` green.
- **Full smoke executed locally on native arm64** (Apple Silicon, the
incident-class arch): the exact script block passes under `bash -e` in
2.2s — healthz ready on attempt 2, MCP initialize round-trip clean, and
the initialize curl exits naturally when the SSE response stream closes
(no `--max-time` timeout).
- **Failure path mutation-tested locally**: booting the container
without `MCP_AUTH_TOKEN` (startup exit 1) trips the crash-detection
branch on poll attempt 1 with the startup error visible in `docker
logs`.
- On this PR: both legs run for the first time in CI (new `pull_request`
workflows run from the PR branch). A deliberate red push (probe pointed
at a wrong port) will verify the gate bites and the diagnostics render,
then be reverted.

Also updates CONTRIBUTING.md's required-checks item to name the new
checks.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Tests**
* Added automated smoke testing for native amd64 and arm64 Docker
builds.
* Tests now boot images, verify health and authenticated initialization,
and collect diagnostics on failure.

* **Documentation**
* Updated pull request guidance to note that architecture smoke test
failures block merging.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.

1 participant