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로 컨테이너화하는 것은 개발, 테스트, 배포 환경 간의 일관성을 제공하고 확장성을 개선하는 강력한 방법입니다. 주요 모범 사례는 다음과 같습니다:

  1. 최적화된 Dockerfile 작성: 다단계 빌드를 사용하여 이미지 크기 최소화하기
  2. 적절한 베이스 이미지 선택: 환경에 맞는 슬림 또는 알파인 이미지 사용하기
  3. 보안 강화: 비루트 사용자로 실행하고 의존성을 주기적으로 스캔하기
  4. 레이어 최적화: 변경이 적은 레이어를 먼저 복사하여 캐싱 활용하기
  5. 환경 설정 관리: 환경 변수와 시크릿을 적절히 사용하기
  6. 헬스 체크 구현: 컨테이너 상태를 모니터링할 수 있는 엔드포인트 제공하기
  7. 우아한 종료 처리: SIGTERM 신호에 대응하여 연결을 정상적으로 종료하기
  8. 모니터링 및 로깅 설정: 표준 출력으로 로그를 내보내고 모니터링 도구와 통합하기
  9. CI/CD 파이프라인 통합: 자동화된 빌드 및 배포 프로세스 구축하기

이러한 모범 사례를 따르면 Node.js 애플리케이션을 효율적으로 컨테이너화하고 안정적으로 운영할 수 있습니다. Docker를 통해 마이크로서비스 아키텍처로의 전환도 더 쉽게 이루어질 수 있으며, 각 서비스를 독립적으로 확장하고 배포할 수 있는 유연성을 얻을 수 있습니다.

results matching ""

    No results matching ""