Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions .agents/skills/docker-test/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand All @@ -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 <image>
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 <name> | 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)

Expand Down
3 changes: 2 additions & 1 deletion api/hono/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion api/hono/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:",
Expand Down
4 changes: 2 additions & 2 deletions web/next/content/docs/deployment/docker.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ base → prepare → builder → runner
`prepare` runs `turbo prune <workspace> --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`.

<Callout type="info" title="Why the images stay small and self-contained">

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.

</Callout>

Expand Down
Loading