home / skills / madappgang / claude-code / bunjs-production

This skill covers Bun.js production deployment patterns, Dockerization, ECS/Fargate, Redis caching, security hardening, and CI/CD readiness.

npx playbooks add skill madappgang/claude-code --skill bunjs-production

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
23.6 KB
---
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-20
---

# 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

```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)

```yaml
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

```bash
# Build image
docker build -t myapp:latest .

# Run locally
docker-compose up -d

# Run migrations
docker-compose exec app bunx prisma migrate deploy

# View logs
docker-compose logs -f app

# Stop
docker-compose down
```

## Graceful Shutdown

### Server with Shutdown Handling

```typescript
// src/server.ts
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;

// Start server
const server = serve({
  fetch: app.fetch,
  port: PORT
});

logger.info(`🚀 Server running on port ${PORT}`);

// Graceful shutdown handler
async function shutdown(signal: string) {
  logger.info(`Received ${signal}, initiating graceful shutdown...`);

  try {
    // Close HTTP server (stop accepting new requests)
    server.close();
    logger.info('HTTP server closed');

    // Close database connections
    await prisma.$disconnect();
    logger.info('Database connections closed');

    // Close Redis connections (if used)
    // await redis.quit();

    logger.info('Graceful shutdown complete');
    process.exit(0);
  } catch (error) {
    logger.error({ error }, 'Error during shutdown');
    process.exit(1);
  }
}

// Handle termination signals
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));

// Handle unhandled errors
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)

```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)

```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

```bash
#!/bin/bash
# deploy.sh

set -e

# Variables
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"

# 1. Login to ECR
aws ecr get-login-password --region $AWS_REGION | \
  docker login --username AWS --password-stdin $ECR_REGISTRY

# 2. Build image
docker build -t $IMAGE_NAME:$IMAGE_TAG .

# 3. Tag image
docker tag $IMAGE_NAME:$IMAGE_TAG $ECR_REGISTRY/$IMAGE_NAME:$IMAGE_TAG
docker tag $IMAGE_NAME:$IMAGE_TAG $ECR_REGISTRY/$IMAGE_NAME:latest

# 4. Push to ECR
docker push $ECR_REGISTRY/$IMAGE_NAME:$IMAGE_TAG
docker push $ECR_REGISTRY/$IMAGE_NAME:latest

# 5. Update ECS service (force new deployment)
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

```typescript
// src/utils/redis.ts
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');
});

// Graceful shutdown
export async function closeRedis() {
  await redis.quit();
  logger.info('Redis connection closed');
}
```

### Cache Utilities

```typescript
// src/utils/cache.ts
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);
  }
}

// Cache-aside pattern
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

```typescript
// src/services/user.service.ts
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);

  // Invalidate cache after update
  await cacheDelete(`user:${id}`);

  const { password, ...withoutPassword } = user;
  return withoutPassword;
};
```

### Cache Key Conventions

```typescript
// ✅ CORRECT: Namespaced keys
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}`,
};

// Cache TTLs
const ttl = {
  short: 60,        // 1 minute
  medium: 300,      // 5 minutes
  long: 3600,       // 1 hour
  veryLong: 86400,  // 1 day
};
```

## Security Best Practices

### Security Headers Middleware

```typescript
// src/middleware/security.ts
import type { Context, Next } from 'hono';

export const securityHeaders = async (c: Context, next: Next) => {
  await next();

  // Prevent MIME type sniffing
  c.header('X-Content-Type-Options', 'nosniff');

  // Prevent clickjacking
  c.header('X-Frame-Options', 'DENY');

  // Enable XSS filter
  c.header('X-XSS-Protection', '1; mode=block');

  // Force HTTPS
  c.header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');

  // Content Security Policy
  c.header('Content-Security-Policy', "default-src 'self'; script-src 'self'; object-src 'none'");

  // Referrer Policy
  c.header('Referrer-Policy', 'strict-origin-when-cross-origin');

  // Permissions Policy
  c.header('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
};
```

### CORS Configuration

```typescript
// src/app.ts
import { cors } from 'hono/cors';

app.use('*', cors({
  origin: (origin) => {
    // Whitelist approach
    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

```typescript
// src/middleware/rateLimit.ts
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();
  };
}

// Usage in routes
app.use('/api/*', rateLimit({
  windowMs: 15 * 60 * 1000,  // 15 minutes
  maxRequests: 100
}));

app.use('/api/auth/login', rateLimit({
  windowMs: 15 * 60 * 1000,
  maxRequests: 5  // Stricter for login
}));
```

### Password Hashing

```typescript
// src/utils/password.ts
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

```typescript
// src/utils/jwt.ts
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

```typescript
// src/core/logger.ts
import pino from 'pino';

const isDev = process.env.NODE_ENV === 'development';

export const logger = pino({
  level: process.env.LOG_LEVEL || (isDev ? 'debug' : 'info'),

  // Pretty print in development
  transport: isDev ? {
    target: 'pino-pretty',
    options: {
      colorize: true,
      translateTime: 'SYS:standard',
      ignore: 'pid,hostname'
    }
  } : undefined,

  // Remove pid and hostname in production
  base: isDev ? undefined : {},

  // Custom formatters
  formatters: {
    level: (label) => ({ level: label })
  },

  // Redact sensitive fields
  redact: {
    paths: ['password', 'token', 'authorization', 'cookie'],
    censor: '[REDACTED]'
  }
});
```

### Request Logging Middleware

```typescript
// src/middleware/requestLogger.ts
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

```typescript
// ✅ CORRECT: Structured logging
logger.info({ userId: '123', action: 'login' }, 'User logged in');

// ✅ CORRECT: Error logging with context
logger.error({ error: err, userId: '123' }, 'Failed to create order');

// ❌ WRONG: String-only logs (not queryable)
logger.info('User 123 logged in');

// ❌ WRONG: Logging sensitive data
logger.info({ password: 'secret123' }, 'User created');  // BAD!
```

## CI/CD with GitHub Actions

### .github/workflows/ci.yml

```yaml
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

```yaml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest

    permissions:
      id-token: write  # Required for AWS OIDC
      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
- [ ] No secrets in code (use AWS Secrets Manager or Parameter Store)
- [ ] Password hashing with bcrypt (10+ rounds)
- [ ] JWT with reasonable expiries (15m access, 7d refresh)
- [ ] CORS restricted to known origins (no wildcards)
- [ ] Rate limiting enabled (per endpoint and global)
- [ ] Security headers configured (CSP, X-Frame-Options, HSTS, etc.)
- [ ] Least-privilege database user (no superuser)
- [ ] Input validation on all endpoints (Zod schemas)
- [ ] SQL injection prevention (Prisma parameterized queries)
- [ ] XSS prevention (escape user input, CSP headers)

### Performance
- [ ] Database indexes on frequently queried fields
- [ ] Query optimization (select only needed fields)
- [ ] Redis caching for expensive operations
- [ ] Compression enabled (gzip/brotli)
- [ ] Connection pooling configured (Prisma)
- [ ] Pagination implemented for list endpoints
- [ ] N+1 query prevention (Prisma includes)

### Reliability
- [ ] Health checks implemented (`/health` endpoint)
- [ ] Graceful shutdown handling (SIGTERM, SIGINT)
- [ ] Structured logging with Pino
- [ ] Error tracking configured (Sentry, CloudWatch)
- [ ] Database backups & disaster recovery plan
- [ ] Zero-downtime deployments (rolling updates)
- [ ] Auto-scaling configured (CPU/memory thresholds)

### Monitoring
- [ ] CloudWatch logs enabled
- [ ] CloudWatch metrics (CPU, memory, request count)
- [ ] CloudWatch alarms (high error rate, high latency)
- [ ] Application metrics (request duration, DB query time)
- [ ] Log aggregation configured
- [ ] Uptime monitoring (external service)

### Quality
- [ ] Tests passing with >80% coverage
- [ ] Biome checks passing (format + lint)
- [ ] TypeScript strict mode enabled
- [ ] No console.log in production code (use logger)
- [ ] Error handling comprehensive
- [ ] API documentation up to date (OpenAPI/Swagger)

### Deployment
- [ ] CI/CD pipeline working (GitHub Actions)
- [ ] Migrations tested and reversible
- [ ] Rollback strategy defined
- [ ] Staging environment with prod parity
- [ ] Load testing performed
- [ ] Security audit completed

## Environment Variables

### Development (.env)
```bash
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)
```bash
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

```typescript
// ❌ WRONG: N+1 query problem
const users = await prisma.user.findMany();
for (const user of users) {
  const orders = await prisma.order.findMany({ where: { userId: user.userId } });
}

// ✅ CORRECT: Include relations
const users = await prisma.user.findMany({
  include: { orders: true }
});

// ✅ CORRECT: Select only needed fields
const users = await prisma.user.findMany({
  select: { userId: true, firstName: true, emailAddress: true }
});
```

### 2. Redis Caching Strategy

```typescript
// Cache frequently accessed, rarely changing data
const popularProducts = await cached('products:popular', 3600, () =>
  productRepository.findPopular(10)
);

// Short TTL for data that changes frequently
const userProfile = await cached(`user:${userId}:profile`, 300, () =>
  userRepository.findById(userId)
);

// Invalidate cache on updates
await cacheDelete(`user:${userId}:profile`);
```

### 3. Connection Pooling

```typescript
// Prisma connection pool (in schema.prisma)
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
  // Add connection pool parameters
  // postgresql://user:password@host:5432/db?connection_limit=10&pool_timeout=60
}
```

---

*Production deployment patterns for Bun.js TypeScript backend. For core patterns, see dev:bunjs. For architecture, see dev:bunjs-architecture.*

Overview

This skill documents production deployment patterns for Bun.js TypeScript backends. It focuses on containerization with Docker, running on AWS ECS/Fargate, Redis caching, security hardening, structured shutdowns, and CI/CD deployment scripts to make Bun.js apps production-ready.

How this skill works

It provides concrete artifacts: a multi-stage Dockerfile and docker-compose for local development, graceful shutdown and logging examples, Redis client and cache-aside utilities, security middleware (headers, CORS, rate limiting), and ECS task/service JSON plus a deploy script. Use the code patterns as drop-in building blocks and adapt environment and secret references for your infra.

When to use it

  • Packaging a Bun.js app into a minimal, non-root Docker image for production
  • Deploying container images to AWS ECS/Fargate with health checks and logging
  • Adding Redis caching via a cache-aside pattern and key/TTL conventions
  • Hardening HTTP surface with security headers, CORS, and rate limiting
  • Implementing graceful shutdown to close DB/Redis and avoid dropped requests
  • Automating builds and deployments in CI/CD pipelines (ECR + ECS)

Best practices

  • Use multi-stage Docker builds to keep final image small and run as a non-root user
  • Expose a /health endpoint and configure container health checks and ECS grace periods
  • Name and namespace Redis keys consistently and choose TTLs by data volatility
  • Invalidate or delete cache entries after writes to ensure consistency
  • Store secrets in AWS Secrets Manager and pass them as ECS secrets, not env files
  • Gracefully close DB and Redis connections on SIGTERM/SIGINT before exit

Example use cases

  • Build and push a Bun.js service image in CI, then trigger an ECS force-new-deployment
  • Run local dev with docker-compose including Postgres and Redis, with hot reload
  • Protect authentication endpoints with stricter rate limits and a Redis-backed counter
  • Use cached() wrapper to reduce DB load for read-heavy user/profile endpoints
  • Apply security headers and CSP middleware to reduce XSS and clickjacking risk

FAQ

How do I handle migrations in production?

Run migrations as a distinct CI/CD step or init job (e.g., `bunx prisma migrate deploy`) before updating ECS tasks to ensure schema is ready.

What Redis error handling is recommended?

Use retry strategies, reconnectOnError, log connection events, and ensure graceful shutdown calls redis.quit() to avoid orphaned connections.