Skip to main content
bunjs-production Use when deploying Bun.js to production, containerizing with Docker, setting up AWS ECS/Fargate, implementing Redis caching, hardening security, or configuring CI/CD pipelines. See bunjs for basics, bunjs-architecture for patterns.
インストールへ移動 Skills Marketplace コミュニティが作成したAIスキルを発見・探索
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
直接コマンドでは確認用 Prompt が省略されます。実行前にソースを確認してください。
npx skills add https://github.com/MadAppGang/claude-code --skill bunjs-productionコマンドは1行のまま表示されます。コピー前に横へスクロールして全体を確認してください。
ローカルで確認しますか?SkillsMP が現在取得できるファイルをダウンロードできます。
Zipをダウンロード ダウンロード中... 関連職種
SOC
name bunjs-production version 1.0.0 description Use when deploying Bun.js to production, containerizing with Docker, setting up AWS ECS/Fargate, implementing Redis caching, hardening security, or configuring CI/CD pipelines. See bunjs for basics, bunjs-architecture for patterns. keywords ["production deployment","Docker","AWS ECS","Redis","caching","security","CI/CD","logging","monitoring","rate limiting"] plugin dev updated "2026-01-20T00:00:00.000Z"
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
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"
volumes:
- redis-data:/data
volumes:
postgres-data:
redis-data:
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.info ('Graceful shutdown complete' );
process.exit (0 );
} catch (error) {
logger.error ({ error }, 'Error during shutdown' );
process.exit (1 );
}
}
process.on ('SIGTERM' , () => shutdown ('SIGTERM' ));
process.on ('SIGINT' , () => shutdown ('SIGINT' ));
process.on ('unhandledRejection' , (reason, promise ) => {
logger.error ({ reason, promise }, 'Unhandled promise rejection' );
});
process.on ('uncaughtException' , (error ) => {
logger.error ({ error }, 'Uncaught exception' );
shutdown ('UNCAUGHT_EXCEPTION' );
});
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" : [
{
"name" : "NODE_ENV" ,
"value" : "production"
} ,
{
"name" : "PORT" ,
"value" : "3000"
}
] ,
"secrets" : [
{
"name" : "DATABASE_URL" ,
"valueFrom" : "arn:aws:secretsmanager:us-east-1:123456789012:secret:myapp/DATABASE_URL"
} ,
{
"name" : "JWT_SECRET" ,
"valueFrom" : "arn:aws:secretsmanager:us-east-1:123456789012:secret:myapp/JWT_SECRET"
}
] ,
"logConfiguration" : {
"logDriver" : "awslogs" ,
"options" : {
"awslogs-group" : "/ecs/myapp" ,
"awslogs-region" : "us-east-1" ,
"awslogs-stream-prefix" : "ecs"
}
} ,
"healthCheck" : {
"command" : [ "CMD-SHELL" , "wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1" ] ,
"interval" : 30 ,
"timeout" : 5 ,
"retries" : 3 ,
"startPeriod" : 30
}
}
]
}
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" ,
"containerName" : "myapp" ,
"containerPort" : 3000
}
] ,
"healthCheckGracePeriodSeconds" : 60
}
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 ( ) {
await redis.quit ();
logger.info ('Redis connection closed' );
}
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 ): Promise <void > {
const keys = await redis.keys (pattern);
if (keys.length > 0 ) {
await redis.del (...keys);
}
}
export async function cached<T>(
key : string ,
ttl : number ,
fn : () => Promise <T>
): Promise <T> {
const hit = await cacheGet<T>(key);
if (hit) return hit;
const value = await fn ();
await cacheSet (key, value, ttl);
return 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 / 1000 ));
}
c.header ('X-RateLimit-Limit' , maxRequests.toString ());
c.header ('X-RateLimit-Remaining' , Math .max (0 , maxRequests - current).toString ());
if (current > maxRequests) {
return c.json ({ error : 'Too many requests' }, 429 );
}
await next ();
};
}
app.use ('/api/*' , rateLimit ({
windowMs : 15 * 60 * 1000 ,
maxRequests : 100
}));
app.use ('/api/auth/login' , rateLimit ({
windowMs : 15 * 60 * 1000 ,
maxRequests : 5
}));
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 ): TokenPayload {
return jwt.verify (token, JWT_SECRET ) as TokenPayload ;
}
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 .status ,
duration : `${duration} ms`
});
};
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
uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- name: Cache dependencies
uses: actions/cache@v3
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lockb') }}
restore-keys: |
${{ runner.os }}-bun-
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Run Biome check
run: bun run check
- name: TypeScript type check
run: bun run typecheck
- name: Generate Prisma client
run: bunx prisma generate
env:
DATABASE_URL: postgresql://test:test@localhost:5432/test
- name: Run migrations
run: bunx prisma migrate deploy
env:
DATABASE_URL: postgresql://test:test@localhost:5432/test
- name: Run tests
run: bun test --coverage
env:
DATABASE_URL: postgresql://test:test@localhost:5432/test
REDIS_URL: redis://localhost:6379
JWT_SECRET: test-secret
NODE_ENV: test
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
.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: ${{ steps.login-ecr.outputs.registry }}
ECR_REPOSITORY: myapp
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker tag $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY:latest
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest
- name: Deploy to ECS
run: |
aws ecs update-service \
--cluster production-cluster \
--service myapp \
--force-new-deployment
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.