Dockerfile을 최적화한 경험에 대해 말씀드리면서, 제가 어떤 부분들을 고려하였는지 말씀드리려고합니다.
보통 DockerFile을 최적화한다는 말은, Docker Image의 용량을 줄이고 빌드 시간을 줄이겠다는 말입니다.
해당 최적화를 통해 팀원들의 1초를 절약해 더욱 더 비즈니스에 집중하는 시간을 만들겠다는 의도를 가지고, 이미지 최적화 작업을 시작했습니다.
먼저 리팩토링하기 전의 Dockerfile을 보겠습니다.
저는 백엔드 개발자지만, 좀 더 극적인(?) 예시를 위해 Next.js 를 실행시키기 위한 Dockerfile을 가져왔습니다.
FROM node:16.13.1
WORKDIR usr/app
COPY ./package*.json ./
COPY ./ ./
RUN npm install
RUN npm run build
RUN npm prune --production
EXPOSE 3000
CMD [ "npm", "--", "start" ]
굉장히 기본적인 Next js를 실행하기 위한 Dockerfile입니다. 위의 파일에서 최적화할 포인트를 찾아보겠습니다.
FROM node:16.13.1
- 이미지의 종류
- 해당 이미지는 기본 Node
- Docker Image에 사용할 베이스 이미지는 가능한 최대한 경량화된 이미지를 사용하는 것이 좋습니다.
- 보통 alphine 이미지를 많이 사용하고, 보안과 단순성, 경량화에 초점이 맞춰진 경량화 이미지입니다. (공식 설명 첨부)
- https://hub.docker.com/_/alpine
RUN npm install
- 종속성 다운로드
- 프로젝트를 빌드하기 위한 종속성을 다운로드 받기위한 구간입니다.
- 빌드 시간을 늘리는 주요 원인 중 하나입니다. yarn berry의 zero-install을 활용하여 빌드 시간을 단축시킬 수 있습니다.
- 다만 위의 방법은 종속성을 캐싱해두는 것이기때문에 빌드 시간은 감소하지만 도커 이미지 자체는 용량이 증가할 수 있어, 프로젝트의 상황에 따라 선택이 필요한 방법입니다.
COPY ./ ./
- 프로젝트 코드 복사
- 프로젝트 전체 코드를 복사하고 있습니다. 필요하지 않은 코드들도 복사되어, 도커 이미지의 용량을 늘리고 있습니다.
- 필요한 파일만 복사하는 것으로 이미지를 경량화할 수 있습니다.
위의 내용들 이외에도, 적용해볼만한 최적화 기법들이 있습니다.
- 멀티 스테이지 빌드 사용
- 빌드 스테이지와 실행 스테이지를 분리하여 불필요한 종속성을 이미지에 넣지 않을 수 있습니다.
- 캐시 활용
- Docker는 빌드 과정에서 각 단계를 캐시하므로, 자주 변경되는 단계는 가능하면 Dockerfile의 아래쪽에 위치하는 것이 좋습니다.
- 따라서 종속성을 설치하는 단계는 코드를 복사하는 단계보다 아래에 있는 것이 좋습니다.
- 보안성 강화
- Docker 는 Host 리소스를 공유합니다. 따라서, 컨테이너 내에서 root로 실행되는 프로세스는 호스트 시스템에도 영향을 미칠 수 있습니다.
- 컨테이너 내에서 실행되는 악성 프로세스가 root 권한을 획득하면 호스트 시스템에 영향을 미칠 수 있습니다. 이를 통해 시스템 전체에 대한 권한을 확보하거나 다른 컨테이너로의 침투가 가능해집니다.
- root 권한을 가진 프로세스는 호스트의 파일 시스템에 접근하고 수정할 수 있습니다. 이로 인해 중요한 시스템 파일이 변경되거나 삭제될 수 있습니다.
- root 권한을 가진 프로세스는 네트워크 스택에 영향을 미칠 수 있습니다. 이로 인해 다른 컨테이너 및 호스트에 대한 네트워크 공격이 가능해집니다.
- Docker 는 Host 리소스를 공유합니다. 따라서, 컨테이너 내에서 root로 실행되는 프로세스는 호스트 시스템에도 영향을 미칠 수 있습니다.
아래는 위의 내용들을 적용하여 최적화해본 Dockerfile입니다.
FROM node:lts-alpine3.14 as builder
WORKDIR /app
COPY ./package*.json /.yarnrc ./.yarnrc.yml ./.pnp.cjs ./.pnp.loader.mjs ./yarn.lock ./
COPY ./.yarn ./.yarn
RUN yarn install
COPY ./tsconfig.json ./.prettierrc ./.env.* ./next-env.d.ts ./.eslintrc.json ./next.config.js ./
COPY ./src ./src
ARG NEXT_PROFILES_ACTIVE
RUN yarn run build:$NEXT_PROFILES_ACTIVE
FROM node:lts-alpine3.14 as runner
ARG APP=/app
ENV APP_USER=runner
RUN addgroup -S $APP_USER \
&& adduser -S $APP_USER -G $APP_USER \
&& mkdir -p ${APP}
WORKDIR /app
COPY --from=builder /app/next.config.js /app/package.json /app/.pnp.cjs /app/.pnp.loader.mjs /app/yarn.lock /app/.yarnrc.yml ./
COPY --from=builder --chown=$APP_USER:$APP_USER /app/.next ./.next
COPY --from=builder /app/.yarn ./.yarn
EXPOSE 3000
USER $APP_USER
CMD ["yarn", "start"]
이 Dockerfile에도 추가적으로 최적화할 포인트들이 더 있습니다.
위의 파일처럼 COPY 레이어가 많아지면, 그만큼 Dockerfile이 무거워지기도하고, 보통 그럴 일은 없지만, Dockerfile이 생성할 수 있는 최대 레이어는 125개이므로, 그것을 초과하는 경우도 발생할 수 있습니다. 따라서 필요하지않은 파일들을 .dockerignore 파일에 지정하고 하나의 레이어로 코드를 복사해서 최적화를 진행할 수 있습니다.
저는 위와 같은 최적화 과정을 통해
[AS-IS]
138.5s / 548.92MB
[TO-BE]
107.8s / 278.48MB
로 최적화할 수 있었습니다.
다른 분들의 Dockerfile 최적화에 도움이 되었으면 좋겠습니다. 감사합니다.