https://docs.docker.com/build/building/multi-stage/
공식 문서를 번역하면서 공부한 것이라 오역이 있을 수 있습니다.
Use multi-stage builds
Multi-stage build는 이미지 빌드와 최종 결과물 사이를 깔끔하게 분리하여 최종 이미지 사이즈를 줄어준다.
Dockerfile 명령어를 여러 단계로 나누어 Dockerfile의 결과물이 애플리케이션을 실행하는 데 필요한 파일만 포함하도록 하자
multi-stage는 빌드 단계를 병렬로 실행하여 효율적으로 빌드할 수 있다.
빌드 프로세스의 마지막 단계에서 실제로 사용되는 이미지가 작성도므로, 빌드 캐시를 활용하여 이미지 레이어를 최소화할 수 있다.
예를 들어, 빌드에 여러 개의 레이어가 포함되어 있는 경우, 변경이 별로 없는 레이어 (빌드 캐시를 적극 활용할 수 있는 레이어)에서 자주 변경이 있어나는 레이어 순으로 지정할 수 있다.
- 애플리케이션 빌드를 위해 필요한 툴 설치
- 라이브러리 의존성 설치 또는 업데이트
- 애플리케이션 생성
아래 Go 애플리케이션 예제를 살펴보자
# syntax=docker/dockerfile:1
FROM golang:1.16-alpine AS build
# `docker build --no-cache .` 실행시 의존성 업데이트
RUN apk add --no-cache git
RUN go get github.com/golang/dep/cmd/dep
# Gopkg.toml Gopkg.lock 에 있는 프로젝트 의존성 나열
# 이러한 레이어는 GoPkg파일이 업데이트 되었을 때만 재 빌드 된다.
COPY Gopkg.lock Gopkg.toml /go/src/project/
WORKDIR /go/src/project/
# 라이브러리 의존성 설치
RUN dep ensure -vendor-only
# 전체 프로젝트를 복사하고 빌드
# 이 레이어는 프로젝트 디렉토리에 파일 변경이 있을 때만 다시 빌드됨
COPY . /go/src/project/
RUN go build -o /bin/project
# 이 결과물이 싱글 레이어 이미지에 들어감
FROM scratch
COPY --from=build /bin/project /bin/project
ENTRYPOINT ["/bin/project"]
CMD ["--help"]
Multi-stage란
multi-stage는 Dockerfile을 읽기 쉽고 유지 관리하기 쉽게 유지하면서 최적하하는데 어려움을 줄여준다.
multi-stage를 사용하면 Dockerfile에서`FROM` 명령어를 여러번 사용할 수 있다.
`FROM` 명령어는 각각 다른 base 이미지를 사용할 수 있다. 그리고 각각 다른 base이미지를 사용하면 빌드의 새로운 stage를 시작할 수 있다.
한 stage에서 원하는 artifacts를 다른 stage로 복사할 수 있다.
다음 예시의 Dockerfile에서 2개의 분리된 stage가 있다.
- binary를 빌드하는 stage
- binary를 첫번째 stage에서 다음 stage로 복사하는 stage
# syntax=docker/dockerfile:1
FROM golang:1.23
WORKDIR /src
COPY <<EOF ./main.go
package main
import "fmt"
func main() {
fmt.Println("hello, world")
}
EOF
RUN go build -o /bin/hello ./main.go
FROM scratch
COPY --from=0 /bin/hello /bin/hello
CMD ["/bin/hello"]
multi-stage 동작 과정
두개의 stage를 생성하는데에 한개의 Dockerfile만 필요하고 build 명령도 한번만 입력하면 된다.
docker build -t hello .
위 빌드 결과물을 binary만 포함된 아주 작은 이미지이다. 애플리케이션을 빌드하는데 필요한 빌드 도구는 이미지 결과에 포함되지 않았다.
두번째 `FROM` 명령어에서 `scratch`를 base image로 새로운 stage를 빌드한다. `COPY --from=0` 라인에서 이전 stage에서 빌드된 산출물을 두번째 stage에 복사한다. 여기에 Go SDK와 다른 중간 산출물은 최종 이미지에 저장되지 않는다.
Multi-Stage로 어떻게 최적화 하나?
Docker 이미지의 목적은 실행이지, 빌드에 있지 않다. 컴파일이나 빌드 관련 명령어는 제외하자
- 과거에는 도커 이미지 생성 시 프로그램 빌드 후 도커 이미지 생성
- 현재에는 이미 프로그램 빌드가 마쳐진 결과물만 가지고 (COPY) 도커 이미지 생성
혹시라도 도커 이미지 내에서 프로그램 빌드(컴파일)을 한다면 멀티 스테이지 도입이 시급
- Java의 경우 `/gradlew clean build`, JS인 경우 `npm install`
- Java인 경우 `build` 디렉토리에 쌓인 `.class` 바이트코드 사이즈가 너무 크고 (.JAR 파일을 COPY해서 사용하는 이유)
- JS의 경우 `node_nodule` 사이즈가 너무 크다.
도커 이미지의 사이즈는 크면 클수록 해당 도커 이미지 실행을 하는 EC2의 메모리와 네트워크에 영향을 준다.
- 이미지 사이즈가 1GB라면 EC2는 적어도 그 이미지를 다운받을 수 있는 1GB 이상의 메모리가 필요
- 네트워크 대역폿 지원이 미흡한 저비용 EC2의 경우 도커 실행(CD 배포 완료)까지 리드타임이 길어진다.
따라서 Muti-Stage Build 를 통해 빌드 이미지(Stage) 따로 실행 이미지(Stage) 따로 Dockerfile에 정의한다.
아래는 빌드에 필요한 파일들을 압축 형태로 빌드한 stage와 실행하는 stage를 따로 정의하고 있다.
# Java 예) JDK -> as 'Build Stage' (node_module 같은거 필요없으니까)
# Java 예) JRE 만 가지고 돌리면되니까 -> as 'App Stage'
from node:18-alpine as buildstage
npm run build
...
from node:18-alpine as app
CMD ["node", "server.js"]
ref
'ASAC 웹 풀스택 > DevOps' 카테고리의 다른 글
[Docker] Docker Workflow (1) | 2024.10.19 |
---|---|
[Docker] Github Action으로 CI/CD - CD (1) | 2024.10.17 |
[Docker] Docker를 통한 어플리케이션 관리: 단일 / 다중 컨테이너 (0) | 2024.10.16 |
[Docker] Docker 사용하는 이유 (0) | 2024.10.16 |