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 스택 등)을 통합하여 애플리케이션의 상태를 실시간으로 파악하는 것이 중요합니다.