이번 글에서는 프로젝트 배포 과정에서 도커파일을 최적화 한 과정에 대해서 다뤄볼까합니다.
제가 글에서 다루는 백엔드는 TypeScript로 작성된 Node Express 프로젝트입니다.
0. 본격적으로 설명하기 이전에
우선 개발하고, 해당 프로젝트를 런타임으로 올리는 과정에 대해서 설명해볼까합니다.
해당 프로젝트는 타입스크립트로 작성되어있기 때문에 해당 프로젝트를 실행하기 위해서는 아래의 과정들이 필요합니다.
- pm2를 설치한다
- 필요한 파일들을 모두 컨테이너에 적재한다
- npm 의존성을 모두 설치한다
- 프로젝트를 tsc를 이용해서 빌드한다
- 필요한 포트를 개방하고 pm2-runtime을 이용해서 빌드된 js 파일을 실행한다
따라서 해당 과정을 이용해 Dockerfile을 작성하면 아래와 같은 파일을 만들수 있습니다.
FROM node:18-alpine
WORKDIR /opt/app
COPY package*.json ./
RUN npm install && npm install -g pm2
COPY . .
EXPOSE 3500
RUN npm run build
CMD ["pm2-runtime", "dist/bin/index.js"]
해당 Dockerfile을 이용해서 Jenkins에서 빌드를 실행하는 경우, Docker build 스테이지에서 46초만큼 걸리며, 이미지의 크기는 423MB로 이미지가 생성되었습니다.
1. 용량 최적화
위 과정을 이용해서 Docker 이미지를 생성하면 실행이 되긴하지만, Docker image 내부에 불필요한 이미지까지 같이 적재되기 때문에 이는 비용 문제로 귀결됩니다. 따라서 Docker image의 용량을 줄일 수 있다면 최대한 줄여주는게 좋습니다.
그러면, 정확하게 저희가 해당 백엔드 서버를 돌리기 위해서 필요한 최소한의 데이터가 무엇일까요? 제가 위에서 제시한 프로젝트 기준으로는 .env 파일과, Data 폴더, dist 폴더, 그리고 package.json, package-lock.json 입니다.
따라서 해당 파일만을 추출해서 런타임으로 올리면 되기 때문에, Multi-Stage 방법으로 Dockerfile을 작성하겠습니다. 스테이지는 총 2개로, 첫번째 스테이지는 빌드 스테이지, 두번째 스테이지는 프로덕션 스테이지로 작성하겠습니다.
우선 2개의 스테이지를 이용해서 작성한 최적화된 도커파일을 소개하겠습니다.
# Stage 1
FROM node:18-alpine as BUILDER
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Stage 2
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
COPY .env Data ./
COPY --from=BUILDER /app/dist ./dist
RUN npm install --production && npm install -g pm2
EXPOSE 3500
CMD ["pm2-runtime", "dist/bin/index.js"]
해당 파일을 이용해서 프로젝트를 빌드하는 경우, devDependencies를 제외하고 npm 의존성을 설치하기 때문에 빌드 용량이 줄어드는 효과를 보일겁니다. 예상대로 용량은 423MB에서 382MB 까지 줄어드는 효과를 보였습니다.
실제로 docker image history <image-id> 명령어를 이용해서 둘을 까보면 어디서 줄어들었는지 한 눈에 확인이 가능합니다.
# 기존
<missing> 4 minutes ago RUN /bin/sh -c npm install -g pm2 && npm ins… 246MB buildkit.dockerfile.v0
# 개선된 이미지
<missing> 5 minutes ago RUN /bin/sh -c npm install --production && n… 206MB buildkit.dockerfile.v0
그러나, 도커 빌드 과정이 길어진만큼 도커 빌드타임이 길어졌습니다. 기존의 방식은 젠킨스 환경에서 평균적으로 46s가 걸린 반면, 해당 방식으로 빌드 시 1분30초 정도가 걸렸습니다.
2. 빌드타임 최적화
이제 빌드타임을 최적화하겠습니다. 해당 이미지에서 빌드타임을 최적화 할 수 있는 부분은 단 한부분, npm 의존성을 설치하는 부분에서 개선이 가능합니다.
npm install --production 을 이용해서 설치하게 되면 package.json을 읽어내서 모든 의존성을 최신 버전으로 설치해옵니다.
그러나, 모든 의존성이 최신 버전일 필요가 없는 경우에는 npm ci --omit=dev 를 이용해서 설치하면 package.json 이 아닌 package-lock.json 을 이용해서 설치하기 때문에 의존성 설치에 걸리는 시간이 줄어든다고 합니다.
해당 방식을 이용한 Dockerfile을 보여드리겠습니다.
# Stage 1
FROM node:18-alpine as BUILDER
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Stage 2
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
COPY .env Data ./
COPY --from=BUILDER /app/dist ./dist
RUN npm ci --omit=dev && npm install -g pm2
EXPOSE 3500
CMD ["pm2-runtime", "dist/bin/index.js"]
해당 Dockerfile을 이용해서 빌드하는 경우 젠킨스 환경에서 1분 30초에서 1분 16초 정도로 대략 14초 정도의 빌드 타임이 개선되는 효과를 보였습니다.
3. Reference
Multi stage builds - Docker docs