Node.js 인터뷰 질문 28

질문: Node.js 애플리케이션을 Docker 컨테이너화하는 방법과 최적화 전략에 대해 설명해주세요.

답변:

Node.js 애플리케이션을 Docker 컨테이너화하면 개발, 테스트, 프로덕션 환경 간의 일관성을 유지하고, 배포 프로세스를 단순화하며, 확장성을 향상시킬 수 있습니다. 효율적인 컨테이너화를 위해서는 이미지 크기, 빌드 시간, 보안 및 성능을 고려한 최적화가 필요합니다.

기본적인 Node.js 애플리케이션 컨테이너화

1. Dockerfile 작성

# 기본 이미지 선택
FROM node:18

# 작업 디렉토리 설정
WORKDIR /usr/src/app

# 의존성 파일 복사
COPY package*.json ./

# 의존성 설치
RUN npm install

# 애플리케이션 소스 복사
COPY . .

# 실행 포트 설정
EXPOSE 3000

# 시작 명령어 정의
CMD ["node", "app.js"]

2. 이미지 빌드 및 실행

# 이미지 빌드
docker build -t my-nodejs-app .

# 컨테이너 실행
docker run -p 3000:3000 -d my-nodejs-app

프로덕션용 Node.js 컨테이너 최적화 전략

1. 다단계 빌드(Multi-stage Build) 활용

다단계 빌드는 최종 이미지 크기를 줄이는 데 매우 효과적입니다. 특히 TypeScript와 같은 컴파일 언어를 사용하는 경우에 유용합니다.

# 빌드 단계
FROM node:18 AS build

WORKDIR /usr/src/app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

# 프로덕션 단계
FROM node:18-slim

WORKDIR /usr/src/app

COPY package*.json ./
RUN npm ci --only=production

# 빌드 단계에서 생성된 파일만 복사
COPY --from=build /usr/src/app/dist ./dist

EXPOSE 3000
CMD ["node", "dist/app.js"]

2. 적절한 베이스 이미지 선택

  • node:slim: 표준 Node.js 이미지보다 크기가 작음
  • node:alpine: 가장 작은 크기의 이미지지만, 일부 네이티브 모듈과 호환성 문제가 있을 수 있음
  • distroless: 최소한의 시스템 라이브러리만 포함하여 보안 이점 제공
# Alpine 이미지 사용 예시
FROM node:18-alpine

WORKDIR /usr/src/app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

EXPOSE 3000
CMD ["node", "app.js"]

3. .dockerignore 파일 활용

불필요한 파일이 이미지에 포함되지 않도록 .dockerignore 파일을 설정합니다.

# .dockerignore
node_modules
npm-debug.log
Dockerfile
.dockerignore
.git
.github
.gitignore
README.md
docker-compose*
*.env
*.log
coverage
.vscode
.idea
tests

4. 의존성 캐싱 최적화

Docker 빌드 캐시를 효과적으로 활용하여 빌드 시간을 단축합니다.

FROM node:18-slim

WORKDIR /usr/src/app

# package.json과 package-lock.json만 먼저 복사
COPY package*.json ./

# 의존성 설치
RUN npm ci --only=production

# 그 다음 나머지 소스 복사
COPY . .

EXPOSE 3000
CMD ["node", "app.js"]

5. 비 루트 사용자로 애플리케이션 실행

보안 강화를 위해 비 루트 사용자로 애플리케이션을 실행합니다.

FROM node:18-slim

WORKDIR /usr/src/app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

# 비 루트 사용자 생성 및 전환
RUN groupadd -r nodejs && useradd -r -g nodejs nodejs
USER nodejs

EXPOSE 3000
CMD ["node", "app.js"]

6. 환경 변수를 통한 구성

환경별 설정을 환경 변수로 관리합니다.

FROM node:18-slim

WORKDIR /usr/src/app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

# 기본 환경 변수 설정
ENV NODE_ENV=production
ENV PORT=3000

EXPOSE 3000
CMD ["node", "app.js"]

실행 시 필요한 환경 변수를 주입합니다.

docker run -p 3000:3000 -e "DB_HOST=mongodb://db" -e "API_KEY=secret" my-nodejs-app

7. 헬스 체크 설정

컨테이너 오케스트레이션 도구와의 통합을 위해 헬스 체크를 구현합니다.

FROM node:18-slim

WORKDIR /usr/src/app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

EXPOSE 3000

# 헬스 체크 설정
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1

CMD ["node", "app.js"]

Express.js 애플리케이션에 헬스 체크 엔드포인트를 추가합니다.

const express = require("express");
const app = express();

// 기본 라우트
app.get("/", (req, res) => {
  res.send("Hello World!");
});

// 헬스 체크 엔드포인트
app.get("/health", (req, res) => {
  // 여기에 데이터베이스 연결 등 필요한 체크 추가
  res.status(200).send("OK");
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Node.js 애플리케이션을 위한 Docker Compose 구성

여러 서비스를 함께 실행해야 하는 경우 Docker Compose를 사용합니다.

# docker-compose.yml
version: "3.8"

services:
  app:
    build: .
    image: my-nodejs-app
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DB_HOST=mongodb://mongo:27017/myapp
    depends_on:
      - mongo
    restart: always
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 3s
      retries: 3
      start_period: 5s

  mongo:
    image: mongo:5.0
    ports:
      - "27017:27017"
    volumes:
      - mongo-data:/data/db
    environment:
      - MONGO_INITDB_ROOT_USERNAME=admin
      - MONGO_INITDB_ROOT_PASSWORD=password

volumes:
  mongo-data:

Node.js 컨테이너 성능 최적화

1. 적절한 메모리 제한 설정

Node.js의 메모리 사용량을 제한하고 최적화합니다.

docker run -p 3000:3000 --memory="1g" --memory-swap="1g" my-nodejs-app

Node.js에 명시적으로 메모리 제한을 설정할 수도 있습니다.

docker run -p 3000:3000 -e "NODE_OPTIONS=--max-old-space-size=512" my-nodejs-app

2. 클러스터 모드 활용

Node.js 애플리케이션을 클러스터 모드로 실행하여 CPU 코어를 모두 활용합니다.

// app-cluster.js
const cluster = require("cluster");
const os = require("os");
const numCPUs = os.cpus().length;

if (cluster.isMaster) {
  console.log(`마스터 프로세스 ${process.pid} 실행 중`);

  // 워커 포크
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  cluster.on("exit", (worker, code, signal) => {
    console.log(`워커 ${worker.process.pid} 종료됨`);
    // 워커 재시작
    cluster.fork();
  });
} else {
  // 워커에서 앱 실행
  require("./app.js");
  console.log(`워커 ${process.pid} 시작됨`);
}

Dockerfile에서 클러스터 모드 스크립트를 실행합니다.

CMD ["node", "app-cluster.js"]

또는 PM2를 사용하여 클러스터 모드로 실행할 수 있습니다.

FROM node:18-slim

WORKDIR /usr/src/app

COPY package*.json ./
RUN npm ci --only=production
RUN npm install pm2 -g

COPY . .

EXPOSE 3000

# PM2로 클러스터 모드 실행
CMD ["pm2-runtime", "app.js", "-i", "max"]

3. 컨테이너 로깅 최적화

Docker 컨테이너의 로깅 드라이버를 최적화합니다.

docker run -p 3000:3000 --log-driver=json-file --log-opt max-size=10m --log-opt max-file=3 my-nodejs-app

또는 docker-compose.yml에서 설정:

services:
  app:
    # ... 기타 설정
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

프로덕션 컨테이너 보안 강화

1. 의존성 취약점 검사

빌드 프로세스에 보안 스캔을 통합합니다.

FROM node:18-slim

WORKDIR /usr/src/app

COPY package*.json ./
RUN npm ci --only=production
# 보안 취약점 스캔
RUN npm audit --production

COPY . .

EXPOSE 3000
CMD ["node", "app.js"]

2. 이미지 스캔 통합

CI/CD 파이프라인에 Docker 이미지 보안 스캔을 통합합니다.

# GitHub Actions 예시
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - name: Build Docker image
        run: docker build -t my-nodejs-app .

      - name: Scan image for vulnerabilities
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: "my-nodejs-app"
          format: "table"
          exit-code: "1"
          severity: "CRITICAL,HIGH"

3. 실행 시간(runtime) 보호

권한을 제한하여 컨테이너를 실행합니다.

docker run -p 3000:3000 --security-opt=no-new-privileges --cap-drop=ALL my-nodejs-app

실제 작업 예시: Express.js 애플리케이션 컨테이너화

프로젝트 구조

my-express-app/
├── src/
│   ├── app.js
│   ├── routes/
│   │   └── index.js
│   └── models/
│       └── user.js
├── package.json
├── package-lock.json
├── Dockerfile
├── .dockerignore
└── docker-compose.yml

package.json

{
  "name": "my-express-app",
  "version": "1.0.0",
  "description": "Express.js application example",
  "main": "src/app.js",
  "scripts": {
    "start": "node src/app.js",
    "dev": "nodemon src/app.js"
  },
  "dependencies": {
    "express": "^4.18.2",
    "mongoose": "^6.8.0",
    "cors": "^2.8.5",
    "helmet": "^6.0.1"
  },
  "devDependencies": {
    "nodemon": "^2.0.20"
  }
}

src/app.js

const express = require("express");
const cors = require("cors");
const helmet = require("helmet");
const mongoose = require("mongoose");

const indexRoutes = require("./routes/index");

const app = express();
const PORT = process.env.PORT || 3000;
const MONGODB_URI =
  process.env.MONGODB_URI || "mongodb://localhost:27017/myapp";

// 미들웨어
app.use(helmet());
app.use(cors());
app.use(express.json());

// 라우트
app.use("/", indexRoutes);

// 헬스 체크
app.get("/health", (req, res) => {
  res.status(200).send("OK");
});

// 데이터베이스 연결
mongoose
  .connect(MONGODB_URI, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  })
  .then(() => {
    console.log("MongoDB 연결 성공");
  })
  .catch((err) => {
    console.error("MongoDB 연결 오류:", err);
  });

// 서버 시작
app.listen(PORT, () => {
  console.log(`서버가 http://localhost:${PORT} 에서 실행 중입니다`);
});

Dockerfile (최적화 버전)

# 빌드 스테이지
FROM node:18-slim AS builder

WORKDIR /usr/src/app

COPY package*.json ./
RUN npm ci

COPY . .

# 프로덕션 스테이지
FROM node:18-slim

WORKDIR /usr/src/app

COPY package*.json ./
RUN npm ci --only=production

COPY --from=builder /usr/src/app/src ./src

# 비 루트 사용자 설정
RUN groupadd -r nodejs && useradd -r -g nodejs nodejs
USER nodejs

# 환경 변수 설정
ENV NODE_ENV=production
ENV PORT=3000

EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1

CMD ["node", "src/app.js"]

docker-compose.yml

version: "3.8"

services:
  app:
    build: .
    container_name: express-app
    restart: always
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - PORT=3000
      - MONGODB_URI=mongodb://mongo:27017/myapp
    depends_on:
      - mongo
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 3s
      retries: 3
      start_period: 5s
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

  mongo:
    image: mongo:5.0
    container_name: mongo-db
    restart: always
    ports:
      - "27017:27017"
    volumes:
      - mongo-data:/data/db
    environment:
      - MONGO_INITDB_DATABASE=myapp

volumes:
  mongo-data:

컨테이너화된 Node.js 애플리케이션의 디버깅 및 모니터링

1. 디버그 모드로 컨테이너 실행

docker run -p 3000:3000 -p 9229:9229 my-nodejs-app --inspect=0.0.0.0:9229

2. 컨테이너 로그 확인

# 컨테이너 로그 확인
docker logs my-nodejs-container

# 실시간 로그 스트리밍
docker logs -f my-nodejs-container

3. 컨테이너 내부 실행

docker exec -it my-nodejs-container /bin/sh

결론

Node.js 애플리케이션을 Docker 컨테이너화하는 것은 개발 및 배포 프로세스를 크게 개선할 수 있습니다. 최적화된 Dockerfile 작성, 다단계 빌드 활용, 적절한 베이스 이미지 선택, 보안 강화 등의 전략을 적용하면 효율적이고 안전한 컨테이너를 구축할 수 있습니다.

또한 컨테이너 오케스트레이션 도구(Kubernetes, Docker Swarm 등)와 함께 사용하면 확장성, 자동 복구, 로드 밸런싱 등의 이점을 활용할 수 있습니다. 프로덕션 환경에서는 이러한 도구와 함께 모니터링 및 로깅 솔루션(Prometheus, Grafana, ELK 스택 등)을 통합하여 애플리케이션의 상태를 실시간으로 파악하는 것이 중요합니다.

results matching ""

    No results matching ""