Multi-stage builds
의 등장 전 상황
이미지를 빌드하고 게시할 때 때때로 해당 이미지의 크기가 상당히 커지는 문제가 있었습니다.
Multi-stage builds
가 등장하기 전에는 이미지 크기를 작게 하려면 이미지에서 리소스를 수동으로 정리했어야 했습니다.
과거에는 하나의 Dockerfile
을 개발용으로 사용하고 다른 하나는 배포용으로 사용하는 것이 일반적이었습니다.
개발버전의 경우 어플리케이션을 구축하는데 필요한 모든 것이 포함되어있었고, 배포용은 어플리케이션과 실행을 위한 종속성이 필요한 항목만 포함되어 있었습니다.
가장 효율적인 Dockerfile
을 작성하려면, shell 명령어 꼼수를 이용하거나 난해한 솔루션을 사용해서, 각 layer
에 필요한 아티펙트
만을 포함하고 다른 것은 포함시키지 않게 했어야 했습니다.
이것은 builder pattern
이라고 불렀습니다. 이 패턴은 build.Dockerfile
(개발 빌드용)와 Dockerfile
(간소화된 배포 빌드용) 으로 나누어졌습니다.
흔히 사용하는 방법 중 쉘 명령어 꼼수로 &&
를 사용해 두 명령을 압축하였습니다.
build.Dockerfile(개발용) 예시
# syntax=docker/dockerfile:1
# Golang 1.16 이미지를 기반 이미지로 사용합니다.
# 이 이미지는 Golang 개발 환경을 구축하는 데 필요한 도구와 라이브러리를 포함합니다.
FROM golang:1.16
# 작업 디렉토리를 /go/src/github.com/alexellis/href-counter/로 설정합니다.
# 이 디렉토리는 컨테이너 내에서 소스 코드를 저장하고 빌드할 위치를 지정합니다.
WORKDIR /go/src/github.com/alexellis/href-counter/
# 현재 디렉토리의 app.go 파일을 컨테이너의 작업 디렉토리로 복사합니다.
# 이 파일은 Golang 프로젝트의 소스 코드일 것으로 예상됩니다.
COPY app.go ./
# Golang의 golang.org/x/net/html 패키지를 다운로드합니다.
# -d 플래그는 패키지를 다운로드만 하고 설치는 하지 않도록 지정합니다.
#app 바이너리를 빌드합니다. CGO_ENABLED=0는 CGO(C Go Call)를 비활성화하고,
#-a는 모든 종속성을 새로 빌드하도록 지정하며,
#-installsuffix cgo는 CGO에 대한 설치 접미사를 추가합니다.
# 마지막으로, -o app는 빌드된 실행 파일의 이름을 app로 지정합니다.
RUN go get -d -v golang.org/x/net/html \
&& CGO_ENABLED=0 go build -a -installsuffix cgo -o app .
이 방법은 추가적인 layer의 생성을 방지합니다. 다만 이런 방법은 실패할수도 있으며, 유지보수 측면에서는 상당이 안 좋습니다.
이 build.Dockerfile
은 Golang 1.16
을 기반으로 하는 alexellis/href-counter
이미지를 빌드하는 데 사용됩니다.
Dockerfile(배포용) 예시
# syntax=docker/dockerfile:1
# 최신 버전의 Alpine Linux를 기반 이미지로 사용합니다.
# Alpine Linux는 경량 리눅스 배포판으로, 작은 크기와 빠른 속도로 알려져 있습니다.
FROM alpine:latest
# Alpine 패키지 관리자인 apk를 사용하여 ca-certificates를 설치합니다.
# --no-cache 플래그는 패키지 캐시를 사용하지 않도록 지정합니다.
RUN apk --no-cache add ca-certificates
# 작업 디렉토리를 /root/로 설정합니다.
# 작업 디렉토리는 컨테이너 내에서 명령을 실행할 위치를 말합니다.
WORKDIR /root/
# 현재 디렉토리의 app 파일을 컨테이너의 /root/ 디렉토리로 복사합니다.
# 이 파일은 컨테이너 내에서 실행될 응용 프로그램 파일일 것으로 예상됩니다.
COPY app ./
# 컨테이너가 시작되었을 때 실행될 명령을 지정합니다.
# 여기서는 /root/app을 실행하는 것으로 지정되어 있으므로,
# 컨테이너가 시작되면 /root/app이 실행될 것입니다.
CMD ["./app"]
이 Dockerfile은 Docker 컨테이너를 빌드하고 실행하기 위한 명령어를 정의하는 파일입니다.
- 첫번째 이미지를 빌드합니다.(도커빌드 파일)
- 아티펙트를 복사할 컨테이너를 만듭니다.
- 두 번째 이미지를 빌드합니다.
유틸리티 스크립트
이 두개의 도커파일을 이용해서 가장 작은 도커파일을 만듭니다.
#!/bin/sh
#빌드 중인 이미지의 정보를 출력합니다.
echo Building alexellis2/href-counter:build
# build.Dockerfile을 사용하여 alexellis2/href-counter:build 이미지를 빌드합니다.
# 현재 디렉토리(.)에 있는 소스 코드를 사용하여 이미지를 빌드합니다.
docker build -t alexellis2/href-counter:build . -f build.Dockerfile
#alexellis2/href-counter:build 이미지를 기반으로 extract라는 이름의 컨테이너를 생성합니다.
#즉 golang을 기반으로하는 컨테이너 생성합니다.
docker container create --name extract alexellis2/href-counter:build
# extract 컨테이너에서 /go/src/github.com/alexellis/href-counter/app 경로에 있는 파일을
# 호스트의 현재 디렉토리로 복사합니다.
# 이 파일은 alexellis2/href-counter:build 이미지에서 빌드된 애플리케이션 파일입니다.
docker container cp extract:/go/src/github.com/alexellis/href-counter/app ./app
#extract 컨테이너를 강제로 제거합니다.
docker container rm -f extract
#최신 버전(latest)의 alexellis2/href-counter 이미지를 빌드하는 작업 정보를 출력합니다.
echo Building alexellis2/href-counter:latest\
#latest 태그를 가진 alexellis2/href-counter 이미지를 빌드합니다.
#--no-cache 플래그를 사용하여 캐시를 사용하지 않고 빌드합니다.
docker build --no-cache -t alexellis2/href-counter:latest .
#호스트의 현재 디렉토리에서 app 파일을 삭제합니다.
#이 파일은 이전에 extract 컨테이너에서 복사된 애플리케이션 파일입니다.
rm ./app
결과적으로 app
두 이미지 모두 시스템에서 공간을 차지하며 로컬 디스크에도 아티팩트가 남게 됩니다 .
Multi-stage builds
는 이런 번거로운 작업을 하던 사용자를 위해 등장했습니다. Multi-stage builds
는 이것을 단순하게 만들었습니다.
Multi-stage builds
Multi-stage builds
는 Dockerfile
에서 FROM
문을 여러 번 사용합니다.
각각 FROM 문
은 각각 다른 base
를 사용할 수 있으며 각 명령어의 빌드는 new stage
로 시작합니다. 당신이 from
이 한 스태이지
지날 때마다 선택적으로 artifacts
를 복사할 수 있습니다. 만약 당신이 마지막 이미지에 원하지 않는 것들을 다 남겨둘 수 있습니다.
# syntax=docker/dockerfile:1
FROM golang:1.16
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html
COPY app.go ./
RUN CGO_ENABLED=0 go build -a -installsuffix cgo -o app .
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=0 /go/src/github.com/alexellis/href-counter/app ./
CMD ["./app"]
이것을 이해하기 위해서는 Dockerfile의 이전 단계에 대한 이해가 필요합니다.
Multi-stage builds
는 Dockerfile
만이 필요하고 ,별도의 스크립트도 필요없이 docker build.
를 통해 실행합니다.
docker build -t alexellis2/href-counter:latest .
결국 최종 이미지는 복잡성은 줄어들었지만 이전과 동일하게 작은 배포이미지가 탄생했다.
이 방법은 middle image
를 만드는 과정이 없어도 되고 로컬 시스템의 아티팩트를 추출할 필요가 없어졌습니다.
이 도커파일의 핵심은 COPY --from=0 /go/src/github.com/alexellis/href-counter/app ./
이 명령이다. 이 명령은 이전 스태이지에서 빌트한 것을 복사해온다. 다만 중간 아티팩트들은 뒤에 캐쉬로 남아 있으며, 최종 이미지 상에서는 저장되지 않습니다.
Stage
에 이름 붙히기
기본적으로 단계는 이름이 지정되지 않으며 첫 번째 FROM
명령에 대해 0으로 시작하는 정수로 참조됩니다. 그러나 FROM에 AS <NAME>
을 추가하여 단계 이름을 지정할 수 있습니다. 그래서 이전에 사용했던 Dockerfile
에서 이전 스태이지를 0으로 지정했지만 이름을 붙혀서 사용이 가능합니다.
# syntax=docker/dockerfile:1
FROM golang:1.16 AS builder
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html
COPY app.go ./
RUN CGO_ENABLED=0 go build -a -installsuffix cgo -o app .
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
#여기를 보면 from=스태이지 이름 으로 카피해온다.
COPY --from=builder /go/src/github.com/alexellis/href-counter/app ./
CMD ["./app"]
이렇게 해주면 나중에 Dockerfile의 순서가 바뀌는 경우에도 Copy 대상이 정확하게 지정되어있기 때문에 더 좋은 방법 입니다.
특정 빌드 단계에서 중지
이미지를 빌드할 때 모든 단계를 포함하여 전체 Dockerfile
을 반드시 빌드할 필요는 없습니다. 대상 빌드 단계를 지정할 수 있습니다.
다음 명령은 사용자가 이전 도커 파일을 사용하고 있지만, builder
라는 단계에서 중지됩니다:
docker build --target builder -t alexellis2/href-counter:latest .
이것은 특정 빌드 단계에서 디버깅할 때 사용할 수 있습니다.
또는, testing
앱이 테스트 데이터로 채워지는 단계를 사용 하지만 실제 데이터를 사용하는 다른 단계를 사용해서 특정 빌드단계에서 중단 하는 경우 입니다.
외부 이미지를 Stage로 사용
Multi-stage builds
는 Dockerfile 내에서 만든 이미지만을 복사할 수 있는게 아닙니다.
COPY --from
명령을 사용하여 로컬 이미지이름(태그ID 등)을 사용해서, 별도의 이미지에서 복사가 가능합니다.
다음은 예시입니다.
COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf
이전 스태이지를 FROM
에 사용
# syntax=docker/dockerfile:1
FROM alpine:latest AS builder
RUN apk --no-cache add build-base
FROM builder AS build1
COPY source1.cpp source.cpp
RUN g++ -o /binary source.cpp
FROM builder AS build2
COPY source2.cpp source.cpp
RUN g++ -o /binary source.cpp
BuildKit
활성화
이전에 사용하는 빌드는 도커파일의 모든 부분을 다 사용합니다. --target
선택한 대상이 해당 단계에 의존하지 않더라도 단계를 구축합니다.
하지만 BuildKit 을 사용하면, 대상 단계가 의존하는 단계만 빌드합니다.
예를 들어 다음과 같은 도커파일이 있다고 보겠습니다.
# syntax=docker/dockerfile:1
FROM ubuntu AS base
RUN echo "base"
FROM base AS stage1
RUN echo "stage1"
FROM base AS stage2
RUN echo "stage2"
BuildKit을 사용하도록 설정한 상태에서,stage2
와 base
만 빌드가 되고, stage 1
은건너뛰는 모습을 볼 수 있습니다.
아래는 BuildKit으로 빌드한 모습입니다.
DOCKER_BUILDKIT=1 docker build --no-cache -f Dockerfile --target stage2 .
[+] Building 0.4s (7/7) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 36B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/ubuntu:latest 0.0s
=> CACHED [base 1/2] FROM docker.io/library/ubuntu 0.0s
=> [base 2/2] RUN echo "base" 0.1s
=> [stage2 1/1] RUN echo "stage2" 0.2s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:f55003b607cef37614f607f0728e6fd4d113a4bf7ef12210da338c716f2cfd15 0.0s
하지만 BuildKit
이 없이 빌드하면 전부 다 빌드가 됩니다.
DOCKER_BUILDKIT=0 docker build --no-cache -f Dockerfile --target stage2 .
참고자료
https://docs.docker.com/build/building/multi-stage/#name-your-build-stages
https://blog.alexellis.io/mutli-stage-docker-builds/
'42Seoul > Inception' 카테고리의 다른 글
Build context [42 inception 과제 개념 심화2] (0) | 2023.06.24 |
---|---|
best practices for writing Dockerfiles (과제 요구사항) [42 inception 과제 개념12] (2) | 2023.06.24 |
컨테이너 빌드에 대한 권장사항 [PID 1] [42 inception 과제 개념11] (0) | 2023.06.24 |
Docker Volume이란? [42 inception 과제 개념 10] (0) | 2023.06.24 |
Dockerfiles이란? [42 inception 과제 개념 9] (0) | 2023.06.24 |