Skip to main content

Bun production deployment: Docker + Kubernetes

Articolo della serie Bun Runtime Production 2027. Pubblicazione prevista 19 November 2027.

What do you think about this article?

Bun production deployment: Docker + Kubernetes
Bun Runtime 2027Photos:panumas nikhomkhaiPexels

At a Glance

Articolo della serie Bun Runtime Production 2027. Pubblicazione prevista 19 November 2027.

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

Discuss this article in the community

Join the Telegram group to discuss with other developers, ask questions and share your experiences.

Did you like this article?

Explore other content on the blog or discover my projects