Cutting It Down With Multi-Stage Builds
한국어 원문으로 표시합니다.
이 실습은 진짜 VM 에서 돕니다
이 상자는 파드가 아니라 KubeVirt 가 띄운 가상머신입니다. 리눅스 커널이
따로 돌고 systemd 가 실제로 서비스를 관리하며, docker 는 흉내가 아니라
진짜 도커 엔진입니다. docker run 으로 띄운 컨테이너는 실제로 프로세스가
되고 docker exec 도 docker logs 도 그대로 동작합니다.
예전에는 이 실습이 파드 안에서 돌았습니다. 커널 권한을 전부 내려놓은 상자라 컨테이너를 띄우는 단계가 막혀 있었고, 그래서 이미지 아카이브를 직접 풀어 보는 우회로 배웠습니다. 이제 우회가 필요 없습니다.
알아 둘 것이 둘 있습니다.
- 처음 뜨는 데 1분 남짓 걸립니다. VM 이 부팅하고 도커를 설치하기 때문입니다. 파드 실습(보통 40초)보다 느립니다.
- 브라우저 미리보기가 없습니다. VM 으로 들어오는 연결은 채점 포트
하나만 열려 있습니다. 웹 서버를 띄웠다면 VM 안에서
curl로 확인하세요.
목표
빌더 스테이지에서 만든 산출물만 최종 이미지로 옮겨, 같은 결과물을 훨씬 작은 이미지로 만드는 과정을 크기 숫자로 확인합니다.
왜 중요한가
이미지 크기는 미관의 문제가 아니라 배포 속도와 롤백 속도의 문제입니다. 노드가 스케일아웃될 때마다, 롤백할 때마다 그 크기를 다시 받습니다. 그리고 크기를 줄이는 가장 큰 지렛대는 압축이나 정리가 아니라 "최종 이미지에 들어가면 안 되는 것을 애초에 그 스테이지에서 만들지 않는 것"입니다. 레이어는 되감기지 않기 때문에, 이 원칙을 어기면 나중에 아무리 지워도 크기가 돌아오지 않습니다.
단계
/root/build3을 만들고/root/build3/Dockerfile에 스테이지를 둘 만듭니다. 첫 스테이지는python:3.12-alpine을 쓰고 이름은builder이며,/out/app.txt에built-in-builder를 써 넣습니다. 두 번째 스테이지는alpine:3.20입니다.labhub/ms:v1로 빌드합니다.- 최종 스테이지에서 빌더의
/out/app.txt만/app/app.txt로 복사하도록 고치고labhub/ms:v2로 빌드합니다. 실행해서 파일을 읽으면built-in-builder가 나와야 합니다. /root/build3/fat.Dockerfile에 단일 스테이지로 같은 결과물을 만드는 버전(베이스는python:3.12-alpine)을 작성하고labhub/ms:fat으로 빌드합니다.- 첫 스테이지만 빌드해
labhub/ms:builder이미지를 만듭니다. 그 안에는/out/app.txt가 있어야 합니다. Dockerfile최종 스테이지에busybox:1.36이미지에서/bin/busybox를/app/busybox로 직접 복사하는 줄을 추가하고labhub/ms:v3로 빌드합니다.labhub/ms:v3를 실행해python3가 없는지 확인합니다. 이미지 크기는 60MB 미만이어야 합니다./root/build3/scratch.Dockerfile을 만들어FROM scratch위에busybox:1.36의/bin/busybox만 올리고, 인자로 받은 문자열을 그대로 출력하도록ENTRYPOINT를 배열 형식으로 지정합니다.labhub/ms:scratch로 빌드하고,scratch-works를 인자로 주면 그 문자열이 출력되어야 합니다./root/build3/size.md에 아래 네 줄을 실제 값으로 적습니다.
fat_mb=<labhub/ms:fat 크기(MB)>
slim_mb=<labhub/ms:v3 크기(MB)>
scratch_mb=<labhub/ms:scratch 크기(MB)>
reduction_pct=<(fat_mb - scratch_mb) / fat_mb * 100 의 정수부>
참고
docker build --target builder -t labhub/ms:builder /root/build3로 중간 스테이지만 빌드합니다.COPY --from=busybox:1.36 /bin/busybox /app/busybox처럼 스테이지가 아닌 이미지도 참조할 수 있습니다.- busybox 는 정적으로 링크되어 있어 scratch 위에서도 동작합니다.
busybox echo <문자열>형태로 실행됩니다. - 흔한 실수 1: 2단계에서 스테이지 디렉터리를 통째로 복사하면 크기가 거의 줄지 않습니다.
- 흔한 실수 2: 7단계에서 ENTRYPOINT 를 셸 형식으로 쓰면 scratch 에 셸이 없어 즉시 실패합니다.
스테이지 두 개 만들기
/root/build3 을 만들고 /root/build3/Dockerfile 에 스테이지를 둘 만듭니다. 첫 스테이지는 python:3.12-alpine 을 쓰고 이름은 builder 이며, /out/app.txt 에 built-in-builder 를 써 넣습니다. 두 번째 스테이지는 alpine:3.20 입니다. labhub/ms:v1 로 빌드합니다.
FROM 을 두 번 쓰고 앞 스테이지에 이름을 붙입니다. 이름은 나중에 COPY 에서 참조합니다.
산출물만 가져오기
최종 스테이지에서 빌더의 /out/app.txt 만 /app/app.txt 로 복사하도록 고치고 labhub/ms:v2 로 빌드합니다. 실행해서 파일을 읽으면 built-in-builder 가 나와야 합니다.
빌더 스테이지에서 만든 파일만 골라 복사합니다. 스테이지 전체를 복사하면 멀티스테이지를 쓴 의미가 없습니다.
단일 스테이지와 크기 비교
/root/build3/fat.Dockerfile 에 단일 스테이지로 같은 결과물을 만드는 버전(베이스는 python:3.12-alpine)을 작성하고 labhub/ms:fat 으로 빌드합니다.
같은 결과물을 무거운 베이스 위에 그대로 남긴 이미지와 비교하세요. 30% 이상 줄어야 합니다.
중간 스테이지만 빌드하기
첫 스테이지만 빌드해 labhub/ms:builder 이미지를 만듭니다. 그 안에는 /out/app.txt 가 있어야 합니다.
디버깅할 때 빌더 스테이지만 따로 이미지로 뽑을 수 있습니다. 최종 스테이지까지 가지 않고 멈추는 옵션이 있습니다.
다른 이미지에서 직접 복사하기
Dockerfile 최종 스테이지에 busybox:1.36 이미지에서 /bin/busybox 를 /app/busybox 로 직접 복사하는 줄을 추가하고 labhub/ms:v3 로 빌드합니다.
--from 은 스테이지 이름만 받는 것이 아닙니다. 이미지 참조를 그대로 줄 수도 있습니다.
빌드 도구 남기지 않기
labhub/ms:v3 를 실행해 python3 가 없는지 확인합니다. 이미지 크기는 60MB 미만이어야 합니다.
최종 스테이지에서 빌드에만 쓰인 런타임이 사라졌는지 실제로 실행해서 확인하세요.
빈 베이스 위에 올리기
/root/build3/scratch.Dockerfile 을 만들어 FROM scratch 위에 busybox:1.36 의 /bin/busybox 만 올리고, 인자로 받은 문자열을 그대로 출력하도록 ENTRYPOINT 를 배열 형식으로 지정합니다. labhub/ms:scratch 로 빌드하고, scratch-works 를 인자로 주면 그 문자열이 출력되어야 합니다.
scratch 에는 셸이 없으므로 ENTRYPOINT 는 반드시 배열 형식이어야 합니다. 정적으로 링크된 바이너리를 골라야 합니다.
크기 보고서
/root/build3/size.md 에 아래 네 줄을 실제 값으로 적습니다.
세 이미지의 실제 크기를 재서 적고 감소율을 계산하세요. 1MB = 1048576 바이트입니다.