Bun and distroless: design choices
Why the agent runs on Bun and ships in a distroless image.
The agent's runtime choices are deliberate. They shape the image size, the operator surface, and the security posture in production.
Bun#
The agent runs on Bun rather than Node.js. The decisions Bun makes for us:
- Native TypeScript execution. Source files run directly, with no transpile step in the build pipeline.
- Embedded SQLite. The
bun:sqlitemodule replacesbetter-sqlite3. Loses the build dependency onpython,make, andg++. The container image shrinks accordingly. - Native fetch.
axiosis gone. One fewer dependency, one fewer attack surface. - Native scheduling.
setIntervalis enough;node-cronis not in the dependency graph. - Automatic
.envloading.dotenvis gone for the agent's needs.
The trade-off is that Bun is younger than Node and the ecosystem's edge cases sometimes show up. The agent's dependencies are deliberately narrow to limit exposure.
Distroless#
The runtime image is oven/bun:1-distroless. The trade-offs:
- No shell.
sh,bash,busybox,curl,wget, and every other utility are absent. An attacker who reaches the container has no shell to drop into. - No package manager. No
apk,apt, or anything that can install code at runtime. - Smaller surface. The image contains the Bun binary, libc, and the agent's source. Nothing else.
Operational consequence: do not kubectl exec -it into the
container expecting a shell. Diagnose through the dashboard,
through logs, and through restart-and-observe.
Single-file source#
The runtime entry is src/index.ts. The rest of the agent is a
set of ESM modules with no bundler step; Bun runs the source
directly. The image carries the source verbatim, runs it under
Bun, and that is the entire chain from git clone to running
process.
Image size#
The runtime image is roughly 40MB on linux/amd64. The Bun
distroless base is most of that; the agent's own code adds a few
hundred kilobytes. Pull time on a fresh node is dominated by the
base layer; tag-based caching makes subsequent pulls nearly
instant.
Standalone binary#
bun build --compile produces a single-file executable per
platform. The binary embeds the Bun runtime and every dependency,
so a release-binary install reduces to "download, chmod, run" with
no runtime install on the host.
Per-tag CI publishes five binaries to
github.com/useobserver/agent/releases:
linux-x64, linux-arm64, darwin-x64, darwin-arm64, and
windows-x64.exe, plus a SHA256SUMS file for verification.
The binary path is the lightest install but offers fewer guardrails than the container path. Use the container image when you want isolation (separate user namespace), a single upgrade mechanism shared with other services, or tag-based version pinning at the container-runtime layer. Use the binary on constrained or air-gapped hosts that should not run Docker at all.
Binaries are produced from the same source as the container. The
build entry is src/index.ts either way. Runtime flags can be
forwarded to the embedded Bun via BUN_OPTIONS; see
bun.com/docs/bundler/executables.