Skip to content

Docker

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 builder
WORKDIR /app
COPY package.json bun.lock* ./
RUN bun install --no-save
COPY . .
RUN bun run build
FROM gcr.io/distroless/base-debian12:nonroot AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV HOSTNAME="0.0.0.0"
COPY --from=builder --chown=nonroot:nonroot /app/server ./
EXPOSE 3000
CMD ["./server"]

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 runner

For the full recipe (including fontconfig for libvips text rendering with sharp), see Distroless + sharp.

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

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 /tmp
volumes:
- name: runtime
emptyDir: { medium: Memory }

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!)