본문으로 건너뛰기
Advertisement

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()
Advertisement