Docker
Minimal: distroless without native deps
Section titled “Minimal: distroless without native deps”If your app doesn’t use native modules, the smallest viable image is
gcr.io/distroless/base-debian12:nonroot — has glibc and not much
else, ~25MB.
FROM oven/bun:1.3.14 AS builderWORKDIR /appCOPY package.json bun.lock* ./RUN bun install --no-saveCOPY . .RUN bun run build
FROM gcr.io/distroless/base-debian12:nonroot AS runnerWORKDIR /appENV NODE_ENV=productionENV HOSTNAME="0.0.0.0"COPY --from=builder --chown=nonroot:nonroot /app/server ./EXPOSE 3000CMD ["./server"]With native deps (sharp, bcrypt, etc.)
Section titled “With native deps (sharp, bcrypt, etc.)”Native modules need a libstdc++ in the runner. Switch to
gcr.io/distroless/cc-debian12:nonroot — same minimal style, but
has libstdc++ and libgcc:
FROM gcr.io/distroless/cc-debian12:nonroot AS runnerFor the full recipe (including fontconfig for libvips text rendering with sharp), see Distroless + sharp.
Choosing a base image
Section titled “Choosing a base image”| Base image | Size | Has | Use when |
|---|---|---|---|
gcr.io/distroless/base-debian12:nonroot |
~25MB | glibc, openssl | Pure-JS app, no native modules |
gcr.io/distroless/cc-debian12:nonroot |
~30MB | cc deps + libstdc++, libgcc |
Any native dep (sharp, bcrypt, native query engines) |
debian:12-slim |
~75MB | full slim debian | Need apt-get for extra libs at runtime |
oven/bun:1.3.14-slim |
~150MB | bun runtime | Skip the binary entirely; run standalone output with bun directly |
Read-only root filesystems
Section titled “Read-only root filesystems”The binary extracts runtime files next to itself by default. For
readOnlyRootFilesystem: true (Kubernetes) or read-only containers,
point extraction at a writable tmpfs:
ENV NBC_RUNTIME_DIR=/tmp/app# k8s: mount tmpfs at /tmpvolumes: - name: runtime emptyDir: { medium: Memory }Multi-arch builds in CI
Section titled “Multi-arch builds in CI”GitHub Actions example for building both amd64 and arm64:
- run: NBC_TARGET=bun-linux-x64 bun run build- run: mv server server-amd64- run: NBC_TARGET=bun-linux-arm64 bun run build- run: mv server server-arm64- uses: docker/buildx-action@v3- run: | docker buildx build --platform linux/amd64,linux/arm64 \ --build-arg BINARY_AMD64=server-amd64 \ --build-arg BINARY_ARM64=server-arm64 \ -t myapp:latest --push .The cross-compilation flags are documented in Cross-compilation.
Why not just use bun --bun ./server.js instead?
Section titled “Why not just use bun --bun ./server.js instead?”The standalone-with-bun-runtime path is a perfectly valid alternative to compiling a binary. Bigger image, simpler debug story, no compiled-binary resolver quirks.
Use the binary when:
- Image size matters (~30MB vs ~150MB)
- Cold start matters (static tiers answer in ~60ms while Next boots lazily)
- You want a single self-contained artifact
Use bun + standalone when:
- You want zero next-bun-compile-specific behavior
- You’re hitting edge cases the resolver hook doesn’t handle (please file an issue!)