Deploying Bun to production with Docker and Kubernetes requires 8 combined patterns: multi-stage Docker builds optimized for Bun, robust health checks, resource limits and readiness probes, load balancing with NGINX ingress, secret management for env vars, rolling update strategy with disruption budgets, memory and throughput monitoring, and graceful shutdown of JavaScript runtime. This article documents battle-tested best practices for scaling Bun applications from staging to production on Kubernetes clusters.
Introduction: Bun in Production
Bun is a modern and fast JavaScript runtime, built on Zig, that promises 3-4x performance improvements over Node.js for I/O-intensive applications and critical startup times. However, deploying Bun to production with Docker and Kubernetes presents unique challenges: the runtime is relatively new, the community is growing, and many online tutorials stop at "containerize Node.js". This guide documents a production-grade workflow tested on Kubernetes clusters on VPS with NGINX ingress, Bun 1.1.14+, and Elysia/Hono framerate applications.
Who should read this article: backend engineers with Docker + Kubernetes experience who want to explore Bun for new microservices, or engineering teams evaluating migrations from Node.js to Bun. Prerequisite: familiarity with Kubernetes YAML, kubectl, and typical deployment flow (build → push → apply manifests).
What is Bun and Why Containerize It
Bun is an all-in-one JavaScript runtime that includes package manager (bun install), test runner (bun test), built-in transpiler, and Web API support. Unlike Node.js, Bun is written in Zig (a low-level language) and compiles JavaScript code into an optimized binary. Why containerize it in production?
- Environment isolation: Bun has system dependencies (C libraries, SSL certs) that vary machine to machine. Docker ensures app + runtime + deps live in the same reproducible container.
- Kubernetes orchestration: In production, you need to schedule pods, manage resource limits, auto-restart, and rolling update without downtime. Kubernetes does all this via container images.
- Horizontal scaling: Multiple replicas of the container run on different cluster nodes, balanced by NGINX ingress controller or Istio.
- Security: Bun containers run in unprivileged user, with read-only filesystem, network policies, and secret mount encrypts via etcd.
Local Environment Setup: Bun + Docker
Before deploying to Kubernetes, configure a local environment to test the Bun app and Docker image.
Step 1: Install Bun Locally
# macOS / Linux
curl -fsSL https://bun.sh/install | bash
# Verify
bun --version # e.g., bun 1.1.14
# Create example project (Hono framework)
bun create hono myapp
cd myapp
bun install
Step 2: Write a Simple Hono App
// src/index.ts
import { Hono } from 'hono';
const app = new Hono();
app.get('/', (c) => c.json({ message: 'Hello Bun on K8s!' }));
app.get('/health', (c) => c.json({ status: 'healthy', uptime: process.uptime() }));
app.post('/api/echo', async (c) => {
const body = await c.req.json();
return c.json({ echo: body });
});
export default app;
Step 3: Configure package.json with Start Script
{
"name": "bun-app",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "bun --watch src/index.ts",
"start": "bun src/index.ts",
"build": "bun build ./src/index.ts --target bun --outdir ./dist"
},
"dependencies": {
"hono": "^4.0.0"
}
}
Step 4: Local Testing
# Start Bun app
bun start &
# Or
bun run dev
# In another terminal, test endpoints
curl http://localhost:3000/
curl http://localhost:3000/health
# Ctrl+C to stop
Containerization: Multi-Stage Dockerfile Optimized
The key to a lightweight and fast Docker image is multi-stage build: download dependencies in the build stage, copy only the transpiled app to the run stage, reducing final size.
Dockerfile for Bun
# Stage 1: Builder
FROM oven/bun:1.1.14-alpine AS builder
WORKDIR /build
# Copy package.json/lock
COPY package.json bun.lockb* ./
# Install dependencies
RUN bun install --frozen-lockfile
# Copy source
COPY src ./src
# Build (optional if you use direct run)
RUN bun build ./src/index.ts --target bun --outdir ./dist
# Stage 2: Runtime
FROM oven/bun:1.1.14-alpine
WORKDIR /app
# Non-root user for security
RUN addgroup -g 1001 -S bun && \
adduser -S bun -u 1001
# Copy from builder
COPY --from=builder --chown=bun:bun /build/node_modules ./node_modules
COPY --from=builder --chown=bun:bun /build/package.json ./
COPY --from=builder --chown=bun:bun /build/src ./src
# Change user
USER bun
# Expose port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD bun run src/health.ts || exit 1
# Start app
CMD ["bun", "start"]
Health Check Script (src/health.ts)
// src/health.ts - for healthcheck in Dockerfile
const response = await fetch('http://localhost:3000/health');
const data = await response.json();
if (data.status === 'healthy') {
process.exit(0);
} else {
process.exit(1);
}
Build and Push Image to Registry
After the Dockerfile, compile the image and push it to a container registry (Docker Hub, GitHub Container Registry, or private Kubernetes registry).
# Build local image
docker build -t myapp:1.0.0 .
docker build -t myapp:latest .
# Tag for registry (e.g., GitHub Container Registry)
docker tag myapp:1.0.0 ghcr.io/fedcal/myapp:1.0.0
docker tag myapp:latest ghcr.io/fedcal/myapp:latest
# Login registry (if private)
echo $GH_PAT | docker login ghcr.io -u fedcal --password-stdin
# Push
docker push ghcr.io/fedcal/myapp:1.0.0
docker push ghcr.io/fedcal/myapp:latest
# Verify image on registry
curl https://ghcr.io/v2/fedcal/myapp/tags/list
Kubernetes Deployment: YAML Manifests
Now define Kubernetes objects to deploy the Bun app on the cluster.
Deployment YAML (deployment.yaml)
apiVersion: apps/v1
kind: Deployment
metadata:
name: bun-app
namespace: production
labels:
app: bun-app
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: bun-app
template:
metadata:
labels:
app: bun-app
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "3000"
prometheus.io/path: "/metrics"
spec:
securityContext:
fsGroup: 1001
runAsNonRoot: true
containers:
- name: bun
image: ghcr.io/fedcal/myapp:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 3000
name: http
env:
- name: NODE_ENV
value: "production"
- name: LOG_LEVEL
value: "info"
- name: PORT
value: "3000"
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: bun-app-secrets
key: database-url
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 10
periodSeconds: 30
timeoutSeconds: 3
failureThreshold: 3
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 2
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- bun-app
topologyKey: kubernetes.io/hostname
Service YAML (service.yaml)
apiVersion: v1
kind: Service
metadata:
name: bun-app-service
namespace: production
spec:
type: ClusterIP
selector:
app: bun-app
ports:
- port: 80
targetPort: 3000
protocol: TCP
name: http
sessionAffinity: None
Ingress YAML (ingress.yaml)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: bun-app-ingress
namespace: production
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/rate-limit: "100"
spec:
tls:
- hosts:
- myapp.example.com
secretName: bun-app-tls
rules:
- host: myapp.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: bun-app-service
port:
number: 80
Secret Management: Managing Environment Variables
Don't hardcode secrets in Deployment YAML. Create a Kubernetes Secret:
# Create secret from env file
kubectl create secret generic bun-app-secrets \
--from-literal=database-url="postgres://user:pass@db:5432/mydb" \
--namespace=production
# Or from file
kubectl create secret generic bun-app-secrets \
--from-file=.env.production \
--namespace=production
# Verify
kubectl get secrets -n production
kubectl describe secret bun-app-secrets -n production
Deploy to Kubernetes Cluster
Apply the manifests to the cluster:
# Apply Deployment, Service, Ingress
kubectl apply -f deployment.yaml
kubectl apply -f service.yaml
kubectl apply -f ingress.yaml
# Verify pods are running
kubectl get pods -n production -w
kubectl logs -f deployment/bun-app -n production
# Test ingress from outside cluster
curl https://myapp.example.com/health
Monitoring and Logging
In production, monitor memory, CPU, request latency and error rate of the Bun app.
Prometheus Metrics (optional but recommended)
Add a `/metrics` endpoint to expose Prometheus metrics:
// src/index.ts - add metrics endpoint
import { Hono } from 'hono';
const app = new Hono();
// Request counter
let requestCount = 0;
app.use('*', (c, next) => {
requestCount++;
return next();
});
app.get('/metrics', (c) => {
const metrics = `
# HELP requests_total Total HTTP requests
# TYPE requests_total counter
requests_total{app="bun-app"} ${requestCount}
# HELP process_memory_bytes Memory usage
# TYPE process_memory_bytes gauge
process_memory_bytes ${process.memoryUsage().heapUsed}
`.trim();
return c.text(metrics, 200, { 'Content-Type': 'text/plain' });
});
export default app;
Structured Logging
// src/logger.ts
export function log(level: string, message: string, meta?: any) {
const timestamp = new Date().toISOString();
console.log(JSON.stringify({
timestamp,
level,
message,
...(meta && { meta })
}));
}
// Usage
log('info', 'Server started', { port: 3000 });
log('error', 'DB connection failed', { error: 'ECONNREFUSED' });
Best Practices and Troubleshooting
Best Practices
- Resource Limits: Always define CPU and memory requests/limits (see Deployment YAML above).
- Graceful Shutdown: Intercept SIGTERM in Hono to close DB connections before terminating.
- Health Checks: `/health` endpoint that returns 200 OK if app is ready, 500 if not.
- Rolling Updates: RollingUpdate strategy with maxSurge=1 and maxUnavailable=0 ensures zero-downtime deploys.
- Pod Disruption Budgets: If using cluster autoscaling, define PodDisruptionBudget to prevent eviction of all pods simultaneously.
Common Troubleshooting
Pod Pending or CrashLoopBackOff: Check logs with `kubectl logs -f deployment/bun-app`. Often it's resource limit or missing secret.
Slowness after deploy: Bun on first startup compiles and JIT-compiles code. CPU spike is normal in the first 10-30 seconds. Increase initialDelaySeconds of liveness probe if needed.
Memory leak: If the app grows in memory over time, check for loops that don't deallocate, or connection pool not closed. Use `bun --profile` locally for profiling.
FAQ
Can I use Bun with Express.js instead of Hono?
Yes, Bun is compatible with Express, but Express is optimized for Node.js. Hono (or Elysia) are written for Bun-first and have better performance.
Is Bun in production stable?
Bun 1.1.0+ is stable enough for production workload, but it's still in active development (1.x is not 2.0). Monitor breaking changes between minor releases.
How do I achieve zero-downtime deployments?
Use Deployment with RollingUpdate strategy (maxUnavailable: 0). Kubernetes terminates an old pod only after the new one passes readiness check.
Can I use Bun with serverless (AWS Lambda, Google Cloud Run)?
Partially. Bun 1.0+ supports AWS Lambda custom runtime, but setup is complex. For classic serverless, Node.js 20+ is still more mature.
What are runtime alternatives to Bun?
Node.js (reference runtime), Deno (another alternative runtime, but Deno API standard differs from Node), and Vercel Edge Runtime (serverless only).

Commenti
Caricamento commenti...
Sign in to comment