LabHub

이미지 빌드 · 멀티스테이지와 경량화 · 이론

이미지를 줄이는 순서

LabHub 에서 이어서 보기

한 줄 요약

멀티스테이지의 핵심은 문법이 아니라 **최종 스테이지로 무엇만 복사할
것인가**입니다. 빌더 스테이지의 레이어는 최종 이미지의 매니페스트에
아예 들어가지 않습니다.

왜 이게 필요했나

1.9GB 짜리 노드 이미지가 있었습니다. 노드가 늘어날 때마다 파드가 Ready
가 되기까지 90초, 롤백 한 번에 또 90초. 추측 대신 docker history
먼저 봅니다.

RUN npm install               901MBCOPY . .                      286MBRUN apt-get update && ...     412MBADD file:07cf5b0f8bd1d5d5…     74.8MB

소스 코드가 286MB 일 리는 없으니 .git 이나 로컬 node_modules
통째로 들어갔다는 뜻입니다. 도구로 한 번 더 재면 낭비가 408MB,
효율이 71.8% 로 나옵니다. **408MB 가 전송되고 저장되지만 컨테이너에서는
보이지도 않습니다.**

어떻게 동작하나

여기서 가장 반직관적인 사실. 아래처럼 정리해도 **한 바이트도 줄지
않습니다.**

0B      RUN rm -rf /var/lib/apt/lists/*2.1MB   RUN apt-get purge -y build-essential && apt-get autoremove -y18.4MB  RUN make -C /src all396MB   RUN apt-get update && apt-get install -y build-essential

purge 레이어는 크기를 줄이기는커녕 2.1MB 를 더했습니다. 삭제 표식과
갱신된 패키지 DB 가 새 레이어에 기록됐기 때문입니다. 396MB 는 그대로입니다.

규칙은 하나입니다. 만든 것은 만든 그 RUN 안에서 지운다. 다만 "그러니
모든 RUN 을 합쳐라"로 이어지면 안 됩니다. 그것은 캐시 재사용을 파괴합니다.
합쳐야 하는 것은 생성과 정리가 짝을 이루는 명령들뿐입니다.

멀티스테이지의 안티패턴도 분명합니다.

FROM node:22 AS builderCOPY . . ; RUN npm install && npm run buildFROM node:22-slimCOPY --from=builder /app /app     # devDeps, 소스, 테스트, 빌드 캐시 전부

베이스만 바꾼 셈이라 절감폭이 거의 없습니다. 산출물만 골라 복사해야
합니다.

현장에서 만나는 모습

베이스를 고를 때 크기만 보면 손해를 봅니다.

| 베이스 | 크기 | libc | 주의점 |
| --- | --- | --- | --- |
| debian:bookworm-slim | 약 75MB | glibc | 무난한 기본값 |
| python:3.12-slim | 약 130MB | glibc | 컴파일러 없음 |
| alpine:3.20 | 약 8MB | musl | 휠 재빌드·DNS·malloc |
| distroless/static | 약 2MB | 없음 | cgo 쓰면 실패 |
| scratch | 0B | 없음 | CA·tzdata 직접 복사 |

알파인이 손해가 되는 대표적인 경우가 파이썬 휠입니다. 같은 pandas 설치가
slim 에서 9.4초인데 alpine 에서는 소스 컴파일로 넘어가 11분 38초
걸립니다. CI 시간이 70배입니다. 그리고 scratch 를 쓸 때 잊기 쉬운
세 가지가 CA 인증서, 타임존 데이터, /etc/passwd 입니다. TLS 검증이
x509: certificate signed by unknown authority 로 실패하면 대개 첫 번째
때문입니다.

마지막으로 방향을 하나 틀어야 합니다. 크기보다 레이어 재사용률입니다.
400MB 이미지에서 실제로 받는 것이 12MB 일 수 있고, 200MB 짜리 "작은"
이미지라도 의존성 레이어가 매 커밋 무효화되면 매번 200MB 를 전송합니다.
성과는 이미지 크기가 아니라 콜드 스타트 풀 시간으로 검증합니다.

다음 실습에서 할 것

빌더 스테이지에서 산출물을 만들고 최종 스테이지에는 그것만 복사해 크기가
얼마나 줄어드는지 재고, scratch 위에 정적 바이너리 하나만 올린 이미지가
정말 동작하는지(그리고 셸이 없다는 것이) 확인합니다.