18.2 Docker + TypeScript — 멀티 스테이지 빌드와 프로덕션 최적화
기본 Dockerfile
# Dockerfile
# ===== 빌드 스테이지 =====
FROM node:20-alpine AS builder
WORKDIR /app
# 의존성 먼저 복사 (캐시 활용)
COPY package*.json ./
COPY tsconfig*.json ./
# 프로덕션 + 개발 의존성 모두 설치 (빌드에 필요)
RUN npm ci
# 소스코드 복사 및 빌드
COPY src ./src
RUN npm run build
# ===== 의존성 스테이지 =====
FROM node:20-alpine AS dependencies
WORKDIR /app
COPY package*.json ./
# 프로덕션 의존성만 설치
RUN npm ci --only=production && npm cache clean --force
# ===== 프로덕션 스테이지 =====
FROM node:20-alpine AS production
# 보안: 루트가 아닌 사용자 실행
RUN addgroup -g 1001 -S nodejs && \
adduser -S nestjs -u 1001
WORKDIR /app
# 빌드 결과물 복사
COPY --from=builder --chown=nestjs:nodejs /app/dist ./dist
# 프로덕션 의존성만 복사
COPY --from=dependencies --chown=nestjs:nodejs /app/node_modules ./node_modules
# 필요한 설정 파일
COPY --chown=nestjs:nodejs package.json ./
USER nestjs
# 포트 노출
EXPOSE 3000
# 헬스 체크
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "dist/main.js"]
.dockerignore
# .dockerignore
node_modules
dist
build
.git
.gitignore
*.md
*.log
.env
.env.*
coverage
.nyc_output
.DS_Store
Thumbs.db
# 테스트 파일
**/*.test.ts
**/*.spec.ts
**/*.test.js
e2e/
docker-compose.yml
# docker-compose.yml
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
target: production
ports:
- '3000:3000'
environment:
NODE_ENV: production
DATABASE_URL: postgresql://postgres:password@db:5432/myapp
JWT_SECRET: ${JWT_SECRET}
REDIS_URL: redis://redis:6379
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
restart: unless-stopped
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U postgres']
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
# 개발 환경 전용
app-dev:
build:
context: .
target: builder # 빌드 스테이지 사용
command: npm run start:dev
volumes:
- .:/app
- /app/node_modules # 컨테이너 node_modules 보존
ports:
- '3000:3000'
- '9229:9229' # 디버거 포트
environment:
NODE_ENV: development
profiles:
- dev
volumes:
postgres_data:
redis_data:
tsconfig.json 빌드 최적화
// tsconfig.json — 기본 설정
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declarationMap": false,
"sourceMap": false
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
}
// tsconfig.build.json — 프로덕션 빌드 전용
{
"extends": "./tsconfig.json",
"compilerOptions": {
"sourceMap": false,
"declaration": false,
"removeComments": true,
"noEmit": false
},
"exclude": [
"node_modules",
"dist",
"test",
"**/*spec.ts",
"**/*test.ts"
]
}
GitHub Actions CI/CD
# .github/workflows/deploy.yml
name: Build and Deploy
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run typecheck # tsc --noEmit
- run: npm run lint
- run: npm run test:ci
build-and-push:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Docker image
run: |
docker build \
--target production \
-t my-app:${{ github.sha }} \
-t my-app:latest \
.
- name: Push to registry
run: |
echo ${{ secrets.REGISTRY_PASSWORD }} | docker login -u ${{ secrets.REGISTRY_USERNAME }} --password-stdin
docker push my-app:${{ github.sha }}
docker push my-app:latest
deploy:
needs: build-and-push
runs-on: ubuntu-latest
steps:
- name: Apply Prisma migrations
run: |
docker run --rm \
-e DATABASE_URL=${{ secrets.DATABASE_URL }} \
my-app:${{ github.sha }} \
npx prisma migrate deploy
- name: Deploy to production
run: |
# 배포 스크립트 실행
echo "Deploying version ${{ github.sha }}"
고수 팁
Node.js 프로세스 신호 처리
// src/main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule)
await app.listen(process.env.PORT ?? 3000)
// 우아한 종료 (Graceful Shutdown)
const signals: NodeJS.Signals[] = ['SIGTERM', 'SIGINT']
signals.forEach(signal => {
process.on(signal, async () => {
console.log(`${signal} received, shutting down gracefully...`)
await app.close()
process.exit(0)
})
})
}
bootstrap()