본문으로 바로가기

Dockerfile 최적화

category Infra 2023. 7. 9. 20:20

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 권한을 가진 프로세스는 네트워크 스택에 영향을 미칠 수 있습니다. 이로 인해 다른 컨테이너 및 호스트에 대한 네트워크 공격이 가능해집니다.

 

 

아래는 위의 내용들을 적용하여 최적화해본 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 최적화에 도움이 되었으면 좋겠습니다. 감사합니다.