2024년 7월 24일 작성한 글입니다.
2024년 2월 경 도커 파일 크기가 생각보다 많이 큰 것에 대해 의구심을 가지게 되었고 이를 해결한 일지입니다.
1. 도커 파일은 원래 이렇게 큰가
2024년 2월 초, Github actions 배포 자동화 라인을 통해 개발 서버에 배포를 하던 중 오류가 발생했습니다. 원인은 서버에 EBS 볼륨 용량이 부족하다는 것이였습니다. EC2 생성 시 자동 할당되는 EBS 볼륨의 크기는 8GB이며, 도커 파일을 저장하는 것 외에 디스크 사용을 하지 않는 서버다보니 용량 부족은 제겐 좀 의문이 드는 상황이였습니다.
서버에 들어가서 용량을 확인해보니 정말 디스크 용량 사용률이 100% 였습니다. 아니 이게 무슨 황당한 경우인가. 리눅스에 깔려있는 것이라곤 도커와 도커 이미지 파일뿐들인데.. 그래도 일단 깔려있는 유일한 리소스인 도커 이미지 파일을 확인해보자 라는 생각이 들었고 조회 결과, 도커 이미지의 크기는 무려 1.8G 였습니다.
당시 도커에 대한 지식이 높지 않은 편이다보니, 원래 도커 파일의 크기가 이런건가(큰건가).. 아니 우리 서버 규모가 타 서버들에 비해 큰 것도 아닌데 왜 이렇게 크지라는 별 많은 생각이 들었고 이를 해결할 수 있는 방법을 생각해보았습니다.
3. 방법 찾기
당장 떠오른 생각은 “EBS 볼륨의 크기를 키운다” 였습니다. 이 방법은 가장 간단하지만 비용이 발생합니다. 그리고 뭔가 도커 파일 크기가 너무 큰 게 위화감이 들어서 다른 방법을 모색해보자 생각이 들었습니다.
일단 뭔가 마음에 안든다 싶으면 ‘키워드 + 최적화’ 를 검색하면 된다는 경험에 따라 ‘도커 빌드 최적화’ 를 검색했고, 제가 원했던 내용을 찾을 수 있었습니다.
그것은 바로 ‘Multi-Stage Build’
4. 원인과 해결
결과부터 말하자면, 도커 이미지의 크기가 컸던 이유는 두 가지였습니다.
- 첫번째는, 경량화 도커 이미지가 따로 있는데 일반 이미지로 빌드했기 때문이였고
- 두번째는, 도커 이미지의 크기가 컷던 이유는 빌드 파일을 제외한 소스 파일이 모두 함께 도커 이미지로 빌드되었기 때문이였습니다.
개선 전 Dockerfile 소스코드의 일부입니다.
(아래 코드를 포함하여 이후에 공개되는 코드에서 민감한 정보는 모두 제거하고 포스팅합니다.)
FROM node:18.13.0
# STEP 1
RUN mkdir -p /app
WORKDIR /app
ADD . /app/
# STEP 2
RUN rm yarn.lock || true
RUN rm package-lock.json || true
RUN yarn && yarn prisma generate && yarn build
# STEP 3
ENV HOST 0.0.0.0
EXPOSE 3000
# STEP 4
ARG BUILD_ENV
RUN echo "Build environment: ${BUILD_ENV}"
ENV RUNTIME_BUILD_ENV=${BUILD_ENV}
# STEP 5
CMD yarn start:${RUNTIME_BUILD_ENV}
정말 단순합니다. 정말 프로젝트 파일을 도커로 빌드하는 게 끝인 기초적인 빌드 방식이였습니다.
개선 후 Dockerfile 소스코드입니다.
# Multi Stage Build
# STEP 1. generate builder image
FROM node:18-alpine AS builder
RUN apk --no-cache add --virtual builds-deps build-base python3
RUN mkdir -p /app/floom-client-server/
WORKDIR /app/floom-client-server/
COPY . .
RUN rm package-lock.json || true
RUN yarn install --silent && yarn prisma generate
ARG BUILD_ENV
ENV RUNTIME_BUILD_ENV=${BUILD_ENV}
#build builder image
RUN yarn build:${RUNTIME_BUILD_ENV}
# STEP 2. generate running image
FROM node:18-alpine
RUN mkdir -p /app
WORKDIR /app
ARG BUILD_ENV
ENV RUNTIME_BUILD_ENV=${BUILD_ENV}
COPY --from=builder /app/floom-client-server/dist ./dist
COPY --from=builder /app/floom-client-server/node_modules ./node_modules
COPY --from=builder /app/floom-client-server/.env.${RUNTIME_BUILD_ENV} ./.env
ENV HOST 0.0.0.0
EXPOSE 3000
CMD [ "node", "./dist/src/main.js" ]
뭔가 전체적인 분량도 많아졌습니다. 하나하나 살펴보겠습니다.
- 우선 가장 먼저 빌드를 돌리는 이미지가 변경되었습니다.
- 기존
node:18.13.0
에서node:18-alpine AS builder
로 변경되었습니다. alpine 이미지는 빌드를 위한 경량화 이미지입니다. 말그대로 node 를 돌리는 깡통 이미지인 것입니다. 그래서RUN apk --no-cache add --virtual builds-deps build-base python3
명령어를 통해 node 빌드에 필요한 의존성 패키지들을 설치해주었습니다. - 다음으로, 빌드할 때 Multi Stage Build 기법을 사용했습니다. Multi Stage Build 기법은 Dockerfile 단에서 빌드할 때 dist 폴더를 생성하는 빌드 이미지를 만들고 이 이미지에서 dist 폴더만 따로 뽑아 실행용 이미지를 만드는 기법입니다. (말그대로, 2단계로 나눠 진행하는 빌드 기법입니다.)
‘#1. generate builder image’ 단계에서 소스 파일 폴더를 빌드하여 dist 폴더를 만듭니다. 여기서 만들어지는 이미지는 정말 dist 폴더를 뽑아내기 위한 이미지입니다.
‘#2. generate running image’ 단계에서 ‘#1. generate builder image’ 단계에 만들어낸 이미지의 dist, node_modules, .env 를 뽑아내 이 파일들로만 이미지를 다시 빌드합니다.
```
#빌드 이미지에서 파일 뽑아내기
COPY --from=builder /app/floom-client-server/dist ./dist
COPY --from=builder /app/floom-client-server/node_modules ./node_modules
COPY --from=builder /app/floom-client-server/.env.${RUNTIME_BUILD_ENV} ./.env
```
이 이미지가 실제 서버에 저장되고 돌아가는 실행용(서비스용) 이미지입니다.
5. 개선 결과
개선 결과는 만족스러웠습니다. 무려 1.8G 이상이였던 이미지의 크기가 523MB로 약 4분의 1 크기로 축소되었습니다.