build(seeder): shrink the image build context and cache the dependency layer

The seeder image build handed podman a build context full of files the
Dockerfile never reads, and reinstalled the entire dependency tree on every
release because the source copy came before `npm ci`.

Two changes, neither of which alters what is built:

- `.dockerignore` now excludes the desktop installer formats (*.AppImage,
  *.dmg, *.exe, *.deb, *.pkg) and the Start9 packaging output (*.s9pk plus
  seeder-launcher/start9/docker-images). The repo root accumulates a full set
  of installers on every release and none of it is referenced by the umbrel
  Dockerfile.
- The builder stage copies package.json, package-lock.json and patches/
  first, runs `npm ci`, and only then copies the sources. patches/ has to come
  along: postinstall runs patch-package, and without the directory the build
  still succeeds and quietly ships a seeder on unpatched autobase.

Measured on this machine, linux/amd64, podman:

  build context      2.5 GB -> 1015 MB
  cold build           224s -> 155s
  rebuild after a
  one-line src change  224s -> 35s

The rebuild figure is the real point: before, any source edit invalidated the
install layer, so an incremental rebuild cost exactly as much as a cold one.

Verified the output is unchanged rather than assuming it: the shipped /app
payload is byte-identical across 267 of its 268 files, and the one that
differs is host/brand.png, whose pixel signature is identical and whose 14
differing bytes are ImageMagick's embedded date chunks - non-deterministic on
every build, before this change and after. patch-package reports
autobase@7.25.1 applied in both. Image size is 365 MB either way. The amd64
image boots: worklet up, dashboard listening, /api/status 200. The arm64 leg
builds too, staging linux-arm64.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VzbDH8kk3vYuQ1CzNNz39h
This commit is contained in:
Your Name
2026-07-31 16:19:38 -05:00
co-authored by Claude Opus 5
parent 11f6759bcd
commit 8841044750
2 changed files with 29 additions and 3 deletions
+14
View File
@@ -21,6 +21,20 @@ assets/*.bundle
*.ipa
*.keystore
*.jks
# Desktop installers. These were missing, and the repo root accumulates every
# release's artifacts - 6.4 GB of them as of 2026-07-27 - so the whole lot was
# being tarred up and handed to podman on every seeder image build, twice (once
# per platform), for a Dockerfile that references none of it.
*.AppImage
*.dmg
*.exe
*.deb
*.pkg
# Start9 packaging output: the s9pk itself plus the exported image tars it is
# built from. Another 1.8 GB of build context the umbrel image never reads.
*.s9pk
*.s9pk.sha256
seeder-launcher/start9/docker-images
metadata
proposals
.github
+15 -3
View File
@@ -19,11 +19,23 @@
# ---- builder (always native via $BUILDPLATFORM) ----------------------------
FROM --platform=$BUILDPLATFORM node:22-bookworm AS builder
WORKDIR /src
# Full repo (see .dockerignore for what's excluded — notably node_modules, which
# we regenerate so the prebuilds match).
COPY . .
# Dependencies BEFORE sources, so the install layer is cached.
#
# This used to be `COPY . .` followed by `npm ci`, which meant ANY source change
# invalidated the install layer and reinstalled the whole dependency tree from
# scratch on every release — twice, once per platform. Copying just the manifests
# first lets the layer survive until a dependency actually moves.
#
# patches/ must come along: postinstall runs patch-package, and without the
# directory the autobase patch would silently not apply. That would not fail the
# build — it would quietly ship a seeder running unpatched autobase.
COPY package.json package-lock.json ./
COPY patches ./patches
# Regenerate node_modules so the worklet bundle + native prebuilds are present.
RUN npm ci --no-audit --no-fund
# Now the sources (see .dockerignore for what's excluded — notably node_modules,
# which we regenerate above so the prebuilds match).
COPY . .
# Stage the flat payload (bare, worklet/, host/ + qrcode, package.json) for the
# TARGET arch. TARGETARCH (amd64 | arm64) comes from the build platform; map it
# to the bare-runtime triple. SEEDER_VERSION (passed by the image build script)