Skip to content

Multi-arch notebook images (native arm64), non-root user, prebaked font cache - #182

Merged
bendichter merged 2 commits into
masterfrom
multiarch-notebook-images
Aug 20, 2026
Merged

Multi-arch notebook images (native arm64), non-root user, prebaked font cache#182
bendichter merged 2 commits into
masterfrom
multiarch-notebook-images

Conversation

@bendichter

Copy link
Copy Markdown
Member

Validating the pilot image (#180) on Apple Silicon surfaced a failure mode worse than slowness: Docker's Rosetta emulation can deadlock on subprocess spawns from a threaded x86-64 process. In practice that meant the first import matplotlib.pyplot froze the kernel forever (its font-cache build shells out to fc-list), and restarting the kernel wedged the whole Jupyter server while it tried to spawn the replacement kernel process. py-spy stack dumps against the live container pinpointed both. Since a large share of our users are on Apple Silicon Macs, amd64-only images are not shippable.

Changes

Multi-arch images, built and verified natively. The workflow now runs each group's build on two native runners (ubuntu-latest for amd64, ubuntu-24.04-arm for arm64, which is free for public repos), verifies every notebook inside each per-arch candidate with the unmodified harness, pushes hash-<h>-amd64 / hash-<h>-arm64 images, and a merge job assembles them into one multi-arch manifest carrying the user-facing tags (latest, sha-, date, hash-). The per-arch hash tags double as skip markers, so unchanged groups still cost nothing to rebuild. There is no emulation anywhere in the pipeline, and docker run on a Mac now picks the native image automatically.

Non-root user. The server now runs as jovyan (uid 1000) rather than root; the JupyterLab tool env moves from /root/.local to /opt so it stays readable. CI verification passes --user root so the harness's transient installs can write to the system Python; the published default stays non-root.

Font cache prebaked. Matplotlib's font cache is built at image-build time (as jovyan), so the first import in a fresh container no longer stalls on it. This also removes the exact runtime subprocess spawn that deadlocked under Rosetta, for anyone who does still run the amd64 image emulated.

Compiler during pin install. gcc/libc6-dev are installed for the pin layer and purged in the same layer, covering pins with no wheel on the target architecture (the pilot's psutil==5.9.5 has no arm64 wheel).

This includes the docs commit from #181 (loopback port binding), which can be closed if this merges first. One correction to #180's description: the pilot image is about 1.7 GB uncompressed, not 380 MB; that figure was a misreading during the first local build.

Verification

Built the arm64 image natively on an M-series Mac with the new Dockerfile: both pilot notebooks verify green inside it (7 to 8 seconds each), and kernel execution over the websocket as jovyan completes in milliseconds where the emulated image hung indefinitely: a = 1 in 0.05s, a subprocess spawn in 0.04s, import matplotlib.pyplot in 0.62s with the prebaked cache. The amd64 path reuses the already-exercised build and verify steps from #180's pilot runs.

After merge: dispatch the workflow with force for the pilot filter so both arches rebuild and the multi-arch manifest replaces the amd64-only tags, then continue the fleet rollout.

🤖 Generated with Claude Code

bendichter and others added 2 commits August 19, 2026 20:53
A bare -p 8888:8888 binds all interfaces, and on macOS it coexists with
a local Jupyter server already listening on 127.0.0.1:8888, so the
browser silently reaches the wrong server and shows a confusing 404.
Binding 127.0.0.1 explicitly keeps the container off network interfaces
and turns a port collision into a loud "address already in use" error.
Found during the pilot pull-and-run validation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pilot validation on Apple Silicon showed amd64-only images are unreliable
under Rosetta: subprocess spawns from the threaded kernel process can
deadlock, which surfaced as cells hanging forever on the first matplotlib
import (its font-cache build shells out to fc-list) and as the server
wedging while spawning a restarted kernel. Emulation is the problem, so
the fix is native images for both architectures.

The workflow now builds and verifies each group on native runners
(ubuntu-latest for amd64, ubuntu-24.04-arm for arm64), pushes per-arch
hash-tagged images, and merges them into one multi-arch manifest that
carries the user-facing tags. No emulation anywhere in the pipeline.

The image also gains a non-root jovyan user (uid 1000; the JupyterLab
tool env moves to /opt so it stays readable) and builds the matplotlib
font cache at image-build time so first imports don't stall on it. CI
verification runs the container with --user root so the harness's
transient installs can still write to the system Python; gcc is
available during the pin install (purged in the same layer) for pins
without a wheel on the target arch, such as old psutil on arm64.

Verified natively on arm64: both pilot notebooks green inside the image,
and kernel execution over the websocket completes in milliseconds,
including subprocess spawns and the matplotlib import.

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

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Preview for this PR has been removed (PR closed).

github-actions Bot added a commit that referenced this pull request Aug 20, 2026
@bendichter
bendichter merged commit 62fcaef into master Aug 20, 2026
3 checks passed
github-actions Bot added a commit that referenced this pull request Aug 20, 2026
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