Docker multi-stage build를 통해 이미지 경량화하기

Why?

보통 베이스가 되는 도커 이미지는 생각보다 큰 용량을 가지고 있습니다. 거기에 의존성으로 추가적인 도구를 설치하고, 빌드하게 되면 생각보다 엄청 큰 용량으로 탄생합니다. 이런 경우에 alpine이나 minimum 이미지와 같이 경량화된 이미지를 사용하는 방법을 많이 사용합니다.

예전 dalfox의 dockerfile

FROM golang:1.15.2-alpine3.12

WORKDIR /go/src/app
COPY . .

RUN go get -d -v ./...
RUN go install -v ./...

CMD ["dalfox"]

지난달인가.. golang-latest -> alpine으로 교체 후 반토막 났네요 :D

Builder Pattern

전 위 도커파일에서 하나의 파일에서 그냥 빌드와 실행 모두를 담았지만, 빌드와 러닝 이미지를 나누는 Builder Pattern도 있습니다. 그래서 Builder image에선 앱 빌드에 필요한 디펜던시 설정, 빌드 후 바이너리를 만들고, 실제로 동작하는 러닝 이미지에선 빌더로 부터 바이너리만 받아서 사용하는 방식입니다. 이러한 과정을 거치면 결국 Build에만 필요한 불필요한 도구, 라이브러리, 이미지 내 파일들을 제외하고 아주 컴팩트한 이미지에서 바이너리만 가지고 동작시킬 수 있습니다.

Multi-stage build

빌드 패턴을 사용하기 위해선 Builder 와 실제 실행될 이미지, 이렇게 2개의 Dockerfile이 필요하게 됩니다. 이는 관리상 굉장히 불편할 것 같은데, 다행히 Muilti-stage build가 있어 한 파일에서 2개의 이미지를 빌드하고 사용할 수 있습니다.

사용법은 단순한데, 그냥 한 파일에서 Base 이미지를 바꿔서 사용하면 마치 2개 이상의 Dockerfile이 있는 것과 동일하게 빌드를 수행할 수 있습니다. Builder에서 빌드한 바이너리를 실행할 이미지로 전달해주기 위해선 COPY--from 옵션을 통해 실행 이미지로 전달해줄 수 있습니다. 회사에서 이것저것 테스트해보고, Dalfox에도 반영해봤습니다.

현재 dalfox의 dockerfile

# BUILDER
FROM golang:latest AS builder
WORKDIR /go/src/app
COPY . .RUN go get -d -v ./...
RUN go build -o dalfox
​
# RUNNING
FROM debian:buster
RUN mkdir /app
COPY --from=builder /go/src/app/dalfox /app/dalfox
COPY --from=builder /go/src/app/samples /app/samples
WORKDIR /app/
CMD ["/app/dalfox"]

​ 정리하면 이렇습니다. Golang:latest에서 빌드를 수행하고, 실행 바이너리를 debian:buster로 전달하여 해당 이미지에서 동작하게 합니다. debian:buster 는 golang 이미지의 베이스 이미지이고, 정말 미니멀한 이미지입니다. 그래서.. 이렇게 적용해보면..

Conclusion

생각보다 간단한 방법으로 이미지의 크기를 엄청 줄일 수 있었네요… dalfox는 디펜던시가 적어서, 크게 줄어들진 않았지만 회사에서 사용하던 이미지는 약 1GB정도나 줄였습니다..ㅋㅋ ​

References

​* https://docs.docker.com/develop/develop-images/multistage-build/