Skip to content

Make the webapp Docker installs fast and cacheable #559

Open

Description

@HMarzban

Part of #554. Change set B. Work on main. Rulings: #554 rulings 1, 4, 5, 6, 7, 15 and 16.

Summary

  • Today. The two webapp bun install --frozen-lockfile steps run lifecycle scripts. They took 144–182 s per deploy, median 158 s, and never printed CACHED on the 6 deploys below. Admin and backend install the same 1751 packages with --ignore-scripts in 6–18 s, in the same jobs.
  • Part 1. Add --ignore-scripts to both webapp installs (Dockerfile.bun:107 and :191). Expected saving: about 120 s of 🏗️ Build Docker Images per deploy.
  • Part 2. In build-extensions, copy the six extension manifests before the install and the sources after it. The saving is small.
  • bun.lock and the root package.json stay byte-identical.

Evidence: DONE seconds of the two webapp installs, from each deploy job's BuildKit log.

Run Job build-extensions install builder install Sum
39398067267 118549257854 96.9 61.1 158.0
39297468969 118217497479 102.5 75.4 177.9
39275929831 118142985559 86.1 58.2 144.3
38392614215 115281929767 91.3 56.4 147.7
38239392350 114780192678 94.4 63.5 157.9
38068638114 114262648443 93.4 88.8 182.2

In job 118549257854, Bun reports 6.45 s (backend base), 9.59 s (admin deps) and 11.05 s (admin builder) with --ignore-scripts. The two webapp installs report 96.32 s and 60.46 s.

Depends on / Blocks

Current behaviour

Links are pinned to d0a53756f8676556358b6adc6c2397d2ed7c64c8.

  1. The build-extensions install copies extension sources first (Dockerfile.bun:91-107):

    # Overwrite extension source with context (in case deps had stale copy)
    COPY extensions/extension-hyperlink ./extensions/extension-hyperlink
    ...
    COPY extensions/extension-mermaid ./extensions/extension-mermaid
    
    ENV CYPRESS_INSTALL_BINARY=0
    RUN bun install --frozen-lockfile

    So any extension source edit re-runs the install. The extension builds follow at lines 109-118.

  2. The builder install has scripts on too (Dockerfile.bun:190-191). Its cache key includes the built extensions from line 182, so it re-runs on every extension edit. This issue does not change that.

  3. The pattern already exists. Dockerfile.bun:193-198 copies apps/webapp below its install: "COPY merges into the existing directory, so apps/webapp/node_modules survives." It also says "Never add --link here".

  4. .dockerignore drops every node_modules and dist (.dockerignore:6-8, :62-66). So a source COPY after the install cannot overwrite the extensions/*/node_modules it wrote.

  5. Admin and backend already use the flag. Admin: Dockerfile.bun:41 and :77, then next build with @sentry/nextjs. Backend: Dockerfile.bun:49-56, then five tsup (esbuild) builds.

  6. The two root scripts already fail in the webapp image. Root package.json:58-59 has "prepare": "husky" and "postinstall": "bun --filter @docs.plus/hocuspocus prisma:generate || true". Job 118549257854 prints, in both webapp installs:

    • "Could not load --schema from provided path prisma/schema.prisma: file or directory not found";
    • ".git can't be found" (husky).

    They take about 2 s. The rest of each install is silent for 94.66 s (build-extensions) and 58.72 s (builder). So dependency scripts hold the time.

  7. What each script does. Every package in the tree with an install-time script. It does not claim Bun runs each one today. Versions are from bun.lock. "Log" means seen in job 118549257854; "code" means read from the package source.

    Package (bun.lock) Script What it does Webapp image needs it?
    root package.json postinstall, prepare prisma:generate and husky No. Both fail today (log).
    sharp@0.35.5 none Binary from optional @img/sharp-linux-x64@0.35.5 (bun.lock:3613, :1151) (code). Unaffected.
    @sentry/cli@2.58.6 postinstall scripts/install.js resolves @sentry/cli-linux-x64 and downloads only when that fails (code). bun.lock pins @sentry/cli-linux-x64@2.58.6 (bun.lock:1451). No. Step 4 proves it.
    esbuild@0.27.7, esbuild@0.28.2 postinstall install.js checks the binary and may swap the JS shim for it (code). No. @esbuild/linux-* serves the JS API. Lines 110-118 fail loudly if it breaks.
    re2@1.24.1 install Downloads or compiles the addon with node-gyp (code). No. Nothing in apps/webapp, extensions or packages/floating-* imports it (Step 0.4). The backend image already ships without it.
    @prisma/engines, @prisma/client, prisma (6.19.3) postinstall, preinstall Download engines, generate a client (code). No. The webapp does not use Prisma.
    cypress@15.21.1 postinstall Downloads the binary. CYPRESS_INSTALL_BINARY=0 skips it. No.
    @parcel/watcher@2.6.0 install Builds only when npm_config_build_from_source=true (code). No.
    msgpackr-extract@3.0.4 install Loads or compiles a prebuilt addon (code). No. The webapp does not import it.
    unrs-resolver@1.12.2 postinstall napi-postinstall checks the binding (code). No. Jest and ESLint only.
    @turbodocx/html-to-docx@1.23.1 postinstall Prints a message (code). Backend dependency. No.

    The log has no per-script output, so this issue does not say which script costs most.

  8. Two failures would escape every deploy gate. Only /_next/image uses sharp at runtime, and the health check calls /api/health only. The GlitchTip sourcemap upload runs with silent: true (next.config.js:435-449). A broken esbuild, next build or next-pwa sw.js step fails 🏗️ Build Docker Images.

  9. A failed image build is not harmless at the pinned commit. 🚨 Rollback on Failure is a bare if: failure() (prod.docs.plus.yml:855-857). It runs deploy_service on all five services, and Compose uses the failed SHA, not PREVIOUS_TAG (Roll back only after the deploy has started rolling services #555, Current behaviour item 8). Roll back only after the deploy has started rolling services #555 lands first and fixes both. Step 5 still builds the full image locally.

  10. Every (build): deploy builds the webapp image (prod.docs.plus.yml:612-640).

Target behaviour

  • Both webapp installs run bun install --frozen-lockfile --ignore-scripts.
  • On the prod host, each webapp install ends in 20 s or less, or prints CACHED.
  • The build-extensions install's inputs are: root package.json and bun.lock, tsconfig.base.json and tsup.base.ts, packages/ and apps/ from deps, and the six extension manifests. An extension-source-only edit keeps it CACHED.
  • By design, a bun.lock change, any manifest change, or a change under packages/floating-*, packages/email-templates or packages/eslint-config still re-runs it (line 94).
  • The line 191 install still re-runs on every extension edit (line 182).
  • The shipped image works as today: /_next/image returns 200 image/webp, /sw.js returns 200, sourcemaps reach GlitchTip, and a pad loads.

Implementation steps

"Comment here" means a comment on this issue. The local Docker steps run on any machine. Judge no timing on a laptop (AGENTS.md §Docker).

Step 0. Read and check the base

  1. Run git diff --stat d0a53756f8676556358b6adc6c2397d2ed7c64c8 HEAD -- apps/webapp/docker/Dockerfile.bun package.json bun.lock .dockerignore AGENTS.md. To name the commits, run git log --oneline d0a53756f8676556358b6adc6c2397d2ed7c64c8..HEAD -- apps/webapp/docker/Dockerfile.bun AGENTS.md.

  2. Run sed -n '87,118p;175,198p' apps/webapp/docker/Dockerfile.bun. Confirm it matches items 1–3 of Current behaviour. If not, stop and comment here.

  3. Run grep -oE '"(sharp|@sentry/cli|@sentry/cli-linux-x64|esbuild|re2)@[0-9][^"]*"' bun.lock | sort -u. Expect sharp@0.35.5, @sentry/cli@2.58.6, @sentry/cli-linux-x64@2.58.6, esbuild@0.27.7, esbuild@0.28.2 and re2@1.24.1. Commit 7c3d7d26c (Sentry 11.9.0) removes @sentry/cli and @sentry/cli-linux-x64 from bun.lock. Sentry 11 adds sentry@0.45.0 and oxc-parser@0.152.0, and neither has an install script. Without @sentry/cli, skip the three sentry-cli lines of the Step 4 probe and their pass rule. If any other version differs, re-check that row in item 7 and comment here.

  4. Run this. Expect no output. If it prints a match, stop and comment here: that import may need its install script.

    git grep -n -E "['\"](re2|url-regex-safe|metascraper[^'\"]*|@prisma/client|msgpackr[^'\"]*)['\"]" -- apps/webapp extensions packages/floating-popover packages/floating-tooltip
  5. Record the production baseline for the hand checks (read on 2026-10-20):

    curl -sS -o /dev/null -w '%{http_code} %{content_type}\n' -H 'Accept: image/webp,*/*' 'https://docs.plus/_next/image?url=%2Ficons%2Ficon-96x96.png&w=64&q=75'
    # 200 image/webp
    curl -sS -o /dev/null -w '%{http_code} %{content_type}\n' 'https://docs.plus/sw.js'
    # 200 application/javascript; charset=utf-8

Step 1. Parts 1 and 2 in build-extensions

In apps/webapp/docker/Dockerfile.bun, replace lines 98–107 (from # Overwrite extension source with context to RUN bun install --frozen-lockfile) with this block. Keep lines 91–96 and 109–118 unchanged.

# Manifests only, so an extension source edit leaves the install below CACHED.
COPY extensions/extension-hyperlink/package.json ./extensions/extension-hyperlink/
COPY extensions/extension-hypermultimedia/package.json ./extensions/extension-hypermultimedia/
COPY extensions/extension-indent/package.json ./extensions/extension-indent/
COPY extensions/extension-inline-code/package.json ./extensions/extension-inline-code/
COPY extensions/extension-placeholder/package.json ./extensions/extension-placeholder/
COPY extensions/extension-mermaid/package.json ./extensions/extension-mermaid/

ENV CYPRESS_INSTALL_BINARY=0
# No build step here needs a dependency install script, and the scripts are
# most of this install's time. Native binaries come from optional packages.
# AGENTS.md §Docker records the check.
RUN bun install --frozen-lockfile --ignore-scripts

# Sources after the install. COPY merges into each directory, so the
# extensions/*/node_modules the install wrote survive. Never add --link here.
COPY extensions/extension-hyperlink ./extensions/extension-hyperlink
COPY extensions/extension-hypermultimedia ./extensions/extension-hypermultimedia
COPY extensions/extension-indent ./extensions/extension-indent
COPY extensions/extension-inline-code ./extensions/extension-inline-code
COPY extensions/extension-placeholder ./extensions/extension-placeholder
COPY extensions/extension-mermaid ./extensions/extension-mermaid
  • Keep ENV CYPRESS_INSTALL_BINARY=0. It guards a later revert of the flag.

  • Do not touch the deps stage (lines 53–58 and 69–74).

  • Check:

    sed -n '87,130p' apps/webapp/docker/Dockerfile.bun
    grep -c 'COPY extensions/extension-[a-z-]*/package.json ./extensions/' apps/webapp/docker/Dockerfile.bun   # expect 12 (6 in deps, 6 here)

Step 2. Part 1 in builder

Replace ENV CYPRESS_INSTALL_BINARY=0 and RUN bun install --frozen-lockfile (lines 190–191) with:

ENV CYPRESS_INSTALL_BINARY=0
# Same flag and reason as the build-extensions install.
RUN bun install --frozen-lockfile --ignore-scripts

Check:

grep -n 'RUN bun install' apps/webapp/docker/Dockerfile.bun
# expect exactly two lines, both ending in: --frozen-lockfile --ignore-scripts
git diff --exit-code -- bun.lock package.json .dockerignore

Step 3. Prove Part 2 with the cache-hit check

Build the build-extensions target twice with the cache on, changing one extension input between builds. Do the same with the base Dockerfile as the control. Never use --no-cache here. Run from the repo root; the target needs no build args.

T="$(mktemp -d)"; echo "$T"
git show d0a53756f8676556358b6adc6c2397d2ed7c64c8:apps/webapp/docker/Dockerfile.bun > "$T/Dockerfile.before"
cat > "$T/installs.awk" <<'EOF'
# Prints each webapp install step, and the extension-indent source COPY, with its DONE or CACHED line.
match($0, /#[0-9]+ \[(webapp )?(build-extensions|builder) +[0-9]+\/[0-9]+\] (RUN bun install|COPY extensions\/extension-indent )[^\r]*/) {
  s = substr($0, RSTART, RLENGTH); split(s, a, " "); if (!(a[1] in id)) id[a[1]] = s; next
}
{
  for (k in id) if ($0 ~ ("(^|[ \t])" k " (DONE|CACHED)")) {
    r = $0; sub(/.*#[0-9]+ /, "", r); print id[k] "  =>  " r; delete id[k]
  }
}
EOF
ext() { docker build -f "$1" --target build-extensions --progress=plain -t "$2" . > "$3" 2>&1; echo "exit $?"; }

ext apps/webapp/docker/Dockerfile.bun webapp-ext:after "$T/after-1.log"
echo "probe $T $(date +%s)" > extensions/extension-indent/cache-probe.txt
ext apps/webapp/docker/Dockerfile.bun webapp-ext:after "$T/after-2.log"
rm extensions/extension-indent/cache-probe.txt

ext "$T/Dockerfile.before" webapp-ext:before "$T/before-1.log"
echo "probe $T $(date +%s)" > extensions/extension-indent/cache-probe.txt
ext "$T/Dockerfile.before" webapp-ext:before "$T/before-2.log"
rm extensions/extension-indent/cache-probe.txt

git status --porcelain extensions/   # expect no output
awk -f "$T/installs.awk" "$T/after-2.log"
awk -f "$T/installs.awk" "$T/before-2.log"

Pass rule:

  • Every ext call prints exit 0.
  • In after-2.log, RUN bun install --frozen-lockfile --ignore-scripts prints CACHED, and COPY extensions/extension-indent ./extensions/extension-indent prints DONE.
  • In before-2.log, RUN bun install --frozen-lockfile prints DONE (the control).
  • If the extension-indent source COPY prints CACHED in either log, the probe did not change. Run Step 3 again from T= and do not judge.
  • If after-2.log shows the install as DONE, stop and comment here with both awk outputs.

BuildKit pads a step number below 10 with a second space ([build-extensions 8/20]). The + in the awk pattern allows for it.

Step 4. Check the skipped scripts

probe() { docker run --rm --entrypoint sh "$1" -c '
  cd /app/apps/webapp && bun -e "require(\"sharp\"); console.log(\"sharp loads\")"
  c=$(dirname "$(find /app/node_modules -path "*/node_modules/@sentry/cli/package.json" | head -1)")
  f=$(cd "$c" && node -e "console.log(require(\"./js/helper\").getPath())")
  echo "sentry-cli: $f"; "$f" --version
  echo "re2.node files: $(find /app/node_modules -name re2.node | wc -l)"'; }
probe webapp-ext:before
probe webapp-ext:after
grep -c 'prisma:generate' "$T/after-1.log"    # expect 0
grep -c 'prisma:generate' "$T/before-2.log"   # expect 1 or more; the Step 3 control proves this install ran

Pass rule for webapp-ext:after:

  • It prints sharp loads.
  • sentry-cli --version prints sentry-cli 2.58.6. The sentry-cli: path points into @sentry/cli-linux-*, not to @sentry/cli/sentry-cli.
  • re2.node files is 0. Record the webapp-ext:before value next to it.
  • after-1.log has no prisma:generate line. This counts only when awk -f "$T/installs.awk" "$T/after-1.log" shows the install as DONE. If CACHED, record "not evidence".

The runtime starts with bun (Dockerfile.bun:286), so the sharp check uses bun. On arm64 the optional packages are linux-arm64; the mechanism is the same.

Step 5. Build the full image and compare it with the base

The build args are the Build Verification placeholders for build:ci (prod.docs.plus.yml:390-396). next.config.js fails only when NEXT_PUBLIC_RESTAPI_URL is empty (next.config.js:221-226).

full() { docker build -f "$1" --target runner --progress=plain \
  --build-arg NEXT_PUBLIC_RESTAPI_URL=http://localhost:4000 \
  --build-arg NEXT_PUBLIC_SUPABASE_URL=http://localhost:54321 \
  --build-arg NEXT_PUBLIC_SUPABASE_ANON_KEY=dummy-key \
  -t "$2" . > "$3" 2>&1; echo "exit $?"; }
full apps/webapp/docker/Dockerfile.bun webapp:after "$T/runner-after.log"
full "$T/Dockerfile.before" webapp:before "$T/runner-before.log"

smoke() { docker run -d -P --name "smoke-$1" "webapp:$1" > /dev/null
  H="http://$(docker port "smoke-$1" 3000 | head -1)"
  for i in $(seq 1 30); do curl -fsS "$H/api/health" > /dev/null 2>&1 && break; sleep 2; done
  echo "== $1"
  curl -sS -o /dev/null -w 'health %{http_code}\n' "$H/api/health"
  curl -sS -o /dev/null -w 'image %{http_code} %{content_type}\n' -H 'Accept: image/webp,*/*' "$H/_next/image?url=%2Ficons%2Ficon-96x96.png&w=64&q=75"
  curl -sS -o /dev/null -w 'sw %{http_code} %{content_type}\n' "$H/sw.js"
  docker rm -f "smoke-$1" > /dev/null; }
smoke before
smoke after

Pass rule:

  • Both full calls print exit 0.
  • If before fails, this machine cannot build the image. Stop and comment here with tail -40 "$T/runner-before.log".
  • If only after fails, this change broke the build. Stop and comment here with tail -40 "$T/runner-after.log".
  • The after lines equal the before lines. Expected: health 200, image 200 image/webp, sw 200 application/javascript; charset=utf-8.
  • If before fails a line, record it and rely on the production hand check for it.
  • If after differs from before, stop and comment here with both outputs.

This step cannot check the GlitchTip upload (no token locally). Hand check 3 covers it.

Step 6. Record the rule in AGENTS.md

In AGENTS.md §Docker, add these two bullets right after the bullet that starts "A cache-ordering fix cannot be measured with --no-cache." (AGENTS.md:301). Find it by text. If a sibling bullet already follows it, add yours below that one. Replace <date> with the day you run Step 4.

- **Every `bun install` in the webapp, admin and backend Dockerfiles uses `--ignore-scripts`.** No image build step needs a dependency install script. sharp 0.35 has none, and esbuild gets its binary from an optional platform package. Without the flag, the webapp installs took 144–182 s per deploy, against 6–18 s for the same 1751 packages in admin and backend. The root `postinstall` and `prepare` scripts failed in the webapp image anyway. Checked on <date>. After a major bump of sharp or esbuild, check that the binary still comes from an optional package.
- **The webapp `build-extensions` stage copies the extension `package.json` files before its install, and the sources after it.** An extension-only edit then keeps that install `CACHED`. The `builder` install still re-runs, because it copies the built extensions.

Do not edit the "both webapp installs should print CACHED" sentence at AGENTS.md:290. #557 owns it.

bun run check:agent-docs
bun run format

Step 7. Post the local proof

Comment here with the heading "Local proof, ". Include:

  • the two Step 3 awk outputs;
  • the two Step 4 probe outputs and grep -c counts;
  • the Step 5 before and after smoke outputs;
  • the machine and its CPU architecture. Give no timings.

Review checkpoint

Show git diff --stat and git diff -- apps/webapp/docker/Dockerfile.bun AGENTS.md, then stop. Expect exactly these two files. Do not run git add, git commit, git push or git stash. The maintainer commits and pushes per SP2 (#554), as its own commit in set B, after #555.

Acceptance criteria

  • grep -n 'RUN bun install' apps/webapp/docker/Dockerfile.bun prints two lines, both ending in --frozen-lockfile --ignore-scripts.
  • In build-extensions, only the six extension package.json files are copied before the install, and the six source COPY lines follow it. Lines 109–118 are unchanged and in the same order.
  • git diff --exit-code -- bun.lock package.json .dockerignore passes. The deps and runner stages are unchanged.
  • The Step 3, Step 4 and Step 5 pass rules hold.
  • The AGENTS.md §Docker bullets exist. bun run check:agent-docs and bun run format pass.
  • The "Local proof" comment exists.
  • On the first (build): deploy with this change, each webapp install prints DONE in 20 s or less, or CACHED.
  • The four hand checks in Accountability pass, and the SP3 comment records them.

Verification

Local: the Step 2 and Step 6 checks, plus the Docker commands of Steps 3–5. No workflow file or triage script changes, so actionlint, bash .github/scripts/parse-build-trigger.test.sh and a dry_run_subject dispatch do not apply.

Post-merge. Read the first successful attempt-1 (build): deploy that contains the commit. Push no extra deploy. Find the job that holds 🏗️ Build Docker Images (this works before and after #565). In a fresh shell, first run T="$(mktemp -d)" and the Step 3 cat > "$T/installs.awk" heredoc.

RUN=<run id>
JOB=$(gh run view "$RUN" --repo docs-plus/docs.plus --json jobs --jq '.jobs[] | select(any(.steps[]?; .name | test("Build Docker Images"))) | .databaseId')
gh run view --repo docs-plus/docs.plus --job "$JOB" --log > "$T/deploy.log"
awk -f "$T/installs.awk" "$T/deploy.log"

Record the cache fields of that deploy:

  • lock_changed: git diff --quiet <previous deployed SHA> <SHA> -- bun.lock;
  • ext_src_changed: the same check on extensions/;
  • gap_h: hours since the previous deploy run started.

Record per SP3 (#554). Metrics are defined in #569.

Metric Read from Baseline Expected
Webapp install DONE seconds (primary) awk output on the job log 144.3–182.2 s per deploy, median 158 s (6 jobs above) each 20 s or less, or CACHED; sum 40 s or less
M3 step summary (#556) median 546 s, n = 14 about 426 s. One deploy is a read, not a result.
M2i step summary (#556) equals the M3 end today lower by about the same seconds as M3
M1 step summary (#556) median 1090 s, p90 1386 s, n = 14 about 970 s for this change alone. One deploy has no pass or fail.
M2g, M4, M5, W, M6 not read — This issue does not change them.
Part 2 in production. On the first deploy with ext_src_changed yes, lock_changed no and gap_h 24 or less, expect the build-extensions install to print CACHED. Record it in a new SP3 comment then. It is not a gate: the 24 h build-cache prune can clear the cache (#564).

Accountability

  • The builder (the assignee) runs Steps 0–7, posts "Local proof, ", and posts the SP3 comment on the deploy day. It adds the job id, the two awk install lines, lock_changed, ext_src_changed and gap_h.
  • The maintainer runs four hand checks the same day and adds them to that comment:
    1. /_next/image (sharp): the Step 0.5 command prints 200 image/webp.
    2. /sw.js: the Step 0.5 command prints 200 application/javascript; charset=utf-8.
    3. Sourcemaps: in GlitchTip project prod_docsplus, find the source map upload made during this deploy's 🏗️ Build Docker Images step, by date, not by release. The image build has no .git and no GITHUB_SHA, so the plugin uploads by debug ID with the literal release undefined. Confirm the previous deploy also has an upload at its build time. If GlitchTip lists no uploads by date, open the newest client error after this deploy. Its stack frames must show original src/ file names, as an earlier error does. If the previous deploy has neither, record "not comparable".
    4. A pad loads: create a scratch pad you own, type one character, reload, and see the character.
  • Stop condition, revert: any hand check fails, or 🏗️ Build Docker Images fails because of this change. Follow Rollback the same day.
  • Stop condition, no revert: an install still prints DONE above 20 s. The image works, so do not revert. Comment here with the awk output, and read that install's log for the slow part.

Rollback

Revert per SP4 (#554), on the revert stop condition above. The maintainer runs it.

  • Before the push, run git diff --stat HEAD~1. Expect only apps/webapp/docker/Dockerfile.bun and AGENTS.md.
  • The revert affects the deploy, so run git commit --allow-empty -m "(build): front" and push it. Prod deploys read only the pushed tip commit subject. A tip subject that starts with "Revert" does not deploy.
  • On that deploy, run the Verification post-merge commands. Expect both install lines to end in RUN bun install --frozen-lockfile, with no --ignore-scripts. Run the four hand checks again.
  • In the Reverted comment, add the revert commit, the awk output and the hand check results.

No host step and no setting change is needed.

Docs to update

  • AGENTS.md §Docker: the two new bullets (Step 6).
  • apps/webapp/docker/Dockerfile.bun: the new comments (Steps 1 and 2) replace the line 98 comment.
  • No CHANGELOG entry. The change has no user-visible effect.

Out of scope

  • Moving lines 93–94 or 180–183 to manifests-only. No tracker child covers it. File it separately with measurements.
  • A permanent build step that fails when the sentry-cli binary is missing. Step 4 proves it once, and the AGENTS.md bullet asks for a re-check after a major bump.
  • A BuildKit cache mount on the Bun install cache. The tracker Not doing list refutes it on disk safety. The header at Dockerfile.bun:6 keeps mounts off dependency installs.
  • --filter on the webapp installs, or removing python3, make, g++ or nodejs from base (lines 19-27). Not covered, not measured.
  • Any dependency, bun.lock, trustedDependencies or root overrides change. The metascraper family stays at 5.50.6 (AGENTS.md:193). Do not add a direct require('re2') (AGENTS.md:200).
  • The Bun version. The base stays on oven/bun:1-slim, the floor on Bun 1.4.0 (AGENTS.md:66). The CI pin is Keep the Actions cache under its limit and pin Bun in CI #561.
  • The settled Docker rules and the Docker judging rule in [Tracking] Faster, change-aware CI/CD pipeline #554 §Shared rules.
  • Sibling items: the siblings under Depends on / Blocks, and Build the deploy images while the quality gates run #565 (build images early).
  • Tracker Not doing list: Turbopack, hosted runners with a GHCR push, and one RUN for the extension builds.
  • Tests. No unit or Cypress test. The cache-hit check and the smoke comparison are the proof.

Read first

  • AGENTS.md §Docker (lines 286-307), and lines 66, 193 and 200.
  • apps/webapp/docker/Dockerfile.bun, the whole file.
  • apps/admin-dashboard/docker/Dockerfile.bun lines 15–41 and 66–87, and apps/hocuspocus.server/docker/Dockerfile.bun lines 48–56 and 88–94.
  • .dockerignore.
  • apps/webapp/next.config.js lines 37–45 (next-pwa) and 435–449 (Sentry).
  • .github/workflows/prod.docs.plus.yml lines 612–640 (image build) and 855–857 (rollback).
  • apps/hocuspocus.server/CLAUDE.md §Production And Docker Compose.
  • .cursor/skills/tech-writer/SKILL.md §Simplified English, for the AGENTS.md bullets and the comments.

Activity

  1. added this to the v2.3.0 milestone on Oct 20, 2026
  2. HMarzban commented on Oct 25, 2026

    @HMarzban
    CollaboratorAuthor

    Built and committed on local main. Awaits the maintainer's final push.

    Remaining gate: the push. The next production webapp image build then checks the install cache.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions