A Dagger module for Bun — the all-in-one JavaScript & TypeScript runtime, package manager, bundler, and test runner.
It models a Bun project as a typed object graph and maps Bun's toolchain onto
Dagger's first-class verbs, rather than wrapping the bun CLI. Bun ships no
linter, formatter, or typechecker (it transpiles TypeScript by stripping
types), so this module implements the verbs Bun actually owns — test, audit,
and frozen-lockfile install — plus build and container primitives, and leaves
lint/format/typecheck to specialized modules (see below).
Design rationale: design/bun-module.md.
- Pinned toolchain — one
versiondrives the base image and theinstallprimitive, so local and CI run the exact samebun. - Checks, surfaced individually —
bun test,bun audit, and frozen-lockfile install become named checks thatdagger checkruns and reports separately. - Warm cache —
~/.bun/install/cacheis acacheVolume; installs stay fast across runs without leaking into the source tree. - Hermetic binaries —
compilecross-compiles standalone executables with no local Bun install. - Composable images —
install(ctr)layers the pinnedbuninto any base;installed(ws)hands back a container withnode_modulesmaterialized.
| Object | What it is |
|---|---|
Bun |
The pinned toolchain: version, base, install, and project discovery. |
BunProject |
A single project (a directory with a package.json), plus its install root. |
Bun reaches projects two ways: projects (cwd-aware discovery — the nearest
enclosing project plus every project at or below the current directory, per
dagger/dagger#13688) and
project --path <p> (resolve the project containing a path, snapping up to its
root by default). Running the workspace-wide verbs from a subdirectory acts on the
project you're in, not the whole workspace.
Discovery keys on package.json and its workspaces field, so a monorepo's
members are enumerated individually (a Bun workspace has one root bun.lock, so
keying on the lockfile would only find the root). Each member runs at its
workspace root, so hoisted deps and workspace:* cross-deps resolve; standalone
and independent projects work unchanged.
Per project (Bun.project → BunProject):
test—bun testaudit—bun audit(--levelsets the minimum severity)install-check—bun install --frozen-lockfile(a stale lockfile fails CI)
Workspace-wide, fanning out over every discovered project:
test-all,audit-all,install-check-all
Build artifacts are outputs, not checks — they return files/dirs you export or ship:
bundle→Directory!—bun buildinto an output dircompile→File!—bun build --compile;--targetcross-compiles (e.g.bun-linux-x64,bun-linux-arm64-musl,bun-windows-x64)
bundle/compile default their entrypoint to package.json's main.
base→Container!—oven/bun:<version>-alpinewith the cache wired up.install(ctr, musl)→Container!— copy the pinnedbunbinary into any container. Copies the glibc build by default; pass--muslfor an alpine/musl target. The target must carry a C++ runtime (libstdc++/libgcc_s).BunProject.container/installed→Container!— the source mounted, before and afterbun install, for composing custom pipelines.
Constructor arguments on the root object:
--version(default1.3.14) — pinned Bun version; matches anoven/bunimage tag.--base— override the base container (must havebunonPATH, or run it throughinstallfirst).
Operation flags live on the function that uses them (audit --level,
compile --target/--entrypoint/--output-name, bundle --entrypoint/--outdir).
This module targets Dagger engine
v1.0.0-beta.7. Until that release is your default, prefix commands withdagger --x-release=v1.0.0-beta.7.
Run all checks:
dagger checkCall individual functions:
# Toolchain version
dagger call version
# Test / audit / lockfile-check a project (ws is the current workspace)
dagger call project --path ./app test
dagger call project --path ./app audit --level high
dagger call project --path ./app install-check
# Build artifacts
dagger call project --path ./app bundle export --path ./dist
dagger call project --path ./app compile --target bun-linux-x64 export --path ./app-bin
# Layer bun into your own image
dagger call install --ctr debian:stable-slimAdd it to your module's dagger-module.toml:
[[dependencies]]
name = "bun"
source = "github.com/dagger/bun"Then compose it — Bun owns test/audit/build; you add the checks Bun lacks and build your image:
type MyApp {
"Reuse Bun's native test check."
pub test(ws: Workspace!): Void @check {
bun().project(ws, ".").test(ws)
}
"Lint via a dedicated Biome module (Bun has no linter)."
pub lint(ws: Workspace!): Void @check {
biome().project(ws, ".").lint(ws)
}
"Ship a distroless image built from Bun's primitives."
pub image(ws: Workspace!): Container! {
bun().install(container.from("gcr.io/distroless/cc"))
.withDirectory("/app", bun().project(ws, ".").installed(ws).directory("/src"))
}
}
Bun is a runtime, package manager, bundler, and test runner — not a linter, formatter, or typechecker. Rather than fake those, compose specialized modules:
| Need | Use |
|---|---|
| Lint | Biome, oxlint, or ESLint |
| Format | Biome, Prettier, or dprint |
| Typecheck | tsc --noEmit (a TypeScript module) — Bun only strips types |
Dependency mutations (bun add/remove/update) and dev servers are also not
modeled: they are parameterized/non-idempotent or un-guessable, so they don't map
to dagger generate/dagger up. See the design doc for
the reasoning.
End-to-end checks live in .dagger/modules/e2e: they install this module as a
dependency and drive it against the fixture under
.dagger/modules/e2e/modules/hello (plus in-memory workspaces for failure
paths). Run them with dagger check.