icon

Docker를 이용한 컨테이너화


 컨테이너화는 애플리케이션과 그 의존성을 하나의 독립적인 유닛으로 패키징하는 기술입니다.

 Docker를 사용한 NestJS 애플리케이션의 컨테이너화는 일관된 개발 및 배포 환경을 제공하고, 확장성과 이식성을 향상시킵니다.

컨테이너화의 이점

  1. 환경 일관성 : 개발, 테스트, 프로덕션 환경의 차이 최소화
  2. 빠른 배포 : 컨테이너 이미지를 통한 신속한 애플리케이션 배포
  3. 격리 : 애플리케이션 간 리소스와 의존성 충돌 방지
  4. 확장성 : 손쉬운 수평적 확장 가능
  5. 리소스 효율성 : 가상 머신에 비해 적은 리소스 사용

NestJS 애플리케이션 Dockerfile 작성

 기본 Dockerfile

FROM node:14
 
WORKDIR /usr/src/app
 
COPY package*.json ./
 
RUN npm install
 
COPY . .
 
RUN npm run build
 
EXPOSE 3000
 
CMD ["npm", "run", "start:prod"]

 최적화된 Dockerfile (멀티 스테이지 빌드)

# 빌드 스테이지
FROM node:14 AS build
 
WORKDIR /usr/src/app
 
COPY package*.json ./
 
RUN npm install
 
COPY . .
 
RUN npm run build
 
# 프로덕션 스테이지
FROM node:14-alpine
 
WORKDIR /usr/src/app
 
COPY --from=build /usr/src/app/dist ./dist
COPY --from=build /usr/src/app/node_modules ./node_modules
 
EXPOSE 3000
 
CMD ["node", "dist/main"]

 이 멀티 스테이지 빌드 방식은 최종 이미지 크기를 크게 줄이고 보안을 향상시킵니다.

Docker Compose를 사용한 서비스 구성

 docker-compose.yml 예시

version: '3'
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgres://user:pass@db:5432/dbname
    depends_on:
      - db
  db:
    image: postgres:13
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass
      - POSTGRES_DB=dbname
  redis:
    image: redis:6

 이 설정으로 NestJS 애플리케이션, PostgreSQL 데이터베이스, Redis 캐시를 함께 실행할 수 있습니다.

환경 변수 관리

  1. .env 파일 사용
DATABASE_URL=postgres://user:pass@localhost:5432/dbname
REDIS_URL=redis://localhost:6379
  1. Docker 실행 시 환경 변수 주입
docker run -e NODE_ENV=production -e DATABASE_URL=postgres://user:pass@db:5432/dbname my-nestjs-app
  1. NestJS ConfigModule을 사용한 환경 변수 관리
import { ConfigModule } from '@nestjs/config';
 
@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: `.env.${process.env.NODE_ENV}`,
    }),
  ],
})
export class AppModule {}

환경별 Docker 이미지 관리

 태그 지정 전략

  • 개발 : myapp:dev-<commit-hash>
  • 테스트 : myapp:test-<version>
  • 프로덕션 : myapp:<version>

 예시 빌드 및 푸시 스크립트

#!/bin/bash
VERSION=$(node -p "require('./package.json').version")
ENV=$1
 
docker build -t myapp:$ENV-$VERSION .
docker push myapp:$ENV-$VERSION

Docker 네트워크 구성

 마이크로서비스 간 통신을 위한 네트워크 설정

version: '3'
networks:
  myapp-network:
    driver: bridge
 
services:
  service1:
    build: ./service1
    networks:
      - myapp-network
  service2:
    build: ./service2
    networks:
      - myapp-network

로그 관리와 모니터링

  1. Docker 로그 드라이버 사용
services:
  app:
    logging:
      driver: "json-file"
      options:
        max-size: "200k"
        max-file: "10"
  1. ELK 스택 통합
services:
  app:
    logging:
      driver: "fluentd"
      options:
        fluentd-address: localhost:24224
  fluentd:
    image: fluent/fluentd
    volumes:
      - ./fluentd/conf:/fluentd/etc
    ports:
      - "24224:24224"
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:7.9.2
  kibana:
    image: docker.elastic.co/kibana/kibana:7.9.2

Best Practices 및 주의사항

  1. 경량 베이스 이미지 사용 : Alpine 리눅스 기반 이미지 선호
  2. 멀티 스테이지 빌드 활용 : 빌드 아티팩트만 최종 이미지에 포함
  3. 불필요한 파일 제외 : .dockerignore 파일 사용
  4. 레이어 최소화 : RUN 명령어 결합
  5. 비-루트 사용자로 실행 : 보안 강화
  6. 헬스 체크 구현 : HEALTHCHECK 명령 사용
  7. 시크릿 관리 : 환경 변수 또는 Docker secrets 사용
  8. 이미지 스캐닝 : 취약점 검사 도구 사용 (예 : Clair, Trivy)
  9. 캐시 활용 : 빌드 시간 단축을 위한 레이어 캐싱
  10. 문서화 : Dockerfile과 관련 스크립트에 주석 추가

 Docker를 이용해 컨테이너화하는 것은 현대적인 애플리케이션 개발 및 배포 과정에서 중요한 부분입니다.

 컨테이너화를 통해 개발 환경과 운영 환경의 일관성을 유지하고, 배포 프로세스를 간소화하며, 애플리케이션의 확장성을 높일 수 있습니다.

 멀티 스테이지 빌드를 사용하면 최종 이미지의 크기를 줄이고 보안을 강화할 수 있습니다.

 이는 특히 NestJS와 같은 TypeScript 기반 애플리케이션에서 중요한데, 빌드 과정에서 생성되는 불필요한 파일들을 제거할 수 있기 때문입니다.

 Docker Compose를 활용하면 NestJS 애플리케이션과 관련 서비스(데이터베이스, 캐시 등)를 쉽게 구성하고 관리할 수 있습니다.

 이는 개발 환경 설정을 단순화하고, 마이크로서비스 아키텍처를 쉽게 구현할 수 있게 해줍니다.

 환경 변수 관리는 컨테이너화된 애플리케이션에서 중요한 부분입니다.

 NestJS의 ConfigModule과 Docker의 환경 변수 주입 기능을 결합하여 유연하고 안전한 설정 관리가 가능합니다.

 특히 다양한 환경(개발, 테스트, 프로덕션)에 따라 설정을 쉽게 변경할 수 있어 유용합니다.

 Docker 이미지 관리 전략은 애플리케이션의 버전 관리와 밀접하게 연관됩니다.

 적절한 태그 지정 전략을 사용하면 다양한 환경에서의 이미지 버전을 쉽게 추적하고 관리할 수 있습니다.

 Docker 네트워크를 활용한 마이크로서비스 간 통신 설정은 복잡한 시스템 아키텍처를 효과적으로 구현하는 데 도움이 됩니다.

 이를 통해 서비스 간의 격리와 안전한 통신을 보장할 수 있습니다.