도커의 이미지는 여러 레이어를 통한 일련의 파일 시스템 계층으로 구성되어 있다. 각 계층은 파일 시스템의 이전 레이어로부터 파일을 추가, 제거 수정하는 오버레이 파일 시스템(overlay filesystem)의 구조를 갖는다
실제로, 모든 도커 이미지의 레이어들은 var/lib/docker/overlay2에 저장되고 해시값을 통해 구분된다.
(Dockerfile의 FROM, RUN, COPY 명령은 새로운 레이어를 생성한다)
이미지 A: 레이어 A -> 레이어 B -> 레이어 C
이미지 B: 레이어 A -> 레이어 B -> 레이어 C -> 레이어 D
이미지 C: 레이어 A -> 레이어 B -> 레이어 E
위와 같이 이미지 3개가 존재한다고 할 때, 이미지 A를 삭제하더라도 레이어 A, B, C는 이미지 B에서 사용되고 있기 때문에 파일 시스템에서 지워지지 않고, 이미지 C를 추가로 생성할 때에 레이어 A와 B가 이미 다운로드 되어있기에 별도로 다운로드하거나 생성하지 않고 레이어 E만 만들게 된다.
ubuntu에 openssh server를 설치하는 이미지를 생성하려고 한다.
# Dockerfile A
FROM ubuntu
RUN apt-get update && apt-get upgrade -y
RUN apt-get install -y openssh-server
# Dockerfile B
FROM ubuntu
RUN apt-get update && apt-get upgrade -y && apt-get install -y openssh-server
만약 위와 같이 2개의 이미지가 존재한다면, 해당 이미지를 docker inspect 명령어를 통하여 보았을 때 레이어를 파악할 수 있다.
$ docker inspect image_a
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:17ccff5b06b404f920af4557c0e2cb6ee5551b7136dc0cdb4f9aaf1bf71c643c",
"sha256:ede334733a68a6091f2137efcdc0b86284885442db0c96cb72b0c47b27d74ed6",
"sha256:47592da01660e3fb88cfbb307cbcd8faf61b879bf6fc2eaccae598d0a35ab53b"
]
},
$ docker inspect image_b
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:17ccff5b06b404f920af4557c0e2cb6ee5551b7136dc0cdb4f9aaf1bf71c643c",
"sha256:d1bd4cf291b49e133ee9c9f4679f45fffe91e2cfca7a048466d67eebd7bef269"
]
},
위와 같이 기반 이미지의 레이어인 sha256:17ccff5b06b404f920af4557c0e2cb6ee5551b7136dc0cdb4f9aaf1bf71c643c을 공통으로 갖고 실행된 RUN 명령어마다 새로운 레이어가 추가되는 것을 확인할 수 있다.
만약 이미지 레이어 계층이
레이어 A: 대용량 파일 DumpFile 포함
레이어 B: 레이어 A 기반 DumpFile 제거
레이어 C: 레이어 B 기반 정적 바이너리 추가
와 같이 구성되어있다고 할 때에, 레이어 C에서는 DumpFile에 접근할 수 없지만, 해당 이미지를 푸시하거나 풀하게 되면 네트워크를 통해 해당 파일을 전송하고 이미지 용량이 커지는 것을 확인할 수 있다.
위와 같은 상황을 고려하여 이미지 레이어를 구축하여야 한다.
# Dockerfile A
FROM ubuntu
COPY server.js /root
RUN apt-get update && apt-get upgrade -y && apt-get install -y nodejs
# Dockerfile B
FROM ubuntu
RUN apt-get update && apt-get upgrade -y && apt-get install -y nodejs
COPY server.js /root
또한 다음과 같이 2개의 도커 파일을 통해 생성한 이미지가 있을 때, 만약 server.js 파일이 변경된다면 파일 A는 패키지를 다운로드하는 레이어를 다운로드하고 업로드해야하지만 파일 B는 변경된 파일만 변경하도록 한다.
이처럼 변경 가능성이 가장 적은 계층부터 변경 가능성이 가장 높은 계층 순으로 배치하는 것이 좋다.
위와 같이 여러 레이어로 하나의 이미지 파일을 생성할 수도 있지만, 어플리케이션 컨테이너 이미지에 불필요한 빌드를 위한 도구를 포함하는 것은 컨테이너 용량을 키워 배포 속도 및 성능에 영향을 줄 수도 있다.
이러한 상황을 multi-stage build를 통해 해결할 수 있는데, 하나의 이미지 파일로 여러개의 이미지를 생성하여 빌드 이미지와 어플리케이션 이미지를 구분하는 방법이다.
FROM golang:1.21 AS build
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=build /bin/hello /bin/hello
CMD ["/bin/hello"]
$ docker build -t multi-stage-example
$ docker inspect multi-stage-example
위와 같이 명령어를 실행하면 레이어가 COPY를 통해 만들어진 하나만 존재하는 것을 확인할 수 있다.
만약 build 시점의 이미지를 확인하고 싶다면
$ docker build --target build -t multi-stage-example-build
$ docker inspect multi-stage-example-build
위 명령어를 실행하면 빌드 이미지를 위하여 여러 레이어가 쌓여있는 것을 확인할 수 있다.