Node.js 인터뷰 질문 56
질문: Node.js 애플리케이션을 Docker로 컨테이너화하는 방법과 모범 사례는 무엇인가요?
답변:
Docker는 애플리케이션과 그 종속성을 함께 패키징하여 어디서나 동일하게 실행할 수 있는 컨테이너화 기술입니다. Node.js 애플리케이션을 Docker로 컨테이너화하면 개발, 테스트, 배포 환경 간의 일관성을 확보할 수 있고, 확장성과 이식성이 향상됩니다. 여기서는 Node.js 애플리케이션을 Docker로 컨테이너화하는 방법과 모범 사례에 대해 알아보겠습니다.
1. Node.js 애플리케이션 컨테이너화 기본 단계
1.1 기본 Dockerfile 생성
# 베이스 이미지 선택
FROM node:14
# 작업 디렉토리 설정
WORKDIR /app
# 의존성 파일 복사
COPY package*.json ./
# 의존성 설치
RUN npm install
# 소스 코드 복사
COPY . .
# 포트 노출
EXPOSE 3000
# 애플리케이션 실행 명령
CMD ["node", "index.js"]
1.2 .dockerignore 파일 생성
node_modules
npm-debug.log
Dockerfile
.dockerignore
.git
.gitignore
README.md
1.3 이미지 빌드 및 실행
# 이미지 빌드
docker build -t my-node-app .
# 컨테이너 실행
docker run -p 3000:3000 my-node-app
2. 다단계 빌드를 통한 최적화
프로덕션 환경을 위해 더 작고 안전한 이미지를 생성하는 다단계 빌드:
# 빌드 단계
FROM node:14 AS build
WORKDIR /app
# 의존성 파일만 먼저 복사하여 캐싱 활용
COPY package*.json ./
RUN npm ci
# 소스 코드 복사 및 빌드
COPY . .
RUN npm run build
# 프로덕션 단계
FROM node:14-slim
WORKDIR /app
# 프로덕션 의존성만 설치
COPY package*.json ./
RUN npm ci --only=production
# 빌드 결과물만 복사
COPY --from=build /app/dist ./dist
# 보안을 위해 비루트 사용자로 실행
USER node
EXPOSE 3000
# 프로덕션 모드로 실행
CMD ["node", "dist/index.js"]
3. 환경별 구성 관리
3.1 환경 변수 활용
FROM node:14
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
# 환경 변수 설정
ENV NODE_ENV=production
ENV PORT=3000
EXPOSE ${PORT}
# 환경 변수 활용
CMD ["node", "index.js"]
3.2 Docker Compose로 환경 관리
# docker-compose.yml
version: "3"
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- PORT=3000
- DB_HOST=db
- DB_USER=user
- DB_PASSWORD=password
depends_on:
- db
db:
image: mongo:4.4
volumes:
- mongodb_data:/data/db
environment:
- MONGO_INITDB_ROOT_USERNAME=user
- MONGO_INITDB_ROOT_PASSWORD=password
volumes:
mongodb_data:
3.3 환경별 다른 Compose 파일 사용
# 개발 환경
docker-compose -f docker-compose.yml -f docker-compose.dev.yml up
# 프로덕션 환경
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up
4. 노드 애플리케이션 모범 사례
4.1 적절한 베이스 이미지 선택
# 개발 환경: 더 많은 도구 포함
FROM node:14
# 프로덕션 환경: 최소 크기
FROM node:14-slim
# 보안 강화 버전
FROM node:14-alpine
4.2 비루트 사용자로 실행
FROM node:14-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
# 비루트 사용자 생성 및 소유권 변경
RUN mkdir -p /app/node_modules && chown -R node:node /app
# 비루트 사용자로 전환
USER node
EXPOSE 3000
CMD ["node", "index.js"]
4.3 헬스 체크 추가
FROM node:14
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
EXPOSE 3000
# 헬스 체크 정의
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node healthcheck.js || exit 1
CMD ["node", "index.js"]
헬스 체크 스크립트 예시 (healthcheck.js):
const http = require("http");
const options = {
host: "localhost",
port: process.env.PORT || 3000,
path: "/health",
timeout: 2000,
};
const request = http.request(options, (res) => {
console.log(`헬스 체크 상태 코드: ${res.statusCode}`);
process.exit(res.statusCode === 200 ? 0 : 1);
});
request.on("error", (error) => {
console.error("헬스 체크 실패:", error);
process.exit(1);
});
request.end();
5. 성능 최적화 기법
5.1 Node.js 메모리 관리
FROM node:14-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
# 가비지 컬렉션 최적화
ENV NODE_OPTIONS="--max-old-space-size=512"
EXPOSE 3000
CMD ["node", "index.js"]
5.2 이미지 레이어 최적화
변경이 적은 레이어를 먼저 배치하여 캐싱 활용:
FROM node:14
WORKDIR /app
# 자주 변경되지 않는 종속성 파일 먼저 복사
COPY package*.json ./
RUN npm ci
# 그 다음 자주 변경되는 소스 코드 복사
COPY . .
EXPOSE 3000
CMD ["node", "index.js"]
6. 프로덕션 환경 준비 사항
6.1 Graceful Shutdown 처리
// index.js
const express = require("express");
const app = express();
const server = require("http").createServer(app);
// 애플리케이션 로직
app.get("/", (req, res) => {
res.send("Hello World!");
});
// 서버 시작
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`서버가 포트 ${PORT}에서 실행 중입니다`);
});
// Graceful Shutdown 처리
process.on("SIGTERM", () => {
console.log("SIGTERM 신호를 받았습니다. 서버를 종료합니다.");
server.close(() => {
console.log("서버가 정상적으로 종료되었습니다.");
process.exit(0);
});
// 10초 후에도 종료되지 않으면 강제 종료
setTimeout(() => {
console.error("서버 종료 시간 초과. 강제 종료합니다.");
process.exit(1);
}, 10000);
});
6.2 로깅 관리
// 로그를 stdout/stderr로 출력하여 Docker가 수집할 수 있게 함
const logger = {
info: (message) => {
console.log(
JSON.stringify({
level: "info",
message,
timestamp: new Date().toISOString(),
})
);
},
error: (message, error) => {
console.error(
JSON.stringify({
level: "error",
message,
error: error.stack || error.toString(),
timestamp: new Date().toISOString(),
})
);
},
};
// 사용 예
app.get("/api/data", (req, res) => {
logger.info(`데이터 요청 받음: ${req.ip}`);
// ...
});
app.use((err, req, res, next) => {
logger.error("API 오류 발생", err);
res.status(500).json({ error: "서버 오류" });
});
7. 보안 강화 방법
7.1 의존성 보안 스캔
FROM node:14 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
# 보안 취약점 스캔
RUN npm audit --audit-level=high
COPY . .
# ... 이하 동일
7.2 최소 권한 원칙 적용
FROM node:14-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && \
# npm 캐시 제거로 이미지 크기 축소
npm cache clean --force
COPY . .
# 비루트 사용자 생성 및 권한 설정
RUN addgroup -g 1001 -S nodejs && \
adduser -S -u 1001 -G nodejs nodejs && \
chown -R nodejs:nodejs /app
# 비루트 사용자로 전환
USER nodejs
EXPOSE 3000
CMD ["node", "index.js"]
7.3 시크릿 관리
# 시크릿을 빌드 시점에 포함하지 않음
FROM node:14
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
# 시크릿은 환경 변수를 통해 런타임에 주입
ENV JWT_SECRET=
ENV DB_PASSWORD=
EXPOSE 3000
CMD ["node", "index.js"]
Docker Compose에서 시크릿 관리:
version: "3.8"
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
secrets:
- db_password
- jwt_secret
secrets:
db_password:
file: ./secrets/db_password.txt
jwt_secret:
file: ./secrets/jwt_secret.txt
8. 마이크로서비스 아키텍처 지원
8.1. 서비스 디스커버리
Docker Compose를 사용한 간단한 서비스 디스커버리:
version: "3"
services:
api-gateway:
build: ./api-gateway
ports:
- "3000:3000"
environment:
- USER_SERVICE_URL=http://user-service:4000
- PRODUCT_SERVICE_URL=http://product-service:5000
user-service:
build: ./user-service
environment:
- PORT=4000
- DB_HOST=user-db
product-service:
build: ./product-service
environment:
- PORT=5000
- DB_HOST=product-db
user-db:
image: mongo:4.4
product-db:
image: postgres:13
8.2 컨테이너 간 통신
// api-gateway/index.js
const express = require("express");
const axios = require("axios");
const app = express();
const USER_SERVICE = process.env.USER_SERVICE_URL || "http://user-service:4000";
const PRODUCT_SERVICE =
process.env.PRODUCT_SERVICE_URL || "http://product-service:5000";
app.get("/users/:id", async (req, res) => {
try {
const { data } = await axios.get(`${USER_SERVICE}/users/${req.params.id}`);
res.json(data);
} catch (error) {
res.status(500).json({ error: "사용자 서비스 오류" });
}
});
app.get("/products/:id", async (req, res) => {
try {
const { data } = await axios.get(
`${PRODUCT_SERVICE}/products/${req.params.id}`
);
res.json(data);
} catch (error) {
res.status(500).json({ error: "상품 서비스 오류" });
}
});
app.listen(3000, () => {
console.log("API 게이트웨이가 포트 3000에서 실행 중입니다");
});
9. CI/CD 파이프라인 통합
9.1 GitHub Actions 예시
# .github/workflows/docker-build.yml
name: Docker Build and Push
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: 로그인 to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Docker 빌드 및 푸시
uses: docker/build-push-action@v2
with:
context: .
push: true
tags: username/my-node-app:latest
- name: 배포 트리거
run: |
curl -X POST ${{ secrets.DEPLOYMENT_WEBHOOK }}
9.2 Docker Compose를 사용한 배포
# docker-compose.prod.yml
version: "3"
services:
app:
image: username/my-node-app:latest
restart: always
ports:
- "80:3000"
environment:
- NODE_ENV=production
- DB_HOST=db
depends_on:
- db
db:
image: mongo:4.4
volumes:
- mongodb_data:/data/db
restart: always
volumes:
mongodb_data:
배포 스크립트:
#!/bin/bash
# deploy.sh
# 최신 이미지 가져오기
docker-compose pull
# 다운타임 없이 업데이트
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d --no-deps app
# 사용하지 않는 이미지 정리
docker image prune -af
10. 모니터링 및 로깅 설정
10.1 Prometheus 지표 노출
const express = require("express");
const promClient = require("prom-client");
const app = express();
// 기본 지표 수집
const collectDefaultMetrics = promClient.collectDefaultMetrics;
collectDefaultMetrics({ timeout: 5000 });
// 사용자 정의 지표
const httpRequestDurationMicroseconds = new promClient.Histogram({
name: "http_request_duration_ms",
help: "HTTP 요청 지속 시간 (밀리초)",
labelNames: ["method", "route", "status_code"],
buckets: [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000],
});
// 미들웨어
app.use((req, res, next) => {
const start = Date.now();
res.on("finish", () => {
const duration = Date.now() - start;
httpRequestDurationMicroseconds
.labels(req.method, req.route?.path || req.path, res.statusCode)
.observe(duration);
});
next();
});
// 지표 엔드포인트
app.get("/metrics", async (req, res) => {
res.set("Content-Type", promClient.register.contentType);
res.end(await promClient.register.metrics());
});
// 애플리케이션 로직
app.get("/", (req, res) => {
res.send("Hello World!");
});
app.listen(3000, () => {
console.log("서버가 포트 3000에서 실행 중입니다");
});
10.2 Docker Compose로 모니터링 스택 설정
version: "3"
services:
app:
build: .
ports:
- "3000:3000"
prometheus:
image: prom/prometheus
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
grafana:
image: grafana/grafana
ports:
- "3001:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=secret
depends_on:
- prometheus
prometheus.yml 설정:
global:
scrape_interval: 15s
scrape_configs:
- job_name: "node-app"
static_configs:
- targets: ["app:3000"]
요약
Node.js 애플리케이션을 Docker로 컨테이너화하는 것은 개발, 테스트, 배포 환경 간의 일관성을 제공하고 확장성을 개선하는 강력한 방법입니다. 주요 모범 사례는 다음과 같습니다:
- 최적화된 Dockerfile 작성: 다단계 빌드를 사용하여 이미지 크기 최소화하기
- 적절한 베이스 이미지 선택: 환경에 맞는 슬림 또는 알파인 이미지 사용하기
- 보안 강화: 비루트 사용자로 실행하고 의존성을 주기적으로 스캔하기
- 레이어 최적화: 변경이 적은 레이어를 먼저 복사하여 캐싱 활용하기
- 환경 설정 관리: 환경 변수와 시크릿을 적절히 사용하기
- 헬스 체크 구현: 컨테이너 상태를 모니터링할 수 있는 엔드포인트 제공하기
- 우아한 종료 처리: SIGTERM 신호에 대응하여 연결을 정상적으로 종료하기
- 모니터링 및 로깅 설정: 표준 출력으로 로그를 내보내고 모니터링 도구와 통합하기
- CI/CD 파이프라인 통합: 자동화된 빌드 및 배포 프로세스 구축하기
이러한 모범 사례를 따르면 Node.js 애플리케이션을 효율적으로 컨테이너화하고 안정적으로 운영할 수 있습니다. Docker를 통해 마이크로서비스 아키텍처로의 전환도 더 쉽게 이루어질 수 있으며, 각 서비스를 독립적으로 확장하고 배포할 수 있는 유연성을 얻을 수 있습니다.