← writing
2026-06-17Infra

Why we run Nitro on Bun in production

AiChat's server runs on Bun in production — nitro.preset: 'bun', so the process is Bun.serve. The interesting part wasn't the runtime. It was the build.

The build doesn't run on Bun — on purpose

The Nitro server bundle (Rollup) is a known Nuxt memory hog — it peaks around 7.5 GB regardless of runtime. DigitalOcean App Platform hard-caps every build at 8 GB, un-raisable. Node's --max-old-space-size throttles the build heap so it fits under that cap; Bun's JS engine has no equivalent throttle, peaks higher, and risks an exit-137 OOM on the builder.

So the Dockerfile builds on Node and runs on Bun. The build engine doesn't change the artifact — preset: 'bun' emits the same Bun.serve output either way — so the production server is still 100% Bun.

The lesson

"Full Bun" is a runtime decision, not a build one. Match the tool to the constraint: Bun where it wins (the server), Node where the platform forces your hand (a memory- capped builder). The user never sees the difference.

Note

If we ever outgrow the 8 GB build cap, the escape hatch is building the image in CI and deploying from a registry — which sidesteps the cap entirely.