Bun Cheatsheet
What it is
Bun is an all-in-one JavaScript runtime, package manager, bundler, and test runner — bun install replaces npm install, bun run server.ts replaces node + tsx, bun build replaces webpack/esbuild, and bun test replaces Jest. It's positioned as the fastest tool in each of those roles, and at this point it's mature enough to run production workloads.
This sheet is the command-recall companion to the deeper Bun runtime guide — the install one-liners you need without reading prose, the daily-use commands grouped by phase (project init → install → run → build → test → deploy), and the lockfile + Docker patterns that hold up under CI. For the why-and-how of Bun's architecture, Node.js compatibility, and runtime APIs, the guide goes deeper.
Quick reference
Install
# macOS, Linux, WSL
curl -fsSL https://bun.sh/install | bash
# macOS with Homebrew
brew install oven-sh/bun/bun
# npm (works anywhere npm does — useful in CI runners that already have Node)
npm install -g bun
# Docker / CI image
# use the official image: oven/bun:1 (or pin: oven/bun:1.1.34-alpine)
# Windows (PowerShell)
powershell -c "irm bun.sh/install.ps1 | iex"
# Windows with Scoop
scoop install bun
Verify the install and confirm you're on a recent version:
bun --version # e.g. 1.1.34
bun --revision # commit + build date
which bun # confirm PATH resolution
If bun: command not found after install, the installer added ~/.bun/bin to your shell rc but the current shell hasn't sourced it — open a new shell, or source ~/.bashrc (or ~/.zshrc, or whichever bun install wrote to).
Update and pin
bun upgrade # latest stable
bun upgrade --canary # latest canary build
bun upgrade --version 1.1.30 # pin to a specific version
# In CI / Dockerfile — never use `latest`; pin the image tag instead
# FROM oven/bun:1.1.34-alpine
The bun upgrade command self-updates the binary in ~/.bun/bin/bun. On macOS systems that installed via Homebrew, use brew upgrade bun instead — bun upgrade would diverge from Homebrew's bookkeeping.
Project init
bun init # interactive scaffold
bun init -y # accept all defaults (TS + minimal)
bun init --minimal # bare-bones, no example files
bun create react ./my-app # scaffold from a Bun template
bun create vite ./my-app # any `create-*` npm package works
bun create elysia ./my-api # Bun-native HTTP framework
bun init produces package.json, tsconfig.json, index.ts, .gitignore, and a bun.lock. No node_modules until you bun install.
Install packages
bun install # install everything in package.json
bun install --frozen-lockfile # fail if bun.lock doesn't match — use in CI
bun install --production # skip devDependencies — use in Docker prod stage
bun install --no-save # install without writing to package.json
bun add react react-dom # add dependencies
bun add -d typescript @types/node # add devDependencies (-D also works)
bun add -g typescript # global install
bun add github:facebook/react # install directly from a GitHub repo
bun add jsr:@std/path # JSR registry package
bun remove lodash # uninstall + remove from package.json
bun update # update all packages within semver
bun update --latest react # update one package across major versions
bun outdated # show outdated packages without installing
Bun installs are dramatically faster than npm because the binary cache lives in ~/.bun/install/cache/ and is content-addressable — every project shares the same downloaded tarballs.
Workspaces (monorepos)
// package.json (root)
{
"name": "monorepo",
"private": true,
"workspaces": ["packages/*", "apps/*"]
}
bun install # installs every workspace's deps
bun add lodash --filter @scope/some-package # add to one specific workspace
bun run --filter '*' build # run "build" in every workspace
bun run --filter './apps/*' test # filter by glob path
--filter accepts package names, workspace names, or path globs — the same syntax pnpm uses.
Run scripts and files
bun run dev # run a package.json script
bun dev # same — `run` is optional for scripts
bun run --filter '*' test # run "test" in every workspace
bun run server.ts # execute a TS/JS file directly (no tsx/ts-node)
bun server.ts # same — `run` is optional for files
bun --hot server.ts # hot-reload on file change (dev server pattern)
bun --watch server.ts # restart on change (more aggressive than --hot)
bun x cowsay "hello" # ad-hoc execute a package (like npx)
bunx cowsay "hello" # alias for `bun x`
bun --hot is the daily-use server-side flag — it preserves application state between reloads (database connections, in-memory caches), which --watch doesn't. Use --hot for dev servers; --watch for scripts that need a fresh process on every change.
Build (bundler)
bun build ./src/index.ts --outdir ./dist
bun build ./src/index.ts --outfile ./dist/bundle.js
bun build ./src/index.ts --outdir ./dist --target browser
bun build ./src/index.ts --outdir ./dist --target node # produce a Node-compat bundle
bun build ./src/index.ts --outdir ./dist --target bun # default — uses Bun APIs
bun build ./src/index.ts --outdir ./dist --minify
bun build ./src/index.ts --outdir ./dist --sourcemap
bun build ./src/index.ts --outdir ./dist --external react # exclude from bundle
# Single-file executable — ships the runtime + your code in one binary
bun build ./src/cli.ts --compile --outfile ./dist/mycli
bun build ./src/cli.ts --compile --target=bun-linux-x64 --outfile ./dist/mycli-linux
--compile is unique to Bun — it produces a self-contained native executable (no bun binary required on the target machine). Useful for distributing CLI tools and serverless cold-start optimisation.
Test runner
bun test # run all *.test.ts / *.spec.ts files
bun test ./path/to/file.test.ts # run a single file
bun test --watch # re-run on file change
bun test --coverage # text coverage report
bun test --coverage --coverage-reporter=lcov # lcov for CI tools (Coveralls, Codecov)
bun test --bail # stop on first failure
bun test --timeout 10000 # per-test timeout in ms (default 5000)
bun test -t "login flow" # filter tests by name pattern
bun test --update-snapshots # rewrite snapshot expectations
The test API is Jest-compatible (describe, it, expect, beforeEach, etc.) so existing Jest test files often run unmodified. The big practical difference: bun test starts in ~20ms vs Jest's 1-3 seconds, which changes how you run tests during development (more often, smaller scopes).
Environment variables and .env files
# Bun auto-loads .env files — no dotenv package needed
bun server.ts # reads .env, .env.local, .env.${NODE_ENV}
# Override per-invocation
NODE_ENV=production bun server.ts
# Specify a different file
bun --env-file=.env.staging server.ts
// In code — typed access to env vars
const port = Bun.env.PORT || "3000";
const dbUrl = process.env.DATABASE_URL; // also works (Node compat)
Bun loads .env files in this order (later files override earlier ones): .env, .env.local, .env.${NODE_ENV}, .env.${NODE_ENV}.local. The .local variants should be in .gitignore.
Lockfile
bun install # generates/updates bun.lock
bun install --frozen-lockfile # CI mode — fails if lockfile is out of sync
bun install --lockfile-only # update lockfile, skip node_modules
# Convert from another package manager
bun install --yarn # generate yarn.lock alongside bun.lock
bun.lock is text-based (not binary) and intended to be reviewed in pull requests. Older Bun versions used bun.lockb (binary) — they're forward-compatible, but if you're starting a new project today, you'll see bun.lock.
Commit bun.lock to git unless you have a specific reason not to. CI builds should run bun install --frozen-lockfile so a stale lockfile breaks the build instead of silently picking a different package version.
Bun shell
// Cross-platform shell scripting from inside Bun
import { $ } from "bun";
const branch = (await $`git rev-parse --abbrev-ref HEAD`.text()).trim();
const files = await $`ls -la`.text();
// Pipes, env vars, all standard shell behaviour
await $`echo $HOME > /tmp/home.txt`;
await $`cat package.json | jq '.dependencies'`;
// Glob expansion happens in Bun's parser — portable across macOS/Linux/Windows
const ts_files = await $`ls src/*.ts`.text();
Bun.$ is portable shell scripting that works the same on macOS, Linux, and Windows — useful for build scripts that historically needed cross-env and rimraf to be cross-platform.
Deployment workflows (the moat)
1. Lock-file discipline in CI
The single most important Bun deploy pattern: every CI run must install with --frozen-lockfile so a stale lockfile fails the build instead of silently changing package versions.
# .github/workflows/ci.yml (or any equivalent CI config)
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.1.34
- run: bun install --frozen-lockfile
- run: bun test
- run: bun build ./src/index.ts --outdir ./dist
For DeployHQ build pipelines, the same flag goes in the build configuration — the build pipelines doc covers the shape; the Bun-specific commands are above.
Pin the Bun version (bun-version: 1.1.34) rather than latest. A Bun update can introduce a behaviour change between releases, and a deploy isn't where you want to discover it.
2. Multi-stage Docker build
The production-ready Bun Dockerfile pattern: build in one stage with full dev dependencies, copy the artifact into a slim runtime stage with production deps only.
# syntax=docker/dockerfile:1.7
# --- build stage ---
FROM oven/bun:1.1.34-alpine AS build
WORKDIR /app
# Cache the dep layer separately from source for fast rebuilds
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun run build # whatever your build script does — bun build, vite build, etc.
# --- production stage ---
FROM oven/bun:1.1.34-alpine
WORKDIR /app
ENV NODE_ENV=production
# Production deps only (skips ./node_modules from the build stage)
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile --production
# Copy only the build artifact + anything else the runtime needs
COPY --from=build /app/dist ./dist
USER bun # non-root user shipped with the image
EXPOSE 3000
CMD ["bun", "run", "dist/server.js"]
The --production flag in the runtime stage strips devDependencies, which typically halves image size. The USER bun line uses the non-root user the official image ships with — required for any image that'll be scanned by a security tool.
For a static-output build (Vite, Next.js export, Astro), the runtime stage can skip Bun entirely and just COPY --from=build /app/dist /usr/share/nginx/html into an nginx image.
3. Bun + DeployHQ build pipeline
DeployHQ's build pipeline runs your build commands inside a container before the deploy happens. The container picks up bun automatically if bun.lock is present in the repository, per the Using Bun with DeployHQ Build integration. The build commands look the same as the CI block above:
bun install --frozen-lockfile
bun run build
bun test
The artifact produced by bun run build is what DeployHQ uploads to the target servers — your runtime servers don't need Bun installed at all unless the application itself is running under Bun. See the Bun support article for the configuration steps.
For the wider question of which JavaScript runtime to standardise on, the npm vs yarn vs pnpm vs bun comparison walks through the tradeoffs.
4. Compile to a single-file binary
The --compile flag produces a self-contained native executable — useful for CLI tools, edge-deployed functions, and any case where installing Bun on the target machine is friction.
# Build for the current platform
bun build ./src/cli.ts --compile --outfile ./bin/mycli
# Cross-compile (no Docker required)
bun build ./src/cli.ts --compile --target=bun-linux-x64 --outfile ./bin/mycli-linux
bun build ./src/cli.ts --compile --target=bun-linux-arm64 --outfile ./bin/mycli-arm64
bun build ./src/cli.ts --compile --target=bun-darwin-arm64 --outfile ./bin/mycli-mac-arm
bun build ./src/cli.ts --compile --target=bun-windows-x64 --outfile ./bin/mycli.exe
The resulting binaries are ~50-100 MB (they include the Bun runtime), but they start in single-digit milliseconds and have no external dependencies. For a DeployHQ CLI-style distribution model, this is the cleanest way to ship a TypeScript tool to users who don't have Bun installed.
5. Hot reload for dev, restart for workers
Two reload modes serve different deploy-adjacent workflows:
bun --hot server.ts # dev server — preserves DB connections, in-memory state
bun --watch worker.ts # background worker — full restart on change, clean slate
--hot is the right default for HTTP servers because it keeps Bun.serve()'s socket bound across reloads — no port flapping, no losing in-flight requests. --watch is the right default for queue workers because state from a partial run shouldn't carry into the next run.
For local development with a package.json dev script:
{
"scripts": {
"dev": "bun --hot src/server.ts",
"worker": "bun --watch src/worker.ts"
}
}
Common errors and fixes
| Error / symptom | Cause | Fix |
|---|---|---|
bun: command not found after install |
Installer wrote to ~/.bashrc / ~/.zshrc but current shell hasn't sourced it |
Open a new shell or source ~/.bashrc (or whichever file the installer modified) |
error: failed to fetch package: bun.lock has been updated |
CI lockfile is out of sync with package.json |
Run bun install locally, commit the updated bun.lock, re-trigger CI |
Cannot find module 'X' after bun install |
Package has an optional native binary that didn't install (e.g. sharp, better-sqlite3) |
bun install --force to re-resolve, or add the --cpu/--os overrides to package.json's bun.install.overrides |
bun upgrade reports "already on the latest version" but --version shows old |
macOS Homebrew install — bun upgrade doesn't manage that binary |
brew upgrade bun instead |
EACCES: permission denied on global install |
~/.bun owned by root from a previous sudo install |
sudo chown -R $(whoami) ~/.bun |
Module not found: 'fs' (or path, crypto, etc.) |
Browser build target with Node imports in code | Set --target node or --target bun, or move the Node-specific code behind a runtime check |
bun test hangs without output |
A test imported a module that opens a server/timer and didn't close it | Add --bail to fail fast; ensure afterAll cleanup runs |
bun --hot not reloading on file change |
Watching a symlink or a node_modules-shadowed path |
Add the file path to bun --hot --watch=true or restart with --watch |
bun build --compile binary won't run on the deploy target |
Wrong cross-compile target | Match --target=bun-{platform}-{arch} to the deploy host's OS/arch |
error: Couldn't find or load Bun's lockfile |
bun install --frozen-lockfile in a fresh checkout where bun.lock isn't tracked |
Commit bun.lock to the repository |
bun.lockb vs bun.lock confusion |
Old binary lockfile from Bun <1.1 | Delete bun.lockb and run bun install — Bun writes the new text-based bun.lock |
Different bun.lock on different machines |
Different Bun versions producing slightly different resolutions | Pin Bun in CI (oven-sh/setup-bun@v2 bun-version: X.Y.Z) and document the version in package.json engines |
bun add rewriting package.json formatting |
Bun normalizes JSON — strips indentation/trailing-newline differences | Use --no-save for one-offs; otherwise accept the normalised format as the project standard |
Companion: full DeployHQ deploy workflow
The end-to-end Bun deploy pattern through DeployHQ: push to a Git branch → DeployHQ webhook fires → build pipeline runs bun install --frozen-lockfile && bun run build && bun test inside a container → the build artifact is uploaded to the target servers → optional post-deploy hooks (run migrations, flush caches, restart the process manager).
For applications that run under Bun in production (HTTP server, queue worker), the deploy target needs Bun installed — the Using Bun with DeployHQ Build integration handles the build-time side, and the Bun support article covers the runtime-side configuration. For static-output builds (Vite, Astro), the target just needs nginx or any static-file server — Bun is build-only.
If a release does ship a regression, one-click rollback restores the previous build directory and process artifacts without re-running the pipeline.
Start a free DeployHQ trial to wire Bun into a production deploy pipeline.
Related cheatsheets
- Composer cheatsheet — for projects mixing Bun (frontend) with a PHP backend.
- Docker cheatsheet — for the multi-stage Bun Dockerfile pattern in workflow #2.
- Bash cheatsheet — for the
set -euo pipefailpatterns around CI scripts. - SSH cheatsheet — for the deploy keys that authenticate CI to the runtime host.
- Cron and Crontab cheatsheet — for scheduling Bun-based jobs in production.
- Cheatsheets hub — every DeployHQ cheatsheet in one place.
Need help? Email support@deployhq.com or follow @deployhq on X.