fix(docker): drop dev tooling, git and libcurl from the runtime image #250

Merged
sionzee merged 1 commit from claude/slim-api-image into main 2026-10-22 16:43:37 +02:00
Owner

Resolves the api row of the image vuln report (10 C / 74 H: OpenSSL, libcurl, Go 1.23 binary).

Where the findings came from

  • Go 1.23: esbuild 0.25.10 (built with go1.23.12), pulled in by drizzle-kit. tsgo and tsgolint are also Go binaries (go1.26). All three were in the image only because the Dockerfile installed devDependencies.
  • libcurl: brought in by apk add git. Nothing needs git (no submodules, no git deps).
  • OpenSSL: comes from the base image. Clean on today's Alpine 3.24.3, so the report's findings were from an older pushed image; rebuilding fixes it. apk upgrade added so future rebuilds pick up fixes between base-image releases.

Changes

  • Dockerfile: bun install --production, drop git, apk upgrade
  • .dockerignore (new): keep .git, .env*, node_modules, uploads, backups out of COPY . .
  • scripts/migrate.ts (new): drizzle-orm runtime migrator, same drizzle.__drizzle_migrations journal; migrate:production now runs it (previously pointed at a missing script)
  • Deploy runbook comment in .forgejo/workflows/deploy.yml + deploy/CHANGES.md entry

⚠️ Host action after deploy

In-container bun run db:migrate no longer works (no drizzle-kit in the image). Use:

docker compose run --rm --no-deps --entrypoint sh api -c 'bun run migrate:production'

Local dev keeps using bun run db:migrate.

Verification

Trivy C / H before after
esbuild (Go 1.23) 1 / 25 removed
tsgo 0 / 17 removed
tsgolint 0 / 13 removed
Alpine packages 0 / 0 0 / 0
Image size 552 MB 267 MB
  • Postgres 17: drizzle-kit migrate then migrate:production on the same DB is a no-op; fresh DB via migrate:production gives an identical pg_dump -s and the same 14 journal rows.
  • New image boots and serves HTTP against the migrated DB.
  • tsgo --noEmit and oxlint pass.

🤖 Generated with Claude Code

Resolves the `api` row of the image vuln report (10 C / 74 H: OpenSSL, libcurl, Go 1.23 binary). ## Where the findings came from - **Go 1.23**: `esbuild` 0.25.10 (built with go1.23.12), pulled in by `drizzle-kit`. `tsgo` and `tsgolint` are also Go binaries (go1.26). All three were in the image only because the Dockerfile installed devDependencies. - **libcurl**: brought in by `apk add git`. Nothing needs git (no submodules, no git deps). - **OpenSSL**: comes from the base image. Clean on today's Alpine 3.24.3, so the report's findings were from an older pushed image; rebuilding fixes it. `apk upgrade` added so future rebuilds pick up fixes between base-image releases. ## Changes - `Dockerfile`: `bun install --production`, drop git, `apk upgrade` - `.dockerignore` (new): keep `.git`, `.env*`, `node_modules`, uploads, backups out of `COPY . .` - `scripts/migrate.ts` (new): drizzle-orm runtime migrator, same `drizzle.__drizzle_migrations` journal; `migrate:production` now runs it (previously pointed at a missing script) - Deploy runbook comment in `.forgejo/workflows/deploy.yml` + `deploy/CHANGES.md` entry ## ⚠️ Host action after deploy In-container `bun run db:migrate` no longer works (no drizzle-kit in the image). Use: ``` docker compose run --rm --no-deps --entrypoint sh api -c 'bun run migrate:production' ``` Local dev keeps using `bun run db:migrate`. ## Verification | Trivy C / H | before | after | |---|---|---| | esbuild (Go 1.23) | 1 / 25 | removed | | tsgo | 0 / 17 | removed | | tsgolint | 0 / 13 | removed | | Alpine packages | 0 / 0 | 0 / 0 | | Image size | 552 MB | 267 MB | - Postgres 17: `drizzle-kit migrate` then `migrate:production` on the same DB is a no-op; fresh DB via `migrate:production` gives an identical `pg_dump -s` and the same 14 journal rows. - New image boots and serves HTTP against the migrated DB. - `tsgo --noEmit` and oxlint pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(docker): drop dev tooling, git and libcurl from the runtime image
Some checks failed
OpenCodeReview / review (deepseek) (pull_request) Failing after 5s
Tests / test (pull_request) Successful in 1m53s
OpenCodeReview / review (luna) (pull_request) Successful in 3m59s
96ee4b8421
The image installed devDependencies, which ship prebuilt Go binaries the
server never runs: esbuild 0.25.10 (go1.23.12, via drizzle-kit), tsgo and
tsgolint. Together with git (and its libcurl) they were the source of every
CRITICAL/HIGH finding on the api image.

- bun install --production; remove git (no submodules or git deps)
- apk upgrade so rebuilds pick up Alpine security fixes
- add .dockerignore so COPY . . no longer pulls in .git, .env, node_modules
- scripts/migrate.ts: drizzle-orm runtime migrator on the same journal,
  wired to migrate:production (previously pointed at a missing script)
- deploy runbook + CHANGES.md: in-container migrations now use
  `bun run migrate:production`

Trivy C/H: 1/55 -> 0/0. Image 552 MB -> 267 MB.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
sionzee deleted branch claude/slim-api-image 2026-10-22 16:43:37 +02:00
ocr-bot left a comment

OpenCodeReview (openai/gpt-5.6-luna) — 1 finding

1 posted inline.


model openai/gpt-5.6-luna · 5 file(s) reviewed · 417,770 tokens (392,214 in / 25,556 out / 339,975 cached — 87% of input) · 3m55s

<!-- open-code-review model="openai/gpt-5.6-luna" --> ### OpenCodeReview (`openai/gpt-5.6-luna`) — 1 finding 1 posted inline. --- <sub>model `openai/gpt-5.6-luna` · 5 file(s) reviewed · 417,770 tokens (392,214 in / 25,556 out / 339,975 cached — 87% of input) · 3m55s</sub>
Lines 29-35
@ -0,0 +26,10 @@
const db = drizzle({ client });
try {
const failure = await migrate(db, { migrationsFolder: new URL("../drizzle", import.meta.url).pathname });
if (failure) {
console.error("Migration failed:", failure);
process.exitCode = 1;
} else {
console.log("Migrations applied");
}
Member

[bug · medium] drizzle-orm's migrate() returns Promise<void> and signals failures by throwing; it does not return a failure value. Therefore this condition is unreachable (and produces a TypeScript error if this script is type-checked), while migration errors bypass the intended message and become an unhandled exception. Catch the rejection instead.

	try {
		await migrate(db, { migrationsFolder: new URL("../drizzle", import.meta.url).pathname });
		console.log("Migrations applied");
	} catch (error) {
		console.error("Migration failed:", error);
		process.exitCode = 1;
	}
**[bug · medium]** `drizzle-orm`'s `migrate()` returns `Promise<void>` and signals failures by throwing; it does not return a failure value. Therefore this condition is unreachable (and produces a TypeScript error if this script is type-checked), while migration errors bypass the intended message and become an unhandled exception. Catch the rejection instead. ```suggestion try { await migrate(db, { migrationsFolder: new URL("../drizzle", import.meta.url).pathname }); console.log("Migrations applied"); } catch (error) { console.error("Migration failed:", error); process.exitCode = 1; } ```
Sign in to join this conversation.
No description provided.