| name | bunjs-production |
| description | Provides Bun.js production patterns — Docker, AWS ECS/Fargate, Redis caching, security hardening, CI/CD. Use when deploying or operationalizing a Bun.js service. |
| user-invocable | false |
Bun.js Production Deployment Patterns
Overview
This skill covers production deployment patterns for Bun.js TypeScript backend applications, including Docker containerization, AWS ECS deployment, Redis caching, security hardening, structured logging, CI/CD pipelines, and production readiness checklists.
When to use this skill:
- Containerizing applications with Docker
- Deploying to AWS ECS/Fargate
- Implementing Redis caching strategies
- Hardening security (headers, CORS, rate limiting)
- Setting up CI/CD pipelines
- Preparing for production deployment
See also:
- dev:bunjs - Core Bun patterns, HTTP servers, database access
- dev:bunjs-architecture - Layered architecture, camelCase conventions
- dev:bunjs-apidog - OpenAPI specifications and Apidog integration
Docker Multi-Stage Build
Production Dockerfile
# Stage 1: Base
FROM oven/bun:1-alpine AS base
WORKDIR /app
# Stage 2: Dependencies
FROM base AS deps
COPY package.json bun.lockb ./
COPY prisma ./prisma/
RUN bun install --frozen-lockfile --production
# Stage 3: Build
FROM base AS build
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN bunx prisma generate
RUN bun run build # Optional: if you have a build step
# Stage 4: Runner
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
# Create non-root user
RUN addgroup -g 1001 bungroup && \
adduser -D -u 1001 -G bungroup bunuser
# Copy dependencies and source
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/src ./src
COPY --from=build /app/prisma ./prisma
COPY --from=build /app/node_modules/.prisma ./node_modules/.prisma
COPY package.json bun.lockb ./
# Set ownership
RUN chown -R bunuser:bungroup /app
USER bunuser
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["bun", "src/server.ts"]
docker-compose.yml (Local Development)
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- NODE_ENV=development
- DATABASE_URL=postgresql://user:password@postgres:5432/mydb
- REDIS_URL=redis://redis:6379
depends_on:
- postgres
- redis
volumes:
- ./src:/app/src
command: bun --hot src/server.ts
postgres:
image: postgres:17-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: password
POSTGRES_DB: mydb
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
ports:
- "6379:6379"
Build and Run Commands
docker build -t myapp:latest .
docker-compose up -d
docker-compose exec app bunx prisma migrate deploy
docker-compose logs -f app
docker-compose down
Graceful Shutdown
Server with Shutdown Handling
import { serve } from '@hono/node-server';
import { app } from './app';
import { prisma } from '@/database/client';
import { logger } from '@core/logger';
const PORT = Number(process.env.PORT) || 3000;
const server = serve({
fetch: app.fetch,
port: PORT
});
logger.info(`🚀 Server running on port ${PORT}`);
async function shutdown(signal: string) {
logger.info(`Received ${signal}, initiating graceful shutdown...`);
try {
server.close();
logger.info('HTTP server closed');
await prisma.$disconnect();
logger.info('Database connections closed');
logger.();
process.();
} (error) {
logger.({ error }, );
process.();
}
}
process.(, ());
process.(, ());
process.(, {
logger.({ reason, promise }, );
});
process.(, {
logger.({ error }, );
();
});
AWS ECS Deployment
Task Definition (JSON)
{
"family": "myapp",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "256",
"memory": "512",
"containerDefinitions": [
{
"name": "myapp",
"image": "123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:latest",
"essential": true,
"portMappings": [
{
"containerPort": 3000,
"protocol": "tcp"
}
],
"environment": [
Service Definition (JSON)
{
"serviceName": "myapp",
"cluster": "production-cluster",
"taskDefinition": "myapp:1",
"desiredCount": 2,
"launchType": "FARGATE",
"networkConfiguration": {
"awsvpcConfiguration": {
"subnets": [
"subnet-12345678",
"subnet-87654321"
],
"securityGroups": [
"sg-12345678"
],
"assignPublicIp": "DISABLED"
}
},
"loadBalancers": [
{
"targetGroupArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/myapp/1234567890123456"
Deployment Script
#!/bin/bash
set -e
AWS_REGION="us-east-1"
ECR_REGISTRY="123456789012.dkr.ecr.${AWS_REGION}.amazonaws.com"
IMAGE_NAME="myapp"
IMAGE_TAG="${GITHUB_SHA:0:7}"
CLUSTER_NAME="production-cluster"
SERVICE_NAME="myapp"
aws ecr get-login-password --region $AWS_REGION | \
docker login --username AWS --password-stdin $ECR_REGISTRY
docker build -t $IMAGE_NAME:$IMAGE_TAG .
docker tag $IMAGE_NAME:$IMAGE_TAG $ECR_REGISTRY/$IMAGE_NAME:$IMAGE_TAG
docker tag $IMAGE_NAME:$IMAGE_TAG $ECR_REGISTRY/$IMAGE_NAME:latest
docker push $ECR_REGISTRY/$IMAGE_NAME:$IMAGE_TAG
docker push $ECR_REGISTRY/$IMAGE_NAME:latest
aws ecs update-service \
--cluster $CLUSTER_NAME \
--service $SERVICE_NAME \
--force-new-deployment \
--region $AWS_REGION
echo "Deployment initiated. Check ECS console for status."
Caching with Redis
Redis Client Setup
import Redis from 'ioredis';
import { logger } from '@core/logger';
const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379';
export const redis = new Redis(redisUrl, {
maxRetriesPerRequest: 3,
retryStrategy(times) {
const delay = Math.min(times * 50, 2000);
return delay;
},
reconnectOnError(err) {
logger.error({ error: err }, 'Redis connection error');
return true;
}
});
redis.on('connect', () => {
logger.info('Redis connected');
});
redis.on('error', (err) => {
logger.error({ error: err }, 'Redis error');
});
export async function closeRedis() {
redis.();
logger.();
}
Cache Utilities
import { redis } from './redis';
export async function cacheGet<T>(key: string): Promise<T | null> {
const value = await redis.get(key);
return value ? JSON.parse(value) : null;
}
export async function cacheSet(
key: string,
value: any,
ttlSeconds: number
): Promise<void> {
await redis.set(key, JSON.stringify(value), 'EX', ttlSeconds);
}
export async function cacheDelete(key: string): Promise<void> {
await redis.del(key);
}
export async function cacheDeletePattern(pattern: string): <> {
keys = redis.(pattern);
(keys. > ) {
redis.(...keys);
}
}
cached<T>(
: ,
: ,
: <T>
): <T> {
hit = cacheGet<T>(key);
(hit) hit;
value = ();
(key, value, ttl);
value;
}
Usage in Services
import { cached, cacheDelete } from '@utils/cache';
import { userRepository } from '@/database/repositories/user.repository';
export const getUserById = async (id: string) => {
return cached(`user:${id}`, 300, async () => {
const user = await userRepository.findById(id);
if (!user) throw new NotFoundError('User');
const { password, ...withoutPassword } = user;
return withoutPassword;
});
};
export const updateUser = async (id: string, data: UpdateUserDto) => {
const user = await userRepository.update(id, data);
await cacheDelete(`user:${id}`);
const { password, ...withoutPassword } = user;
return withoutPassword;
};
Cache Key Conventions
const keys = {
user: (id: string) => `user:${id}`,
userProfile: (id: string) => `user:${id}:profile`,
userOrders: (id: string) => `user:${id}:orders`,
orderList: (page: number) => `orders:page:${page}`,
};
const ttl = {
short: 60,
medium: 300,
long: 3600,
veryLong: 86400,
};
Security Best Practices
Security Headers Middleware
import type { Context, Next } from 'hono';
export const securityHeaders = async (c: Context, next: Next) => {
await next();
c.header('X-Content-Type-Options', 'nosniff');
c.header('X-Frame-Options', 'DENY');
c.header('X-XSS-Protection', '1; mode=block');
c.header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
c.header('Content-Security-Policy', "default-src 'self'; script-src 'self'; object-src 'none'");
c.header('Referrer-Policy', 'strict-origin-when-cross-origin');
c.header('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
};
CORS Configuration
import { cors } from 'hono/cors';
app.use('*', cors({
origin: (origin) => {
const allowedOrigins = [
'https://yourapp.com',
'https://www.yourapp.com',
'https://admin.yourapp.com'
];
if (process.env.NODE_ENV === 'development') {
allowedOrigins.push('http://localhost:3000');
allowedOrigins.push('http://localhost:5173');
}
return allowedOrigins.includes(origin) ? origin : allowedOrigins[0];
},
allowHeaders: ['Content-Type', 'Authorization'],
allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
credentials: true,
maxAge: 86400
}));
Rate Limiting
import { redis } from '@utils/redis';
import type { Context, Next } from 'hono';
interface RateLimitOptions {
windowMs: number;
maxRequests: number;
keyGenerator?: (c: Context) => string;
}
export function rateLimit(options: RateLimitOptions) {
const { windowMs, maxRequests, keyGenerator = (c) => c.req.header('x-forwarded-for') || 'unknown' } = options;
return async (c: Context, next: Next) => {
const key = `ratelimit:${keyGenerator(c)}`;
const current = await redis.incr(key);
if (current === 1) {
await redis.expire(key, Math.ceil(windowMs / ));
}
c.(, maxRequests.());
c.(, .(, maxRequests - current).());
(current > maxRequests) {
c.({ : }, );
}
();
};
}
app.(, ({
: * * ,
:
}));
app.(, ({
: * * ,
:
}));
Password Hashing
import bcrypt from 'bcrypt';
const SALT_ROUNDS = 10;
export async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, SALT_ROUNDS);
}
export async function verifyPassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
JWT Token Security
import jwt from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET!;
const ACCESS_TOKEN_EXPIRES = '15m';
const REFRESH_TOKEN_EXPIRES = '7d';
interface TokenPayload {
userId: string;
email: string;
role: string;
}
export function generateAccessToken(payload: TokenPayload): string {
return jwt.sign(payload, JWT_SECRET, { expiresIn: ACCESS_TOKEN_EXPIRES });
}
export function generateRefreshToken(payload: TokenPayload): string {
return jwt.sign(payload, JWT_SECRET, { expiresIn: REFRESH_TOKEN_EXPIRES });
}
export function verifyToken(token: string): {
jwt.(token, ) ;
}
Structured Logging with Pino
Logger Setup
import pino from 'pino';
const isDev = process.env.NODE_ENV === 'development';
export const logger = pino({
level: process.env.LOG_LEVEL || (isDev ? 'debug' : 'info'),
transport: isDev ? {
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'SYS:standard',
ignore: 'pid,hostname'
}
} : undefined,
base: isDev ? undefined : {},
formatters: {
level: (label) => ({ level: label })
},
redact: {
paths: ['password', 'token', 'authorization', 'cookie'],
censor: '[REDACTED]'
}
});
Request Logging Middleware
import type { Context, Next } from 'hono';
import { logger } from '@core/logger';
export const requestLogger = async (c: Context, next: Next) => {
const start = Date.now();
const requestId = crypto.randomUUID();
c.set('requestId', requestId);
logger.info({
type: 'request',
requestId,
method: c.req.method,
path: c.req.path,
query: c.req.query(),
userAgent: c.req.header('user-agent')
});
await next();
const duration = Date.now() - start;
logger.info({
type: 'response',
requestId,
status: c.res.,
:
});
};
Logging Best Practices
logger.info({ userId: '123', action: 'login' }, 'User logged in');
logger.error({ error: err, userId: '123' }, 'Failed to create order');
logger.info('User 123 logged in');
logger.info({ password: 'secret123' }, 'User created');
CI/CD with GitHub Actions
.github/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:17-alpine
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
redis:
image: redis:7-alpine
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 6379:6379
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
.github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: us-east-1
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Build and push Docker image
env:
ECR_REGISTRY:
Production Readiness Checklist
Security
Performance
Reliability
Monitoring
Quality
Deployment
Environment Variables
Development (.env)
NODE_ENV=development
PORT=3000
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
REDIS_URL=redis://localhost:6379
JWT_SECRET=dev-secret-change-in-prod
LOG_LEVEL=debug
Production (AWS Secrets Manager)
NODE_ENV=production
PORT=3000
DATABASE_URL=<from-secrets-manager>
REDIS_URL=<from-elasticache>
JWT_SECRET=<from-secrets-manager>
LOG_LEVEL=info
NEVER commit .env files to git. Use .env.example template instead.
Performance Optimization Tips
1. Database Query Optimization
const users = await prisma.user.findMany();
for (const user of users) {
const orders = await prisma.order.findMany({ where: { userId: user.userId } });
}
const users = await prisma.user.findMany({
include: { orders: true }
});
const users = await prisma.user.findMany({
select: { userId: true, firstName: true, emailAddress: true }
});
2. Redis Caching Strategy
const popularProducts = await cached('products:popular', 3600, () =>
productRepository.findPopular(10)
);
const userProfile = await cached(`user:${userId}:profile`, 300, () =>
userRepository.findById(userId)
);
await cacheDelete(`user:${userId}:profile`);
3. Connection Pooling
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
Production deployment patterns for Bun.js TypeScript backend. For core patterns, see dev:bunjs. For architecture, see dev:bunjs-architecture.