본문으로 건너뛰기
김신건의 로그

[NestJS] Deployment (Docker, Kubernetes, PM2)

· 수정 · 📖 약 1분 · 433자/단어 #nestjs #deployment #docker #kubernetes #production
NestJS Deployment, NestJS Docker, NestJS Kubernetes, NestJS PM2, NestJS 배포, NestJS production

정의

NestJS Deployment 는 프로덕션 환경에서 앱을 배포/운영하기 위한 절차입니다. Docker 이미지 빌드, Kubernetes 매니페스트, PM2 프로세스 관리, graceful shutdown, health check, 로깅, 관측 도구를 포함합니다.

Build

# 프로덕션 빌드
npm run build     # nest build

# dist/ 에 컴파일된 JS
node dist/main.js

nest-cli.json:

{
  "collection": "@nestjs/schematics",
  "sourceRoot": "src",
  "compilerOptions": {
    "deleteOutDir": true,
    "webpack": false          // SWC (또는 esbuild) 옵션
  }
}

SWC / esbuild (빠른 빌드)

npm i -D @swc/cli @swc/core

nest-cli.json:

{
  "compilerOptions": {
    "builder": "swc",
    "typeCheck": true
  }
}

주의: SWC 는 emitDecoratorMetadata 지원. class-validator 는 여전히 작동.

Dockerfile (multi-stage)

# --- builder ---
FROM node:20-alpine AS builder
WORKDIR /app

# Dependency cache
COPY package*.json ./
RUN npm ci

# Build
COPY . .
RUN npm run build

# Prisma (있으면)
RUN npx prisma generate

# 프로덕션 의존성만
RUN npm ci --omit=dev && npm cache clean --force

# --- runtime ---
FROM node:20-alpine
WORKDIR /app

# non-root user
RUN addgroup -g 1001 -S app && adduser -S app -u 1001

COPY --from=builder --chown=app:app /app/node_modules ./node_modules
COPY --from=builder --chown=app:app /app/dist ./dist
COPY --from=builder --chown=app:app /app/package.json ./
# Prisma
COPY --from=builder --chown=app:app /app/prisma ./prisma

USER app

EXPOSE 3000

# Graceful shutdown
CMD ["node", "dist/main"]

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=30s \
  CMD wget --spider -q http://localhost:3000/health || exit 1

.dockerignore

node_modules
dist
.git
.env
.env.*
*.md
test
coverage
.vscode
.idea

Graceful Shutdown

// main.ts
async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Shutdown hooks 활성화
  app.enableShutdownHooks();

  await app.listen(3000);
}
bootstrap();

Provider 에서 OnApplicationShutdown 구현:

@Injectable()
export class DatabaseService implements OnApplicationShutdown {
  async onApplicationShutdown(signal?: string) {
    console.log(`Shutting down: ${signal}`);
    await this.prisma.$disconnect();
  }
}

동작: SIGTERM 수신 -> 새 요청 accept 중지 -> in-flight 요청 완료 -> shutdown hook 실행 -> 프로세스 종료.

Health Check

@nestjs/terminus + @nestjs/health-check:

npm i @nestjs/terminus
// health.controller.ts
import { Controller, Get } from '@nestjs/common';
import { HealthCheck, HealthCheckService, HttpHealthIndicator, MemoryHealthIndicator } from '@nestjs/terminus';
import { PrismaHealthIndicator } from './prisma.health';

@Controller('health')
export class HealthController {
  constructor(
    private health: HealthCheckService,
    private http: HttpHealthIndicator,
    private memory: MemoryHealthIndicator,
    private prisma: PrismaHealthIndicator,
  ) {}

  @Get('live')
  @HealthCheck()
  liveness() {
    return this.health.check([
      () => this.memory.checkHeap('memory_heap', 250 * 1024 * 1024),
    ]);
  }

  @Get('ready')
  @HealthCheck()
  readiness() {
    return this.health.check([
      () => this.prisma.isHealthy('database'),
      () => this.http.pingCheck('external_api', 'https://external-api/health'),
    ]);
  }
}

Kubernetes probe

  • livenessProbe: 앱 살아있는가 (fail 시 재시작)
  • readinessProbe: 트래픽 받을 준비 (fail 시 LB 에서 제외)
  • startupProbe: 초기 부팅 (긴 초기화)

Kubernetes Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 3
  selector:
    matchLabels: {app: myapp}
  template:
    metadata:
      labels: {app: myapp}
    spec:
      terminationGracePeriodSeconds: 60
      containers:
        - name: app
          image: myrepo/myapp:v1.2.3
          ports: [{containerPort: 3000}]
          env:
            - name: NODE_ENV
              value: production
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef: {name: myapp-secrets, key: db-url}
            - name: JWT_SECRET
              valueFrom:
                secretKeyRef: {name: myapp-secrets, key: jwt-secret}
          resources:
            requests: {cpu: 250m, memory: 256Mi}
            limits: {cpu: 1, memory: 1Gi}
          livenessProbe:
            httpGet: {path: /health/live, port: 3000}
            initialDelaySeconds: 30
            periodSeconds: 10
          readinessProbe:
            httpGet: {path: /health/ready, port: 3000}
            periodSeconds: 5
          startupProbe:
            httpGet: {path: /health/live, port: 3000}
            failureThreshold: 30
            periodSeconds: 5
          lifecycle:
            preStop:
              exec:
                command: ["sleep", "5"]   # LB 제외 대기
---
apiVersion: v1
kind: Service
metadata:
  name: myapp
spec:
  selector: {app: myapp}
  ports:
    - port: 80
      targetPort: 3000
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: myapp
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  ingressClassName: nginx
  tls:
    - hosts: [api.example.com]
      secretName: myapp-tls
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service: {name: myapp, port: {number: 80}}

HPA

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: myapp-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: myapp
  minReplicas: 3
  maxReplicas: 20
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 60

PM2 (Node 프로세스 관리자)

VM 배포 시 사용. Kubernetes 는 kubelet 이 그 역할.

npm i -g pm2

pm2 start dist/main.js --name myapp -i max   # max = CPU 개수
pm2 status
pm2 logs myapp
pm2 reload myapp   # zero-downtime reload
pm2 stop myapp
pm2 startup       # systemd 등록
pm2 save          # 재부팅 시 복원

ecosystem.config.js:

module.exports = {
  apps: [{
    name: 'myapp',
    script: 'dist/main.js',
    instances: 'max',
    exec_mode: 'cluster',
    env_production: {
      NODE_ENV: 'production',
      PORT: 3000,
    },
    max_memory_restart: '1G',
    kill_timeout: 30000,
    listen_timeout: 10000,
    error_file: '/var/log/myapp/error.log',
    out_file: '/var/log/myapp/out.log',
  }],
};

로깅 (JSON, structured)

NestJS 11 native JSON logger

// main.ts
import { ConsoleLogger, LogLevel } from '@nestjs/common';

const app = await NestFactory.create(AppModule, {
  logger: new ConsoleLogger({
    json: true,
    logLevels: ['error', 'warn', 'log'],
  }),
});

nestjs-pino (권장)

npm i nestjs-pino pino-http
import { LoggerModule } from 'nestjs-pino';

@Module({
  imports: [
    LoggerModule.forRoot({
      pinoHttp: {
        level: process.env.LOG_LEVEL || 'info',
        transport: process.env.NODE_ENV === 'development'
          ? { target: 'pino-pretty' }
          : undefined,
        redact: ['req.headers.authorization'],
      },
    }),
  ],
})
export class AppModule {}

main.ts:

const app = await NestFactory.create(AppModule, {bufferLogs: true});
app.useLogger(app.get(Logger));

OpenTelemetry

npm i @opentelemetry/api @opentelemetry/sdk-node \
  @opentelemetry/auto-instrumentations-node \
  @opentelemetry/exporter-trace-otlp-http
// tracing.ts
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';

const sdk = new NodeSDK({
  traceExporter: new OTLPTraceExporter({
    url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
  }),
  instrumentations: [getNodeAutoInstrumentations()],
});

sdk.start();

main.ts 최상단에 import './tracing'; (다른 import 보다 먼저).

Sentry

npm i @sentry/node
import * as Sentry from '@sentry/node';

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  environment: process.env.NODE_ENV,
  tracesSampleRate: 0.1,
});

// AllExceptionsFilter 안에서
Sentry.captureException(exception);

Database Migration

Prisma

# 배포 파이프라인에서
npx prisma migrate deploy

TypeORM

npm run typeorm migration:run

Kubernetes init container

initContainers:
  - name: migrate
    image: myrepo/myapp:v1.2.3
    command: ['npx', 'prisma', 'migrate', 'deploy']
    env:
      - name: DATABASE_URL
        valueFrom:
          secretKeyRef: {name: myapp-secrets, key: db-url}
containers:
  - name: app
    ...

CI/CD 예시 (GitHub Actions)

name: deploy
on:
  push:
    branches: [main]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - name: Build & push
        uses: docker/build-push-action@v5
        with:
          push: true
          tags: |
            ghcr.io/myorg/myapp:${{ github.sha }}
            ghcr.io/myorg/myapp:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max
  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: azure/setup-kubectl@v3
      - uses: azure/k8s-set-context@v3
        with:
          method: kubeconfig
          kubeconfig: ${{ secrets.KUBECONFIG }}
      - run: |
          kubectl set image deployment/myapp app=ghcr.io/myorg/myapp:${{ github.sha }}
          kubectl rollout status deployment/myapp --timeout=5m

함정

WARNING

enableShutdownHooks() 안 부르면 SIGTERM 무시. docker stop 이 SIGKILL 로 강제 종료 -> in-flight 요청 손실.

CAUTION

PM2 는 이미 K8s 컨테이너 안에는 불필요. kubelet 이 프로세스 관리. PM2 는 VM 배포용.

WARNING

환경변수 secret 을 이미지에 굽지 마세요. 항상 runtime injection.

IMPORTANT

terminationGracePeriodSeconds > shutdown hook 완료 시간. K8s 가 강제 종료하기 전에 정리.

CAUTION

Prisma binary target. Alpine + linux-musl 명시 필요. 안 그러면 런타임 오류.

관련 위키

이 글의 용어 (14개)
[Container] Image Best Practices: 작게, 안전하게virtualization
정의 컨테이너 image best practices = 작고 (small), 안전하고 (secure), 재현 가능하고 (reproducible), 서명된 (signed) imag…
[Container] OCI Image: spec, manifest, layer 표준cloud
정의 OCI (Open Container Initiative) = 컨테이너 표준 (Linux Foundation, 2015). image format + runtime + dis…
[FastAPI] Deployment (Uvicorn, Gunicorn, Docker)fastapi
정의 FastAPI 배포는 ASGI 서버 (Uvicorn) + 프로세스 관리자 (Gunicorn 또는 Uvicorn 자체) + 컨테이너 (Docker) + 오케스트레이터 (Kub…
[Flask] Deployment (WSGI, Gunicorn, uWSGI, Docker)flask
정의 Flask 는 WSGI 앱. 프로덕션 배포는 WSGI 서버 (Gunicorn, uWSGI, gevent) + 리버스 프록시 (Nginx, ALB) + 컨테이너 (Docker…
[Framework] NestJSframeworks
정의 NestJS 는 Kamil Myśliwiec 이 2017년 발표한 Node.js 서버측 프레임워크 입니다. TypeScript first, Angular 에서 영감받은 모듈…
[K8s] Deployment: ReplicaSet, rolling update, rollbackkubernetes
정의 Deployment = stateless 워크로드를 위한 컨트롤러. 내부적으로 ReplicaSet 관리 + rolling update / rollback. 사용 시나리오 |…
[K8s] Ingress: L7 라우팅, TLS termination, Gateway APIkubernetes
정의 Ingress = 클러스터 외부 HTTP/HTTPS 트래픽 → 내부 Service 라우팅. L7 LB 역할. Service 의 LoadBalancer 다수 대안. [!IMP…
[K8s] Service: ClusterIP / NodePort / LoadBalancer / ExternalNamekubernetes
정의 Service = Pod 집합에 안정 가상 IP + DNS 부여. Pod 가 죽고 다시 만들어져도 Service IP 는 그대로. 4가지 타입 1. ClusterIP (기본…
[NestJS] Config (Environment, Validation)frameworks
정의 NestJS Config 는 패키지로 환경변수를 관리합니다. 파일 로딩, TypeScript type-safe access, 검증 (Joi/Zod), 여러 환경 (dev/s…
[NestJS] Database (TypeORM, Prisma, Mongoose, Drizzle)frameworks
정의 NestJS Database 통합은 여러 ORM/ODM 을 module 로 감싸 DI + 트랜잭션 + 마이그레이션을 관용화합니다. 주요 선택지: TypeORM (성숙), P…
[NestJS] Microservices (Kafka, NATS, RabbitMQ, gRPC, TCP)frameworks
정의 NestJS Microservices 는 HTTP 대신 message broker (Kafka, RabbitMQ, NATS, Redis) 나 gRPC / TCP 로 통신하는…
[NestJS] Modulesframeworks
정의 NestJS Module 은 데코레이터가 붙은 클래스로, 관련된 controller, provider, import/export 를 하나로 묶는 조직 단위입니다. Angul…
[NestJS] Testing (Jest, Supertest, e2e)frameworks
정의 NestJS Testing 은 Jest (기본) + Supertest 조합으로 unit test 와 e2e test 를 지원합니다. 의 로 module 을 격리된 형태로 만…
Kuberneteskubernetes
정의 Kubernetes (k8s) 는 컨테이너화된 애플리케이션의 배포, 스케일링, 관리 를 자동화하는 오픈소스 오케스트레이터입니다. Google 이 2014년 발표하고 2015…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기