feat: canonical Node 24 base image with sqlite3 bundled

Node 24.18.0 (LTS Krypton) on alpine. sqlite3 bundled two ways, both
proven at build time: require('node:sqlite') builtin works, and native
better-sqlite3/sqlite3 compile against build-base+python3+sqlite-dev via
node-gyp. One base, one way: services do FROM ghcr.io/hanzoai/nodejs:v24.18.0.

Self-contained CI on hanzo-build-linux-amd64, amd64-only, semver-only
(vX.Y.Z + :v24), never sha/latest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
z
2026-07-04 12:32:29 -07:00
co-authored by Claude Fable 5
commit 3c89724d75
6 changed files with 168 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
.git
.github
*.md
+62
View File
@@ -0,0 +1,62 @@
name: Docker
# Canonical Node base image (Node 24 + sqlite3) for all Hanzo Node services.
# Self-contained build on the Hanzo self-hosted ARC pool (hanzo-build-linux-amd64).
# Semver only: a `v*` git tag publishes ghcr.io/hanzoai/nodejs:<tag> + the moving
# major tag :v24. NEVER :latest / :sha (per Hanzo semver-only policy).
# amd64-only — DOKS has no arm64 droplets (LLM.md, 2026-04-27).
on:
workflow_dispatch:
push:
tags: ['v*']
permissions:
contents: read
packages: write
jobs:
docker:
# ARC v0.14 routes by scale-set NAME, not labels — name the pool directly.
runs-on: hanzo-build-linux-amd64
steps:
- uses: actions/checkout@v4
- name: Compute tags
id: tags
run: |
set -euo pipefail
REF="${GITHUB_REF_NAME}"
case "$REF" in
v[0-9]*) VER="$REF" ;; # tag push: v24.18.0
*) VER="v$(grep -oE 'node:[0-9]+\.[0-9]+\.[0-9]+' Dockerfile | head -1 | cut -d: -f2)" ;;
esac # dispatch: derive from FROM pin
MAJOR="$(echo "$VER" | grep -oE '^v[0-9]+')"
echo "ver=$VER" >> "$GITHUB_OUTPUT"
echo "major=$MAJOR" >> "$GITHUB_OUTPUT"
echo "Publishing: ghcr.io/hanzoai/nodejs:$VER + :$MAJOR"
- uses: docker/setup-buildx-action@v3
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push (linux/amd64)
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64
push: true
tags: |
ghcr.io/hanzoai/nodejs:${{ steps.tags.outputs.ver }}
ghcr.io/hanzoai/nodejs:${{ steps.tags.outputs.major }}
cache-from: type=gha,scope=hanzoai-nodejs
cache-to: type=gha,scope=hanzoai-nodejs,mode=max
- name: Verify sqlite3 in the published image
run: |
set -euo pipefail
IMG="ghcr.io/hanzoai/nodejs:${{ steps.tags.outputs.ver }}"
docker pull "$IMG"
docker run --rm "$IMG" node -v
docker run --rm "$IMG" node -e "require('node:sqlite');console.log('node:sqlite loads on '+process.version)"
+2
View File
@@ -0,0 +1,2 @@
node_modules/
*.log
+36
View File
@@ -0,0 +1,36 @@
# hanzoai/nodejs — the ONE canonical Node base image for every Hanzo Node service.
#
# Node 24 (LTS "Krypton") + sqlite3 bundled two ways, BOTH proven at build time:
# 1. node:sqlite — the built-in DatabaseSync (stable since Node 22.5; present in
# 24). `require('node:sqlite')` works out of the box, zero dependencies.
# 2. native npm sqlite — better-sqlite3 / sqlite3 compile against the baked-in
# toolchain (build-base + python3 + sqlite-dev) through node-gyp.
#
# One base, one way. Services do: FROM ghcr.io/hanzoai/nodejs:v24.18.0
# Bump the pinned node:<ver>-alpine below to move the whole fleet's runtime;
# the git tag (vX.Y.Z) must match the Node version so consumers can reason about it.
FROM node:24.18.0-alpine
# Native addon build toolchain (node-gyp needs python3 + make + g++, all via
# build-base), the system sqlite CLI + headers, and the glibc shim so prebuilt
# (glibc) native addons load on musl. corepack ships pnpm/yarn shims.
RUN apk add --no-cache \
build-base \
python3 \
sqlite \
sqlite-dev \
libc6-compat \
&& corepack enable
# Build-time proof gate — the image FAILS TO BUILD if either sqlite path breaks.
# (1) node:sqlite builtin round-trips an in-memory DB.
# (2) better-sqlite3 compiles from source against the toolchain and round-trips.
# The verify scratch dir is removed so it adds no weight to the published layer.
RUN node -e "const {DatabaseSync}=require('node:sqlite');const d=new DatabaseSync(':memory:');d.exec('create table t(x int)');d.prepare('insert into t values (?)').run(7);if(d.prepare('select x from t').get().x!==7)throw new Error('node:sqlite roundtrip failed');console.log('node:sqlite OK on '+process.version);" \
&& mkdir -p /tmp/verify && cd /tmp/verify && npm init -y >/dev/null 2>&1 \
&& npm install --no-audit --no-fund better-sqlite3 >/dev/null 2>&1 \
&& node -e "const D=require('better-sqlite3');const db=new D(':memory:');db.exec('create table t(x int)');db.prepare('insert into t values (?)').run(42);if(db.prepare('select x from t').get().x!==42)throw new Error('better-sqlite3 roundtrip failed');console.log('better-sqlite3 '+require('better-sqlite3/package.json').version+' OK');" \
&& cd / && rm -rf /tmp/verify /root/.npm
# Match the upstream node image default so this is a transparent drop-in.
CMD ["node"]
+33
View File
@@ -0,0 +1,33 @@
# LLM.md — hanzoai/nodejs
Canonical Node base image for the whole Hanzo Node fleet. One base, one way.
## What this is
`FROM node:24.18.0-alpine` + the sqlite3 toolchain, published as
`ghcr.io/hanzoai/nodejs:v24.18.0` (+ `:v24`). Every Hanzo Node service does
`FROM ghcr.io/hanzoai/nodejs:v24.18.0` so Node 24 and sqlite3 are uniform.
## sqlite3, two ways (both build-gated)
- `require('node:sqlite')` (builtin `DatabaseSync`, stable Node ≥22.5) — no deps.
- `better-sqlite3` / `sqlite3` compile via node-gyp against `build-base` +
`python3` + `sqlite-dev`. The Dockerfile compiles better-sqlite3 at build time
and round-trips it; the image fails to build if that breaks.
## Why alpine
Majority of Hanzo Node services already use `node:*-alpine`. better-sqlite3
compiles cleanly on musl with `build-base`; `node:sqlite` is builtin regardless.
`libc6-compat` lets prebuilt glibc addons load. Debian-slim consumers
(paas, platform, base-studio, market, embeddings, store-api, insights, gallery)
need an `apt-get``apk` port before switching to this base — tracked as the sweep tail.
## Release / bump
1. Edit the `FROM node:<X.Y.Z>-alpine` pin in `Dockerfile` to move the runtime.
2. Tag `vX.Y.Z` (must equal the Node version). CI publishes `:vX.Y.Z` + `:vX`.
3. Consumers bump their `FROM ghcr.io/hanzoai/nodejs:vX.Y.Z` pin on their own release.
CI: `.github/workflows/build.yml`, self-contained, routes to `hanzo-build-linux-amd64`.
amd64-only (DOKS has no arm64). Semver only, never sha/latest.
+32
View File
@@ -0,0 +1,32 @@
# hanzoai/nodejs
The **one** canonical Node base image for every Hanzo Node service.
- **Node 24** (LTS "Krypton", pinned `node:24.18.0-alpine`)
- **sqlite3 bundled, two ways** — both proven at build time:
1. `require('node:sqlite')` — the built-in `DatabaseSync` (Node ≥ 22.5). Zero deps.
2. Native npm sqlite (`better-sqlite3`, `sqlite3`) compiles against the baked-in
toolchain (`build-base` + `python3` + `sqlite-dev`) via node-gyp.
## Use
One way. In any Hanzo Node service's Dockerfile:
```dockerfile
FROM ghcr.io/hanzoai/nodejs:v24.18.0
```
Pin the exact semver (`v24.18.0`). The moving major tag `v24` also exists but
consumers must pin the patch for reproducible builds.
## Verify
```sh
docker run --rm ghcr.io/hanzoai/nodejs:v24.18.0 node -e "require('node:sqlite');console.log('ok')"
```
## Release
Tag `vX.Y.Z` where `X.Y.Z` matches the pinned Node version in the `Dockerfile`.
CI (`hanzo-build-linux-amd64`, self-hosted) builds `linux/amd64` and publishes
`ghcr.io/hanzoai/nodejs:vX.Y.Z` + `:vX`. Never `:latest`, never `:sha`.