Bun Production Patterns
Production patterns for Bun: Docker deployments, TypeScript configuration, shell scripting with Bun.$, monorepo setup, database access patterns, and deployment to Fly.io and Railway.
You are an expert in production Bun deployments, covering Docker optimization, TypeScript best practices, shell scripting, monorepo architecture, database integration, and cloud deployment.
## Key Points
- **Not using `--frozen-lockfile` in Docker builds**: Without it, the lockfile may be updated during the build, leading to non-reproducible images.
- **Running Bun as root in containers**: Always use `USER bun` or `USER nonroot` in your Dockerfile. Running as root is a security risk.
- **Skipping health checks in production**: Always implement a `/health` endpoint and configure your orchestrator to check it. This enables zero-downtime deploys and automatic recovery.
- **Using `bun run` for compiled binaries**: If you compile with `bun build --compile`, the output is a standalone executable. Run it directly, not through `bun run`.
- **Hardcoding environment values instead of using Bun.env**: Always read configuration from environment variables for 12-factor app compliance.
- **Not setting PRAGMA WAL mode for SQLite**: Without WAL mode, SQLite blocks readers during writes. Always enable it for server workloads.
- **Deploying without a process manager**: In production, use Docker restart policies, systemd, or your platform's built-in process management. Do not run `bun run server.ts` in a bare terminal.
## Quick Example
```bash
# Type check without emitting (run in CI)
bun run tsc --noEmit
# Or use in package.json scripts
# "typecheck": "tsc --noEmit"
```
```typescript
// With paths configured above, this works directly:
import { db } from "@/database";
import { UserService } from "@/services/user";
```skilldb get bun-skills/Bun Production PatternsFull skill: 461 linesBun Production Patterns — Real-World Deployment and Architecture
You are an expert in production Bun deployments, covering Docker optimization, TypeScript best practices, shell scripting, monorepo architecture, database integration, and cloud deployment.
Bun + Docker
Optimized Production Dockerfile
# Multi-stage build for minimal image size
FROM oven/bun:1-alpine AS base
WORKDIR /app
# Stage 1: Install production dependencies only
FROM base AS production-deps
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile --production
# Stage 2: Install all deps and build
FROM base AS build
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun run build
# Stage 3: Production image
FROM base
COPY --from=production-deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY package.json ./
# Security: run as non-root
USER bun
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
CMD bun --eval "fetch('http://localhost:3000/health').then(r => process.exit(r.ok ? 0 : 1))"
EXPOSE 3000
CMD ["bun", "run", "dist/server.js"]
Compile to Single Binary
Bun can compile your app into a standalone executable that does not require Bun to be installed:
# Compile to a single binary
bun build --compile src/server.ts --outfile server
# Cross-compile for different platforms
bun build --compile --target=bun-linux-x64 src/server.ts --outfile server-linux
bun build --compile --target=bun-linux-arm64 src/server.ts --outfile server-arm
bun build --compile --target=bun-darwin-x64 src/server.ts --outfile server-macos
bun build --compile --target=bun-windows-x64 src/server.ts --outfile server.exe
# Minimal Docker image with compiled binary
FROM oven/bun:1-alpine AS build
WORKDIR /app
COPY . .
RUN bun install --frozen-lockfile
RUN bun build --compile src/server.ts --outfile server
FROM gcr.io/distroless/base-debian12
COPY --from=build /app/server /server
USER nonroot
EXPOSE 3000
CMD ["/server"]
Bun + TypeScript
Bun runs TypeScript natively -- no tsc compilation needed at runtime. However, configure TypeScript properly for editor support and type checking:
// tsconfig.json
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"types": ["bun-types"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInImports": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Type checking is still valuable even though Bun does not need it to run:
# Type check without emitting (run in CI)
bun run tsc --noEmit
# Or use in package.json scripts
# "typecheck": "tsc --noEmit"
Path Aliases
Bun respects tsconfig.json path aliases without additional configuration:
// With paths configured above, this works directly:
import { db } from "@/database";
import { UserService } from "@/services/user";
Shell Scripting with Bun
Bun is excellent for replacing bash scripts. Fast startup and the $ template literal make it practical:
#!/usr/bin/env bun
// deploy.ts -- run with: bun run deploy.ts
import { $ } from "bun";
// Configuration
const DEPLOY_ENV = Bun.env.DEPLOY_ENV ?? "staging";
const IMAGE = `myapp:${Date.now()}`;
console.log(`Deploying to ${DEPLOY_ENV}...`);
// Build and push Docker image
await $`docker build -t ${IMAGE} .`;
await $`docker push ${IMAGE}`;
// Run database migrations
await $`bun run migrate:${DEPLOY_ENV}`;
// Deploy
if (DEPLOY_ENV === "production") {
await $`kubectl set image deployment/myapp myapp=${IMAGE} --namespace=production`;
await $`kubectl rollout status deployment/myapp --namespace=production --timeout=300s`;
} else {
await $`fly deploy --image ${IMAGE} --app myapp-staging`;
}
console.log("Deployment complete!");
Script Utilities
#!/usr/bin/env bun
// setup.ts -- project setup script
import { $ } from "bun";
import { existsSync } from "node:fs";
// Check prerequisites
const { exitCode: dockerCheck } = await $`docker --version`.quiet().nothrow();
if (dockerCheck !== 0) {
console.error("Docker is required. Install it from https://docker.com");
process.exit(1);
}
// Create .env if it does not exist
if (!existsSync(".env")) {
await Bun.write(".env", [
"DATABASE_URL=postgres://localhost:5432/myapp",
"REDIS_URL=redis://localhost:6379",
`SECRET_KEY=${crypto.randomUUID()}`,
].join("\n"));
console.log("Created .env file");
}
// Start services
await $`docker compose up -d postgres redis`;
// Wait for database
for (let i = 0; i < 30; i++) {
const { exitCode } = await $`docker compose exec postgres pg_isready`.quiet().nothrow();
if (exitCode === 0) break;
await Bun.sleep(1000);
}
// Install dependencies and run migrations
await $`bun install`;
await $`bun run migrate`;
console.log("Setup complete! Run 'bun run dev' to start.");
Monorepo Setup
my-monorepo/
package.json # root with workspaces
bunfig.toml # shared Bun config
tsconfig.json # base TypeScript config
packages/
shared/ # shared types and utilities
package.json
src/
database/ # database layer
package.json
src/
apps/
api/ # HTTP API server
package.json
tsconfig.json # extends root
src/
web/ # frontend
package.json
src/
worker/ # background worker
package.json
src/
// Root package.json
{
"name": "my-monorepo",
"private": true,
"workspaces": ["packages/*", "apps/*"],
"scripts": {
"dev": "bun run --cwd apps/api dev",
"build": "bun run --filter '*' build",
"test": "bun test --recursive",
"typecheck": "tsc -b"
}
}
// apps/api/package.json
{
"name": "@myorg/api",
"dependencies": {
"@myorg/shared": "workspace:*",
"@myorg/database": "workspace:*",
"hono": "^4.0.0"
}
}
// apps/api/tsconfig.json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"paths": {
"@myorg/shared": ["../../packages/shared/src"],
"@myorg/database": ["../../packages/database/src"]
}
},
"references": [
{ "path": "../../packages/shared" },
{ "path": "../../packages/database" }
]
}
Database Access
SQLite (Built-in)
import { Database } from "bun:sqlite";
class AppDatabase {
private db: Database;
constructor(path: string) {
this.db = new Database(path);
this.db.exec("PRAGMA journal_mode = WAL");
this.db.exec("PRAGMA foreign_keys = ON");
this.db.exec("PRAGMA busy_timeout = 5000");
}
migrate() {
this.db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
`);
}
createUser(email: string, name: string) {
return this.db.prepare(
"INSERT INTO users (email, name) VALUES (?, ?) RETURNING *"
).get(email, name);
}
getUserByEmail(email: string) {
return this.db.prepare("SELECT * FROM users WHERE email = ?").get(email);
}
close() {
this.db.close();
}
}
PostgreSQL with Drizzle ORM
bun add drizzle-orm postgres
bun add -d drizzle-kit
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import { pgTable, serial, text, timestamp } from "drizzle-orm/pg-core";
import { eq } from "drizzle-orm";
// Schema
export const users = pgTable("users", {
id: serial("id").primaryKey(),
email: text("email").unique().notNull(),
name: text("name").notNull(),
createdAt: timestamp("created_at").defaultNow(),
});
// Connection
const client = postgres(Bun.env.DATABASE_URL!);
const db = drizzle(client);
// Queries
const allUsers = await db.select().from(users);
const user = await db.select().from(users).where(eq(users.email, "alice@example.com"));
await db.insert(users).values({ email: "bob@example.com", name: "Bob" });
Deployment to Fly.io
# fly.toml
app = "my-bun-app"
primary_region = "iad"
[build]
dockerfile = "Dockerfile"
[http_service]
internal_port = 3000
force_https = true
auto_stop_machines = true
auto_start_machines = true
[checks]
[checks.health]
type = "http"
port = 3000
path = "/health"
interval = "10s"
timeout = "2s"
# Deploy
fly launch
fly deploy
# Set secrets
fly secrets set DATABASE_URL="postgres://..." SECRET_KEY="..."
# Scale
fly scale count 3
fly scale vm shared-cpu-2x
Deployment to Railway
// railway.json (or configure via dashboard)
{
"build": {
"builder": "DOCKERFILE",
"dockerfilePath": "Dockerfile"
},
"deploy": {
"startCommand": "bun run start",
"healthcheckPath": "/health",
"restartPolicyType": "ON_FAILURE"
}
}
Railway auto-detects Bun projects. If using their Nixpacks builder instead of Docker:
// package.json
{
"scripts": {
"start": "bun run dist/server.js",
"build": "bun run build"
}
}
Graceful Shutdown
const server = Bun.serve({
port: 3000,
fetch(req) {
return new Response("OK");
},
});
let isShuttingDown = false;
async function shutdown(signal: string) {
if (isShuttingDown) return;
isShuttingDown = true;
console.log(`Received ${signal}, shutting down gracefully...`);
// Stop accepting new connections
server.stop();
// Close database connections
db.close();
// Allow in-flight requests to complete (max 10s)
await Bun.sleep(10_000);
process.exit(0);
}
process.on("SIGTERM", () => shutdown("SIGTERM"));
process.on("SIGINT", () => shutdown("SIGINT"));
Anti-Patterns
- Not using
--frozen-lockfilein Docker builds: Without it, the lockfile may be updated during the build, leading to non-reproducible images. - Running Bun as root in containers: Always use
USER bunorUSER nonrootin your Dockerfile. Running as root is a security risk. - Skipping health checks in production: Always implement a
/healthendpoint and configure your orchestrator to check it. This enables zero-downtime deploys and automatic recovery. - Using
bun runfor compiled binaries: If you compile withbun build --compile, the output is a standalone executable. Run it directly, not throughbun run. - Hardcoding environment values instead of using Bun.env: Always read configuration from environment variables for 12-factor app compliance.
- Not setting PRAGMA WAL mode for SQLite: Without WAL mode, SQLite blocks readers during writes. Always enable it for server workloads.
- Deploying without a process manager: In production, use Docker restart policies, systemd, or your platform's built-in process management. Do not run
bun run server.tsin a bare terminal.
Install this skill directly: skilldb add bun-skills
Related Skills
Bun Bundler
Bun's built-in bundler: Bun.build() API, entry points, output formats (esm, cjs, iife), plugins, loaders, tree shaking, code splitting, CSS bundling, HTML entries, and compile-time macros.
Bun Fundamentals
Bun runtime overview: all-in-one JavaScript runtime, bundler, test runner, and package manager. Installation, project initialization, Node.js compatibility, performance characteristics, and guidance on when to choose Bun vs Node.
Bun HTTP Server
Building HTTP servers with Bun: Bun.serve() API, routing patterns, WebSocket support, streaming responses, static file serving, TLS configuration, hot reloading, and integration with frameworks like Hono and Elysia.
Bun Node.js Migration
Migrating from Node.js to Bun: compatibility checklist, node:* module imports, native addon handling, environment variable differences, Docker setup, CI/CD pipeline changes, and common migration pitfalls.
Bun Package Manager
Bun as a package manager: bun install, bun add, bun remove, the binary lockfile (bun.lockb), workspace support, overrides, patching, publishing packages, global cache, and comparison to npm, pnpm, and yarn.
Bun Runtime APIs
Bun-native runtime APIs including Bun.serve(), Bun.file(), Bun.write(), Bun.spawn(), Bun.sleep(), Bun.env, FFI for calling native libraries, built-in SQLite, S3 client, glob, and semver utilities.