Skip to content

docker 이미지 최적화

ez edited this page Jan 25, 2025 · 3 revisions

문제 상황

열심히 개발을 하고 배포를 하던 중에 원격 서버에서 문제가 발생했습니다.

원격 서버에 패키지를 설치하던 중 디스크 용량 부족으로 인해 패키지 설치가 제대로 이루어지지 않았습니다.

당시 저희 팀은 크레딧을 아끼기 위해 최대한 낮은 스펙의 원격 서버를 사용했는데 디스크의 용량이 10GB밖에 되지 않았습니다.

이러한 환경에서 docker 배포를 위한 여러가지 방법을 시도하다 정리하지 않은 layer들이 너무 쌓인 것이 원인이었습니다.

root@pub-server:/var/lib# du -sh /var/lib/docker/*
22M     /var/lib/docker/buildkit
432K    /var/lib/docker/containers
4.0K    /var/lib/docker/engine-id
7.7M    /var/lib/docker/image
84K     /var/lib/docker/network
6.0G    /var/lib/docker/overlay2
8.0K    /var/lib/docker/plugins
4.0K    /var/lib/docker/runtimes
4.0K    /var/lib/docker/swarm
8.0K    /var/lib/docker/tmp
52K     /var/lib/docker/volumes

서버 디스크 총 용량이 10GB인데 layer들의 용량만 6GB를 차지하고 있는 상황입니다.

다행히 docker에서는 이러한 데이터들을 정리하는 prune이라는 명령어를 제공하기 때문에 쉽게 용량을 확보할 수 있었습니다.

docker system prune --all
root@pub-server:~# du -sh /var/lib/docker/*
22M     /var/lib/docker/buildkit
436K    /var/lib/docker/containers
4.0K    /var/lib/docker/engine-id
7.7M    /var/lib/docker/image
84K     /var/lib/docker/network
2.9G    /var/lib/docker/overlay2
8.0K    /var/lib/docker/plugins
4.0K    /var/lib/docker/runtimes
4.0K    /var/lib/docker/swarm
8.0K    /var/lib/docker/tmp
52K     /var/lib/docker/volumes

사용하지 않은 데이터를 정리한 결과 3.1GB에 해당하는 용량을 확보할 수 있었습니다.

docker 환경 자체가 호스트와 격리된 환경을 제공하기 때문에 상당한 용량을 차지하였고 이는 제한된 원격 서버 환경에서 치명적이라고 생각하였습니다.

특히 이미지가 상당히 많은 용량을 차지하였고 이미지 최적화를 통해 서버 용량을 확보하고자 했습니다.

기존 원격 서버 환경

docker 이미지 볼륨 최적화에 대한 과정을 설명드리기 전에 저희 프로젝트 구조에 대해 먼저 말씀을 드리겠습니다.

저희 프로젝트는 frontend, backend 그리고 websocket, 이렇게 총 세 개의 workspace가 존재하고 workspace들은 모두 node 패키지 매니저로 yarn을 사용하고 있습니다.

중복되는 패키지들이 존재해서 호이스팅을 통해 루트 node_modules에서 공통 패키지들을 공유하는 방식을 사용했습니다.

이 방식을 계속 사용하다가 원격 서버와 로컬 환경이 달라 배포에 실패하는 경우가 종종 발생하여 저희는 docker를 적용하기로 했습니다.

각 workspace들을 docker를 통해 별도의 컨테이너로 띄우면 되는데 이 방식의 문제점은 루트에 존재하는 node_modules가 중복된다는 점입니다.

특히 패키지들의 용량이 굉장히 컸기 때문에 제한된 용량의 디스크를 사용하고 있는 원격 서버 환경에서

이 문제를 해결하기 위해 처음에는 docker volume에 node_modules를 설치하고 각 container에 mount하는 방식을 사용했습니다.

하지만 이 방식은 여러가지 단점이 존재했습니다.

첫 번째, 배포하는 과정에서 오류가 날 가능성이 존재합니다.

docker volume은 container를 실행할 때 mount 하기 때문에 패키지를 설치하는 과정은 container가 실행된 다음 진행되어야 합니다.

만약 패키지 설치 과정에서 오류가 발생한다면 배포 자체가 실패할 수 있습니다.

두 번째, 배포 시간이 늘어납니다.

container가 시작할 때 패키지를 설치한다면 당연히 container 실행 시간이 늘어나게 되고 결국 배포 시간이 늘어나게 됩니다.

물론 docker volume이 캐시 역할도 수행하기 때문에 매번 새로운 패키지를 설치하지 않겠지만 패키지에 변화가 생기면 배포 시간이 늘어나게 됩니다.

정리를 해보면

컨테이너 간 패키지를 공유하지 않으면 안정성이 높아진다는 장점이 있지만 용량을 많이 차지합니다.

컨테이너 간 패키지를 공유하면 용량을 줄일 수 있다는 장점이 있지만 안정성이 떨어집니다.

즉, 저희의 목표는 용량과 안정성 두마리 토끼를 모두 잡는 것입니다.

그래서 저희는 패키지를 가지고 있는 base 이미지를 생성하여 이 이미지를 base로 다른 이미지를 만들기로 결정했습니다.

이 방식을 이해하기 위해서 docker 이미지 layer에 대한 이해가 필요합니다.

docker는 이미지를 만들 때 단일 스냅샷을 만들지 않고 여러 개의 계층으로 나누어 만들기 때문에 중복되는 영역을 하나의 레이어로 만들어 용량을 확보할 수 있습니다.

또한 이미지는 여러 개의 layer로 존재하기 때문에 base 이미지를 가지고 있으면 그 위에 새로운 layer들을 쌓아 나만의 이미지를 만들 수 있습니다.

이 새로운 이미지를 만들 때 base 이미지의 layer는 이미 존재하기 때문에 동일한 layer가 중복되지 않습니다.

저희는 이러한 이미지 layer의 특성을 이용하여 두 마리 토끼를 잡을 수 있었습니다.

해결 방안 1 - 이미지 간 패키지 설치 레이어 공유

먼저 node 이미지를 base로 하여 각 workspace마다 package.json을 복사하여 패키지를 설치한 이미지를 만듭니다.

# node_modules를 가지고 있는 이미지
# 이 이미지를 기반으로 각 workspace 별 이미지를 만들면 
# yarn install 레이어를 공유하게 된다.
FROM node:20-alpine

WORKDIR /app

# 호이스팅을 위해
COPY package.json yarn.lock ./
COPY apps/backend/package.json ./apps/backend/
COPY apps/frontend/package.json ./apps/frontend/
COPY apps/websocket/package.json ./apps/websocket/

# 의존성 설치
RUN yarn install

이 이미지의 이름을 octodocs-modules로 하겠습니다.

그리고 이 이미지를 base로 한 각 서버의 이미지를 만들면 됩니다.

아래는 websocket 이미지를 만들기 위한 Dockerfile입니다.

# 빌드 스테이지
FROM octodocs-modules:latest as builder

# 소스 코드 복사 
COPY . .
COPY ./.env.server /app/apps/websocket/.env

WORKDIR /app/apps/websocket

# 빌드
RUN yarn build

FROM octodocs-modules:latest

WORKDIR /app/apps/websocket

COPY --from=builder /app/apps/websocket/dist ./dist

# 프로덕션 모드로 실행
ENV NODE_ENV=production

EXPOSE 4242

CMD ["yarn", "start:prod"]

backend 이미지도 동일하게 만들면 됩니다.

# 빌드 스테이지
FROM octodocs-modules:latest as builder

# 소스 코드 복사
COPY . .
COPY ./.env.server /app/apps/backend/.env

WORKDIR /app/apps/backend

# 빌드
RUN yarn build

# 프로덕션 스테이지
FROM builder

WORKDIR /app/apps/backend

COPY --from=builder /app/apps/backend/dist ./dist

# 프로덕션 모드로 실행
ENV NODE_ENV=production

EXPOSE 3000

CMD ["yarn", "start:prod"]

이제 이미지를 빌드할 때 패키지를 설치하여 패키지 설치 과정으로 인해 배포가 실패하는 현상을 방지할 수 있었고 패키지가 설치된 layer를 공유하여 용량 문제도 해결할 수 있었습니다.

해결 방안 2 - 필요 없는 yarn cache 삭제

이미지 빌드 환경에서 패키지를 모두 설치하기 때문에 yarn cache는 필요하지 않습니다.

yarn install 패키지 설치 레이어에서 yarn cache를 삭제하여 용량을 확보할 수 있습니다.

여기서 중요한 점은 캐시 삭제를 패키지 설치와 같은 레이어에서 실행해야 한다는 점입니다.

RUN yarn install --check-files && yarn cache clean

만약 아래처럼 분리한다면 패키지 설치 레이어에는 캐시 정보가 그대로 남아있기 때문에 용량 확보에 큰 도움이 되지 않습니다.

RUN yarn install --check-files  
RUN yarn cache clean

결과

패키지 레이어 공유 후 용량 확보

<기존 용량>

root@pub-server:~/octodocs# docker system df
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          6         6         4.558GB   161.7MB (3%)
Containers      6         0         6.595MB   6.595MB (100%)
Local Volumes   2         2         48.23MB   0B (0%)
Build Cache     32        0         29.17MB   29.17MB

<개선 후 용량>

root@pub-server:~/octodocs# docker system df
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          7         6         2.547GB   2.116GB (83%)
Containers      6         0         6.594MB   6.594MB (100%)
Local Volumes   2         2         48.2MB    0B (0%)
Build Cache     36        0         4.564kB   4.564kB

이미지 용량을 4.5GB에서 2.5GB로 줄일 수 있었습니다.

yarn cache 삭제 후 용량 확보

<기존 용량>

root@pub-server:~/octodocs# docker system df
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          7         6         2.547GB   2.116GB (83%)
Containers      6         0         6.594MB   6.594MB (100%)
Local Volumes   2         2         48.2MB    0B (0%)
Build Cache     36        0         4.564kB   4.564kB

<개선 후 용량>

root@pub-server:~/octodocs# docker system df
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          7         6         1.189GB   758.1MB (63%)
Containers      6         2         6.595MB   6.595MB (99%)
Local Volumes   3         2         48.48MB   88B (0%)
Build Cache     35        0         1.095MB   1.095MB

이미지 용량을 2.5GB에서 1.1GB까지 줄일 수 있었습니다.

PR

https://github.com/boostcampwm-2024/refactor-web39-OctoDocs/pull/29

Clone this wiki locally