From f99031d0276d261a14abc92c40996bafaf5a895a Mon Sep 17 00:00:00 2001 From: Neeraj Dalal Date: Wed, 30 Sep 2026 00:17:49 +0530 Subject: [PATCH 1/2] fix(api): run the image with Bun's auto-install off The api runner ships bundle/ and no node_modules, so a bare specifier the bundle still carries makes Bun fetch it from npm at runtime. @better-auth/core's guarded optional import of @opentelemetry/api is one: with a network, the first sign-in on a fresh container pulled 639 files; without one, that request stalled while Bun tried the registry. bun --no-install makes the import fail at once, and the guard falls back to the noop telemetry, which is what happens anyway once the fetch fails. The image's CMD and the api start script both carry the flag. The docker-test skill's offline check now signs in as well as hitting health, since the import fires on the first Better Auth call and never at boot, and asserts the install cache stays empty; its dummy env turns the agent route on so that probe can reach it. Claude-Session: https://claude.ai/code/session_01VBYxUtQGsiCZ57FgqffMu3 --- .agents/skills/docker-test/SKILL.md | 10 +++++++--- api/hono/Dockerfile | 3 ++- api/hono/package.json | 2 +- web/next/content/docs/deployment/docker.mdx | 4 ++-- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/.agents/skills/docker-test/SKILL.md b/.agents/skills/docker-test/SKILL.md index 226e21d5f..485d43ce0 100644 --- a/.agents/skills/docker-test/SKILL.md +++ b/.agents/skills/docker-test/SKILL.md @@ -33,6 +33,7 @@ docker compose down -e 's|^GOOGLE_CLIENT_ID=$|GOOGLE_CLIENT_ID=dummy|' \ -e 's|^GOOGLE_CLIENT_SECRET=$|GOOGLE_CLIENT_SECRET=dummy|' \ -e 's|^POSTGRES_URL=$|POSTGRES_URL=postgres://dummy:dummy@localhost:5432/dummy|' \ + -e 's|^AGENT_SIGNIN_ENABLED=$|AGENT_SIGNIN_ENABLED=true|' \ .env.example > .env ``` @@ -46,17 +47,20 @@ docker compose down ## Self-containment check (catches runtime auto-install) -The api runner ships only `bundle/`, so the bundle must resolve every import with no `node_modules`. When one is missing, Bun auto-installs it from npm at runtime, so a container that "works" online may be downloading packages on every cold start. Prove it offline: +The api runner ships only `bundle/`, so the bundle must resolve every import with no `node_modules`. When one is missing, Bun's runtime auto-install fetches it from npm, so a container that "works" online may be downloading packages at cold start or on a later request. The image runs `bun --no-install`, which turns that off: a missing import fails at once instead. Two probes prove both halves, offline: ```bash sed 's/ #.*//' .env > .env.docker docker run -d --name t-offline --network=none --env-file .env.docker docker exec t-offline sh -c 'for i in $(seq 1 30); do wget -qO- http://localhost:4000/api/health && exit 0; sleep 1; done; exit 1' -docker logs t-offline # on failure: "Cannot find package 'X'" = unresolved import +docker logs t-offline # on failure: "Cannot find package 'X'" = an import the bundle needs and does not carry +# the first sign-in is where Better Auth reaches for its optional telemetry import; it answers with the database error here (nothing is reachable), and it must answer at once +time docker exec t-offline sh -c 'wget -qO- --post-data=x --header="Origin: http://localhost:3000" http://localhost:4000/api/agents/sign-in-as; echo' +docker diff t-offline | grep .bun/install/cache # must print nothing docker rm -f t-offline ``` -Forensics on an online container: `docker diff | grep .bun/install/cache`; entries there mean auto-install fired (history: `--external hono` in the bundle build fetched hono from npm at cold start). +Health alone proves nothing about sign-in: the telemetry import fires on the first Better Auth call, not at boot. Before `--no-install`, this container fetched `@opentelemetry/api` (some 640 files) on that call whenever it had a network, and stalled on it when it did not. Run the same `docker diff` on the online container after the golden suite; entries under `.bun/install/cache` mean a runtime install got past the flag (history: `--external hono` in the bundle build fetched hono from npm at cold start). ## Single-libc check (web image, catches silent re-bloat) diff --git a/api/hono/Dockerfile b/api/hono/Dockerfile index 41fb9390c..9cc07ff22 100644 --- a/api/hono/Dockerfile +++ b/api/hono/Dockerfile @@ -18,5 +18,6 @@ WORKDIR /app/api/hono COPY --from=builder --chown=bun:bun /app/api/hono/bundle ./bundle USER bun -CMD ["bun", "bundle/index.mjs"] +# No node_modules here, so a bare specifier the bundle still carries (Better Auth's optional telemetry import) would make Bun fetch it from npm at runtime; with auto-install off it fails at once and the guarded import falls back +CMD ["bun", "--no-install", "bundle/index.mjs"] EXPOSE 4000 diff --git a/api/hono/package.json b/api/hono/package.json index 7fd1efba5..53abe3f03 100644 --- a/api/hono/package.json +++ b/api/hono/package.json @@ -14,7 +14,7 @@ "check-types": "tsc --noEmit", "dev": "portless", "dev:app": "concurrently \"tsdown --watch\" \"bun ../../.github/scripts/portless.ts bun --hot src/index.ts\"", - "start": "bun bundle/index.mjs" + "start": "bun --no-install bundle/index.mjs" }, "dependencies": { "@arcjet/ip": "catalog:", diff --git a/web/next/content/docs/deployment/docker.mdx b/web/next/content/docs/deployment/docker.mdx index 1ebc1cf15..371cd16f3 100644 --- a/web/next/content/docs/deployment/docker.mdx +++ b/web/next/content/docs/deployment/docker.mdx @@ -34,11 +34,11 @@ base → prepare → builder → runner `prepare` runs `turbo prune --docker` to carve a workspace-only build context, so each image installs and builds just its own dependencies. The final `runner` stage runs as the non-root `USER bun`. - The **web** image uses Next.js `output: "standalone"`: the builder copies `.next/static` and `public` into the standalone bundle, and the runner starts `bun server.js`. `next.config.ts` sets it for every build except Vercel's, which packages its own output through a Next adapter and writes no trace files for the standalone step to copy. -- The **api** image copies its `bundle/` output and runs `bun bundle/index.mjs`. +- The **api** image copies its `bundle/` output and runs `bun --no-install bundle/index.mjs`. -The api runner ships only its bundle: no `node_modules`, so it resolves every import from the bundle and starts with no network. The web build prunes the native binaries it will never run: `next.config.ts` detects the image's libc (alpine is musl) and drops the other-libc `sharp` and `@takumi-rs` binaries from output tracing, keeping only the takumi core `/og` needs. That keeps a single libc stack in the standalone output instead of both. +The api runner ships only its bundle: no `node_modules`, so it resolves every import from the bundle and starts with no network. Bun's runtime auto-install is off (`--no-install`), so a bare specifier the bundle does not carry, such as the optional telemetry import inside Better Auth, fails at once and the code that guards it falls back, instead of a running container reaching npm on its first sign-in. The web build prunes the native binaries it will never run: `next.config.ts` detects the image's libc (alpine is musl) and drops the other-libc `sharp` and `@takumi-rs` binaries from output tracing, keeping only the takumi core `/og` needs. That keeps a single libc stack in the standalone output instead of both. From 2c52e52e0863ee2c099ca1b2a7d6dff9f7fab107 Mon Sep 17 00:00:00 2001 From: Neeraj Dalal Date: Wed, 30 Sep 2026 00:42:16 +0530 Subject: [PATCH 2/2] docs(docker): narrow the api image's no-network claim to module resolution The callout said the runner resolves every import from the bundle and starts with no network, which the next sentence contradicted: the optional telemetry import is not in the bundle, it fails and falls back. Say what --no-install guarantees instead: resolution never reaches npm, and a specifier the bundle lacks fails rather than being fetched. Co-Authored-By: Claude Fable 5.1 --- web/next/content/docs/deployment/docker.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/next/content/docs/deployment/docker.mdx b/web/next/content/docs/deployment/docker.mdx index 371cd16f3..d51d1bff1 100644 --- a/web/next/content/docs/deployment/docker.mdx +++ b/web/next/content/docs/deployment/docker.mdx @@ -38,7 +38,7 @@ base → prepare → builder → runner -The api runner ships only its bundle: no `node_modules`, so it resolves every import from the bundle and starts with no network. Bun's runtime auto-install is off (`--no-install`), so a bare specifier the bundle does not carry, such as the optional telemetry import inside Better Auth, fails at once and the code that guards it falls back, instead of a running container reaching npm on its first sign-in. The web build prunes the native binaries it will never run: `next.config.ts` detects the image's libc (alpine is musl) and drops the other-libc `sharp` and `@takumi-rs` binaries from output tracing, keeping only the takumi core `/og` needs. That keeps a single libc stack in the standalone output instead of both. +The api runner ships only its bundle, no `node_modules`, and module resolution never reaches npm: everything the bundle needs is inside it, and with Bun's runtime auto-install off (`--no-install`) a bare specifier the bundle does not carry fails at once instead of being fetched. The one such specifier, the optional telemetry import inside Better Auth, is guarded and falls back, where a running container used to reach npm on its first sign-in. The web build prunes the native binaries it will never run: `next.config.ts` detects the image's libc (alpine is musl) and drops the other-libc `sharp` and `@takumi-rs` binaries from output tracing, keeping only the takumi core `/og` needs. That keeps a single libc stack in the standalone output instead of both.