Skip to content

chore(docker): shrink runtime image and stop building arm64 under QEMU - #3

Merged
lorenzocorallo merged 1 commit into
mainfrom
chore/docker-build-optimization
Sep 14, 2026
Merged

chore(docker): shrink runtime image and stop building arm64 under QEMU#3
lorenzocorallo merged 1 commit into
mainfrom
chore/docker-build-optimization

Conversation

@lorenzocorallo

Copy link
Copy Markdown
Member

Summary

Follow-up to a review of the Dockerfile and the container publish workflow. Structure was already sound (multi-stage, non-root, frozen lockfile); this fixes the three things that made builds slow and images large.

  • Build once, natively. The deps and build stages now use a toolchain pinned to --platform=$BUILDPLATFORM. The Nitro output is pure JavaScript, so in the linux/amd64,linux/arm64 CI build the pnpm install and Vite build run a single time on the host instead of once natively and once under QEMU. Only the small runtime node_modules install is still done per target platform.
  • Minimal runtime node_modules. Nitro inlines the whole server dependency tree into .output/server. The only code that runs outside the bundle is instrument.server.mjs and scripts/*.mjs, which need just @sentry/tanstackstart-react, drizzle-orm and pg. The prod-deps stage now installs only those. Their versions are derived at build time from the installed (lockfile-resolved) tree by docker/write-runtime-package.mjs, so the Sentry SDK loaded via --import cannot drift from the one bundled into .output.
  • Workflow. Added a pull_request trigger (build only, no push) so Dockerfile breakage is caught before merge, and a concurrency group so overlapping pushes to main cancel superseded builds. The workflow's existing pull_request guards were already in place; only the trigger was missing.
Before After
Runtime node_modules ~200 packages 3 direct packages (39 total)
Final image (arm64) 549 MB 108 MB
Vite build executions in multi-arch CI 2 (one emulated) 1 (native)

Verification

  • docker build of the final Dockerfile succeeds; the container starts, loads Sentry instrumentation, and reaches the database migration step (fails only because no database is provided).
  • docker buildx build --no-cache --platform linux/amd64,linux/arm64 runs pnpm run build in exactly one vertex on the native architecture; the prod-deps stage runs once per platform as intended.
  • Confirmed with a throwaway Dockerfile that --platform=$BUILDPLATFORM is a no-op when applied to a stage reference and only takes effect on an image reference, which is why the flag is on the build-base image line.
  • No .node native binaries in the runtime tree.

Not addressed (from the same review)

  • The RUN --mount=type=cache pnpm store is not persisted by the type=gha cache backend, so CI still relies on layer caching alone. Left as is; buildkit-cache-dance would be the fix if it becomes a problem.
  • corepack is deprecated upstream and works on node:22 but is gone from node:25 images.
  • Build-time tooling such as tailwindcss, @faker-js/faker, dotenv-cli and nitro is listed under dependencies in package.json. No longer affects the image, but could be moved to devDependencies for hygiene.

🤖 Generated with Claude Code

- Pin the toolchain used by the deps and build stages to
  --platform=$BUILDPLATFORM. The Nitro bundle is pure JavaScript and
  identical across target platforms, so those stages now run once,
  natively, instead of once per target (arm64 was running the full
  pnpm install and Vite build under QEMU). The flag is applied to the
  image reference because BuildKit ignores it on a stage reference.
- Replace the full `pnpm install --prod` in the prod-deps stage with a
  minimal manifest listing only the packages imported outside Nitro's
  bundle (instrument.server.mjs and scripts/*.mjs):
  @sentry/tanstackstart-react, drizzle-orm, and pg. Nitro traces and
  inlines everything else, so the previous production install
  (~200 packages, ~210MB) was mostly dead weight. Final image goes
  from ~549MB to ~108MB. The manifest is generated in the build stage
  from the installed versions (docker/write-runtime-package.mjs) so the
  Sentry SDK loaded via --import can never drift from the one bundled
  into .output.
- Add a pull_request trigger (build-only, no push) to docker-publish.yml
  so a broken Dockerfile is caught before merging to main. The existing
  `if: github.event_name != 'pull_request'` guards were already written
  for this but the trigger was missing.
- Add a concurrency group so overlapping pushes to main don't run
  redundant builds.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 14, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 43 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 549a8e22-9b64-41ce-9579-249e6091b184

📥 Commits

Reviewing files that changed from the base of the PR and between d1fd603 and 36ece4a.

📒 Files selected for processing (3)
  • .github/workflows/docker-publish.yml
  • Dockerfile
  • docker/write-runtime-package.mjs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@lorenzocorallo
lorenzocorallo merged commit ddb3731 into main Sep 14, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant