Docker 이미지 사이즈 최적화 기록

1.44GB였던 이미지를 322MB까지 줄이며 확인한 것들

Docker 이미지 사이즈 최적화 기록

문득 내가 이용하는 Docker 이미지들의 사이즈가 생각보다 작다는 것을 알게 되었다. 내가 빌드한 이미지는 GB 단위라서 당연히 그 정도가 정상인 줄 알았는데, 자주 사용하던 filebrowser/filebrowser30.6MB밖에 되지 않았다.

그때부터 내가 만든 이미지는 왜 이렇게 큰지 궁금해졌다. 이번 글은 Docker 이미지 최적화의 정답을 정리한 글이라기보다, sd-prompt-palette 프로젝트의 이미지 사이즈를 줄여보며 확인한 기록에 가깝다.

https://github.com/baealex/sd-prompt-palette

docker build -t sdpp-test .

이 프로젝트의 이미지는 당시 1.44GB 정도에 육박했다 💦


이미지 분석

오픈소스로 만들어진 dive라는 CLI 도구가 있는데 이를 활용하면 Dockerfile에서 각 레이어가 어느 정도의 용량을 차지하는지, 이미지 내부에서 어디에 위치하고 있는지 파악할 수 있다. 대략 아래와 같은 비주얼이다.

dive를 이용해서 사이즈를 파악해보자.

dive sdpp-test

WORKDIR /app

위 라인을 기점으로 위쪽은 베이스 이미지가 차지하는 용량, 아래쪽은 내가 추가한 용량으로 볼 수 있다.


이미지 최적화

1. 작은 베이스 이미지 사용

우선 베이스 이미지가 차지하는 용량이 1GB 정도로 비교적 크다는 것을 알 수 있었다. 이 프로젝트에서는 당시 node:21 이미지를 사용하고 있었다. 기본 이미지는 Debian 계열을 바탕으로 빌드되어 있는데, 더 작은 용량을 목표로 한 이미지들도 제공된다.

  • node:21 : 기본 Debian 기반 이미지다.

  • node:21-slim : 더 경량화된 Debian 기반 이미지다.

  • node:21-alpine : 초경량 리눅스 배포판 Alpine Linux 기반 이미지다.

여기서는 alpine으로 이미지를 바꿨고, 크기는 474MB로 줄어들었다.

다만 Alpine이 언제나 정답이라는 뜻은 아니다. 네이티브 의존성이 있거나 특정 시스템 라이브러리에 기대는 프로젝트라면 오히려 문제가 생길 수도 있다. 이 프로젝트에서는 필요한 의존성이 단순했고, 이미지 크기를 줄이는 목적에 잘 맞았기 때문에 Alpine을 선택했다.

베이스 이미지가 140MB 정도만 차지하게 되었다.

# as-is
FROM node:21

# to-be
FROM node:21-alpine
2. dockerignore

이미지에는 꼭 필요한 파일만 담아야 한다. .gitignore와 유사한 신택스를 사용하는 .dockerignore 파일을 추가해서 빌드나 실행에 필요하지 않은 파일을 명시해 주는 것이 좋다.

# .md 확장자인 파일을 모두 무시 ex) README.md
*.md

# 빌드 및 실행을 위해 사용하는 특정 .md 파일은 포함 (아래는 예시)
!*.spec.md

# 디렉토리의 하위 디렉토리나 파일을 무시
.git/
logs/
node_modules/
3. 멀티 스테이지 빌드

이 과정도 불필요한 파일을 이미지에 포함하지 않는 방법에 해당한다.

현재 최적화를 진행하는 프로젝트에서는 웹 서버와 클라이언트가 모두 포함되어 있다. 이미지 내에서 클라이언트를 빌드하고 있으므로 이미지에는 서버 실행을 위한 패키지와 클라이언트 빌드를 위한 패키지를 모두 담고 있는 형태를 취하고 있다.

하지만 클라이언트의 경우에는 빌드된 파일만 제공되면 되므로 클라이언트 패키지와 빌드를 위해서 사용하는 소스 코드가 이미지에 포함되는 것은 불필요하다고 볼 수 있다.

server
   ├──node_modules # 필요
   ├──src # 필요
   └──client
         ├──node_modules # 불필요
         ├──src # 불필요
         └──dist # 필요

Dockerfile에서 빌드 stage를 분리하고 각 stage에서 필요한 파일만 가져올 수 있도록 할 수 있다.

# client라는 이름으로 stage 설정
FROM node:21-alpine as client

WORKDIR /app

COPY ./src/client/package.json ./
COPY ./src/client/pnpm-lock.yaml ./

RUN npx pnpm i

COPY ./src/client/ ./

RUN npm run build

...

FROM node:21-alpine

WORKDIR /app

# client stage에서 파일 복사
COPY --from=client /app/dist ./client/dist

위와 같이 client stage에서 빌드한 결과물만 실제 이미지에 포함시킬 수 있다. 최종적으로 322MB로 사이즈를 줄일 수 있게 되었다.

결국 이미지 사이즈 최적화는 무작정 작은 이미지를 고르는 일이 아니라, 런타임에 필요하지 않은 것들을 걷어내는 일에 가까웠다. 빌드에만 필요한 파일, 개발 중에만 필요한 의존성, 실수로 함께 복사된 디렉토리를 하나씩 덜어내면 이미지의 성격이 훨씬 분명해진다.

작은 이미지는 배포와 다운로드 측면에서 분명 장점이 있다. 다만 숫자를 줄이는 것 자체가 목적이 되면 곤란하다. 중요한 것은 이 이미지가 실행될 때 정말 무엇이 필요한지 확인하고, 그 외의 것들을 이미지 밖으로 밀어내는 일이라고 생각한다.