Skip to main content
Technology & EngineeringBun461 lines

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.

Quick Summary29 lines
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 lines
Paste into your CLAUDE.md or agent config

Bun 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-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.

Install this skill directly: skilldb add bun-skills

Get CLI access →