@beamhop/beambox
v0.1.2
Published
Build OCI images for microsandbox without Docker. A TypeScript image builder with a Dockerfile front-end.
Maintainers
Readme
beambox
Build OCI images for microsandbox without Docker — a fluent
TypeScript API, a Dockerfile front-end, and the beambox CLI.
bun add @beamhop/beambox # or: npm i @beamhop/beamboxRuns on Node 20+ and on Bun. The beambox binary works under either.
The CLI
beambox build -t my-app:local . # build ./Dockerfile, load into microsandbox
msb run my-app:local
beambox build -t my-app:local -o app.tar . # write a docker-save archive
beambox build -t ghcr.io/me/app:v1 --push . # push to a registry
beambox build --target builder . # stop at a named stage
beambox build --build-arg VERSION=1.2.3 . # set a build argumentWith no output option, beambox build loads the image into the local microsandbox cache, so
it is immediately runnable with msb run. Run beambox help for the full list.
The TypeScript API
ImageSpec is immutable: every method returns a new spec, so specs can be shared and
branched without a later call reaching back and changing an earlier result.
import { image } from "@beamhop/beambox"
const base = image("node:22-slim").workdir("/app").env({ NODE_ENV: "production" })
// `base` is unchanged by either of these.
const api = base.copy("./api/dist", "/app").cmd(["node", "index.js"])
const worker = base.copy("./worker/dist", "/app").cmd(["node", "worker.js"])
const built = await api.build({ tags: ["api:local"] })
await built.load()Multi-stage builds
import { image } from "@beamhop/beambox"
const built = await image("node:22", { as: "builder" })
.workdir("/src")
.copy(["package.json", "package-lock.json"], "./")
.run("npm ci", { mounts: [{ type: "cache", target: "/root/.npm", id: "npm" }] })
.copy(".", ".")
.run("npm run build")
.stage("node:22-slim")
.copy("/src/dist", "/app", { from: "builder" })
.workdir("/app")
.expose(3000)
.cmd(["node", "index.js"])
.build({ tags: ["app:local"] })
await built.load()The cache mount becomes a microsandbox named volume, so the npm cache survives between
builds — and because it is its own filesystem, nothing in it ends up in the image.
From a Dockerfile
import { dockerfile } from "@beamhop/beambox"
const source = await dockerfile("./Dockerfile", { context: "." })
const built = await source.build({
tags: ["app:local"],
buildArgs: { VERSION: "1.2.3" },
onProgress: (event) => {
if (event.kind === "step") console.log(event.instruction)
},
})
await built.load()dockerfileText does the same with a string, which is handy in tests.
Outputs
const built = await image("alpine:3.20").cmd(["/bin/sh"]).build({ tags: ["demo:local"] })
await built.load() // microsandbox cache
await built.toArchive("demo.tar") // docker save format
await built.toArchive("demo.oci.tar", { format: "oci" }) // OCI Image Layout
await built.toLayoutDirectory("./out/oci") // unpacked, for skopeo/crane
await built.push("ghcr.io/me/demo:v1") // any OCI registryA BuiltImage is also a plain ImageArtifact, so built.config, built.manifest, and
built.layers are all available to inspect.
Private registries
const built = await image("registry.corp.io/team/base:v2")
.cmd(["/app/server"])
.build({
registry: {
auth: { kind: "basic", username: "deploy", password: process.env.REGISTRY_TOKEN ?? "" },
},
})Credentials already in ~/.docker/config.json are picked up automatically. Pass
{ insecure: true } for a plain-HTTP local registry.
Declarative builds need nothing installed
A spec with no .run() never boots a VM and never loads the microsandbox SDK:
// Works on any machine, with no container runtime present at all.
const built = await image("gcr.io/distroless/static")
.copy("./server", "/server")
.cmd(["/server"])
.build({ platform: { os: "linux", architecture: "amd64" } })
await built.toArchive("server.tar")Because nothing is executed, this can target any platform — unlike RUN, which is limited
to the host architecture.
Errors
Every failure is typed and explains itself: DockerfileParseError (with line and column),
UnsupportedInstructionError, RunFailedError (exit code plus output), NoExecutorError,
PlatformMismatchError, CopySourceError, UnknownStageError, RegistryAuthError.
import { RunFailedError } from "@beamhop/beambox"
try {
await image("alpine").run("exit 42").build()
} catch (error) {
if (error instanceof RunFailedError) console.error(error.exitCode) // 42
}See the root README for how RUN works and the known limits.
