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. Surrounding modules
(buffer.ts, drain.ts, dashboard.ts, status.ts,
sources/*) are ESM imports. There is no bundler step before
shipping. 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.