Minimize per-deploy NAS transfer — lockfile-pinned deps base image + app-layer split
Why¶
The NAS sits behind a cellular-SIM Deco (I-009) — a slow, unstable, likely-metered link. Pulling the
~4.8 GB app image over it is the recurring deploy bottleneck (2026-06-24: a deploy hung ~an hour on the 2 GB
node_modules layer alone).
Owner requirement (verbatim, 2026-06-24): "I'd also like the downloading and uploading from and to the NAS per deployment to be as little as possible."
So every deploy must move the minimum bytes over the NAS's link.
T-073 already split node_modules into its own image layer so a code-only deploy pulls just the small
.next/source delta and reuses the cached deps layer. BUT that only holds while GitHub's BuildKit cache stays
warm and reproduces the exact same layer digest; on a cache miss npm ci rebuilds node_modules into a
byte-different layer (identical contents, new digest), and the NAS re-pulls the full 2 GB. Confirmed
2026-06-24: no package.json/lockfile change, yet the deps layer digest had changed → full re-pull. So
T-073's win is real but fragile — it leans on a cache that evicts.
Plan¶
- Deps as a separate, lockfile-pinned base image (the big lever). A workflow builds
ghcr.io/<owner>/eop-deps:<package-lock hash>containing justnode_modules, rebuilt ONLY whenpackage-lock.jsonchanges. The appDockerfiledoesFROM eop-deps:<hash>and copies build/source on top. The deps layer digest is then guaranteed identical across every code deploy (a fixed tag, not a cache that can miss) → the NAS pulls the ~2 GB deps once per real dependency change (rare), and never on a code-only deploy. - Split the app layer. Today everything-but-deps ships as one layer. Separate the rarely-changing
disk-read assets (fonts,
public/, the IR56M template, classic invoice scheme,prompts/) from the.nextbuild output, so a normal deploy moves only the.next+ changed-source layer (~a few hundred MB vs 4.8 GB). - (Stretch, phase 2) Next.js
output: 'standalone'. Shrinks the WHOLE runtime image to a few hundred MB (only traced-needed deps +.next/standalone), so even a deps change is small. Deliberately AVOIDED today after theoutputFileTracingExcludesoutage (standalone tracing dropped runtime-needed files). Highest payoff, real risk — needs explicitoutputFileTracingIncludesfor the disk-read assets + thorough testing; only after #1+#2 are proven.
Direction-agnostic: whether the NAS pulls from GHCR or an image is pushed over Tailscale, the cost is the bytes moved over the NAS link, and #1+#2 minimize them.
Phased build — Phase 1 SHIPPED 2026-06-24 (registry-backed cache)¶
Owner go-ahead (verbatim, 2026-06-24): "do that, while make sure that agents only need to commit and this deploy building pipeline works automatically"
I led with the lowest-risk fix that addresses the exact root cause rather than the heavier base-image
restructure: switched nas-image.yml's build cache from the evictable type=gha to a persistent
type=registry cache (ghcr.io/<owner>/eop-app:buildcache-<branch>, mode=max). The deps-layer instability
was a cache miss (gha evicts on 10GB/7-day) silently rebuilding node_modules into a new-digest layer; a
registry cache lives in GHCR and doesn't evict, so the layer's digest stays stable build-to-build and the NAS
reuses its cached ~2GB. Effect: after one establishing pull, code-only deploys pull just the small app
layer (COPY --from=builder /app = .next + source, no node_modules). Validated: YAML parses, cache-from/to
are type=registry, the deploy step is intact. Commit recorded below.
Re-scoped plan:
- Phase 1 (done): registry cache → stable deps layer (the node_modules re-pull this task was opened for).
- Phase 2 (if Phase 1 proves insufficient): the lockfile-pinned deps base image (Plan #1 above) — the
guaranteed version (a fixed tag can't miss), heavier to build. Only if a registry-cache miss is ever observed.
- Phase 3: app-layer split (Plan #2), then standalone (Plan #3).
The pipeline is otherwise already automatic — push to main → nas-image.yml builds + pushes + the deploy
step (with T-105's smart retry) recreates on the NAS, with the nas-deploy.yml ensurer as the /15
backstop. "Agents only commit" already holds; Phase 1 makes the per-deploy pull tiny so it's also reliable*
over the cellular link.
Decision log¶
- ✅ Attestation: read AGENTS.md + checked the board by scope (extends T-073; distinct from T-105's deploy orchestration — not a dup). Source: EOP Local Assistance (fork) · session edb0abc7-6373-449b-8e31-379fd38d391b (local Claude Code).
- Requirement + go-ahead from the owner (quoted above), 2026-06-24. Proposed by EOP Local Assistance (fork); approved by the owner. Phase 1 (registry cache) shipped this session; Phases 2–3 pending need/approval.
2026-07-01 — dropped (superseded by the Vercel move)¶
- ✅ Read AGENTS.md. Verdict: DROPPED (superseded). The app moved back to Vercel, retiring the NAS app-deploy pipeline this task optimized. The NAS
eop-appcontainer was stopped 2026-07-01 (verifiedeop.theestablishers.com→ Vercel). the deps base-image + app-layer split + persistent registry build cache (93b1365d/4d6f40ca) already landed and is harmless, but it's no longer exercised. Reopen if NAS app-hosting is ever revisited. (The NAS still runs the billing scraper + home automation — unaffected; those don't use this pipeline.) - Owner context (2026-07-01, verbatim): "We can hold on NAS web app hosting related tasks as I have moved the web app hosting back to ... Vercel."
- Source (edit): EOP Local Assistance (fork) · session edb0abc7-6373-449b-8e31-379fd38d391b