Go나 자바, 프런트엔드 앱을 도커로 빌드하면 이미지가 수백 MB로 불어나는 경우가 많습니다. 컴파일러, 빌드 도구, 의존성 캐시까지 전부 이미지에 남기 때문입니다. **멀티스테이지 빌드(multi-stage build)**는 "빌드하는 단계"와 "실행하는 단계"를 분리해, 최종 이미지에는 실행에 꼭 필요한 결과물만 남기는 기법입니다.
왜 단계를 나누는가
하나의 Dockerfile 안에 FROM을 여러 번 써서 여러 스테이지를 만들 수 있습니다. 핵심은 마지막 스테이지가 곧 최종 이미지가 되고, 앞 스테이지들은 결과물을 꺼내 쓰고 나면 버려진다는 점입니다.
| 단계 | 베이스 이미지 | 포함되는 것 |
|---|---|---|
| 빌드 스테이지 | golang, node 등(무거움) | 컴파일러, 소스, 의존성 |
| 실행 스테이지 | alpine, distroless(가벼움) | 컴파일된 바이너리뿐 |
빌드 도구는 빌드할 때만 필요하지 실행할 때는 짐일 뿐입니다. 멀티스테이지는 이 짐을 최종 이미지에서 떼어냅니다.
COPY --from 으로 결과만 꺼내기
핵심 문법은 COPY --from=<스테이지>입니다. 앞 스테이지에서 만든 파일을 다음 스테이지로 가져옵니다.
# 1단계: 빌드 (이 스테이지는 최종 이미지에 안 남음)
FROM golang:1.22 AS builder
WORKDIR /src
COPY . .
RUN go build -o /app/server ./cmd/server
# 2단계: 실행 (이게 최종 이미지)
FROM gcr.io/distroless/static
COPY --from=builder /app/server /server
ENTRYPOINT ["/server"]
최종 이미지에는 golang 베이스도, 소스 코드도, 빌드 캐시도 없습니다. 오직 /server 바이너리 하나만 distroless 위에 얹힙니다. 결과적으로 수백 MB짜리가 수십 MB 이하로 줄어듭니다.
용량 말고도 얻는 것
작아지는 것만이 이득이 아닙니다. 빌드 도구와 셸, 패키지 매니저가 빠지면 **공격면(attack surface)**도 함께 줄어듭니다. 컨테이너 안에 컴파일러가 없으니 침투해도 할 수 있는 일이 적습니다.
빌드 결과는 docker history나 이미지 크기로 바로 확인할 수 있습니다.
docker build -t my-app .
docker images my-app
docker history my-app
자주 하는 실수
특정 스테이지까지만 빌드하고 싶다면 --target을 씁니다. 디버깅 시 빌드 스테이지만 떼어 볼 때 유용합니다.
docker build --target builder -t my-app-build .
또 흔한 실수는 COPY --from의 경로를 빌드 스테이지의 실제 산출 경로와 안 맞추는 것입니다. 앞 스테이지에서 파일이 어디 생기는지 먼저 확인하세요.
요점 정리
- 멀티스테이지는 빌드 단계와 실행 단계를 분리해 최종 이미지를 가볍게 한다.
COPY --from으로 앞 스테이지의 결과물만 꺼내 온다.- 빌드 도구가 빠지면 용량뿐 아니라 보안 공격면도 줄어든다.
--target으로 특정 스테이지까지만 빌드할 수 있다.
무거운 이미지를 멀티스테이지로 직접 줄여 보며 용량 차이를 확인하는 실습은 도커 트랙에서 할 수 있습니다 — 회원가입 없이 무료로.