diff --git a/.agents/skills/docker-test/SKILL.md b/.agents/skills/docker-test/SKILL.md index 226e21d5..485d43ce 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 41fb9390..9cc07ff2 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 7fd1efba..53abe3f0 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 1ebc1cf1..d51d1bff 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`, 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.