Docker image and layer
- 출처: https://docs.docker.com/build/guide/intro/
Docker image에 대한 전반적인 이해를 위해서 docker 공식 guide document 중 Build with docker 의 내용이 좋아보여 차근차근 따라해보려고 한다.
- Introduction
Guide 문서 에서는 Go project와 Dockerfile을 통해서 설명하고 있다. (Go에 대한 지식은 필요 없다고 말하고 있다.) Docker desktop이나 Docker engine은 설치되어 있다고 가정한다. 예제 환경 구성을 위해 Github example repository(https://github.com/dockersamples/buildme.git)를 clone 한다. clone후 폴더 구조를 살펴보면 아래와 같이 되어 있다.
% git clone https://github.com/dockersamples/buildme.git
Cloning into 'buildme'...
...
% ls
buildme
% cd buildme
% tree
.
├── Dockerfile
├── README.md
├── Taskfile.yml
├── chapters
...
├── cmd
│ ├── client
│ │ ├── main.go
│ │ ├── request.go
│ │ └── ui.go
│ └── server
│ ├── main.go
│ └── translate.go
├── go.mod
└── go.sum
cmd 폴더는 client와 server component로 구성되어 있다. client는 쓰기, 전송, 메시지를 받는 부분이고 server는 client로 부터 메시지를 수신하여 변환하고 다시 client로 되돌려주는 역할을 한다.
Dockerfile은 application을 빌드하기 위한 과정이 기술된 문서이다.
% cat Dockerfile
# syntax=docker/dockerfile:1
FROM golang:1.20-alpine
WORKDIR /src
COPY . .
RUN go mod download
RUN go build -o /bin/client ./cmd/client
RUN go build -o /bin/server ./cmd/server
ENTRYPOINT [ "/bin/server" ]
위의 Dockerfile의 의미를 살펴보면 아래와 같다.
- #syntax=docker/dockerfile:1 : Dockerfile parse 지시어이며 사용할 Dockerfile 문법의 version을 지정한다.
- FROM golang:1.20-alpine : golang:1.20-alpine 이미지를 사용한다.
- WORKDIR /src : container 내에 /src working directory 를 생성한다.
- COPY . . : Build context 의 파일들을 working directory로 복사
- RUN : 명령어 실행
- ENTRYPOINT : container가 시작할 때 실행할 명령어를 지정한다.
Dockerfile을 사용하면 image를 build할 수 있다. docker build 명령어를 수행해보자.
% docker build --tag=buildme .
[+] Building 20.9s (17/17) FINISHED
...
Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them
% docker images | grep buildme
buildme latest ad54f8e8cc3c 9 seconds ago 410MB
예제 Dockerfile 에서 server와 client binary를 둘 다 build 하였다. server와 client binary를 각각 실행하는 container를 생성하여 예제 프로그램을 구동해보자.
% docker run --name=buildme --rm -d buildme
cda9ce17a5e6b04ad817591cf184ac5faee264f2a4417509131b32fe0174961b
% docker exec -it buildme /bin/client
...
> Translate a message...
╭───────────────────────────────────────╮
│ Input: hi buildme-server │
│ Translation: hohi bobuiloldodmomesose │
╰───────────────────────────────────────╯
Ctrl+C to exit.
- docker run 명령어로 buildme 라는 이름의 container를 detach mode로 실행시켰다. ENTRYPOINT가 /bin/server 이므로 server를 수행할것이다.
- docker exec 명령어로 대화형 terminal 모드로 /bin/client 를 실행시켰다.
- Layers
Dockerfile 명령어의 순서는 중요하다. Dockerfile의 각 명령어는 image layer로 변환된다. 아래 그림은 Dockerfile 명령어들이 어떻게 container image 내의 layer를 구성하는지 보여준다.
build를 수행할 때 builder는 이미 존재하는 layer가 있으면 재사용한다. 만약 image의 layer에 변경사항이 없으면 builder는 cache 에서 가져온다. 만약 특정 layer가 변경되었다면 해당 layer 이후의 layer들은 모두 다시 build 되어야 한다.
이전 Dockerfile에서 COPY . . 명령어로 project의 모든 file을 복사한 후, RUN go mod download로 application 의존성을 download하는 명령어들이 있었다. 만약 project file에 변동이 생기면 COPY layer의 cache는 재사용이 불가하게 되며 COPY . . 이후의 명령어들도 모두 다시 빌드되어야 한다. 즉 현재 상태로는 go 의존성 module에는 변동사항이 없더라도 project file에 변동이 생기면 application 의존성을 다시 download 받아야 한다.
아래 예제는 buildme를 단순히 다시 build 한것과 newfile 하나만 추가하여 다시 build 한것이다. 앞에 CACHED 부분을 눈여겨 보길 바란다.
% docker build -t buildme:2 .
[+] Building 1.9s (15/15) FINISHED
... 0.0s
=> CACHED [2/6] WORKDIR /src 0.0s
=> CACHED [3/6] COPY . . 0.0s
=> CACHED [4/6] RUN go mod download 0.0s
=> CACHED [5/6] RUN go build -o /bin/client ./cmd/client 0.0s
=> CACHED [6/6] RUN go build -o /bin/server ./cmd/server 0.0s
...
% touch newfile
% docker build -t buildme:3 .
[+] Building 10.9s (15/15) FINISHED
... 0.0s
=> CACHED [2/6] WORKDIR /src 0.0s
=> [3/6] COPY . . 0.0s
=> [4/6] RUN go mod download 3.1s
=> [5/6] RUN go build -o /bin/client ./cmd/client 5.0s
=> [6/6] RUN go build -o /bin/server ./cmd/server 0.7s
Project file에 변동이 생기면 client와 server binary는 다시 구성되는게 맞지만 의존성 module 정보는 변동이 없다면 cache를 사용하도록 수정해주자. Go는 의존성 정보 관리를 위해 go.mod와 go.sum 이라는 file을 사용한다. Dockerfile을 아래와 같이 수정해준다.
% cat Dockerfile
# syntax=docker/dockerfile:1
FROM golang:1.20-alpine
WORKDIR /src
COPY go.mod go.sum .
RUN go mod download
COPY . .
RUN go build -o /bin/client ./cmd/client
RUN go build -o /bin/server ./cmd/server
ENTRYPOINT [ "/bin/server" ]
아까와 같이 수정된 Dockerfile을 다시 build 하고 newfile2를 추가하여 다시 build 해보자. 다시 build를 했음에도 go의 의존성 모듈 파일과 의존성을 download 받는 부분은 CACHED 된것을 확인할 수 있다.
% docker build -t buildme:4 .
[+] Building 9.0s (16/16) FINISHED
... 0.0s
% touch newfile2
% docker build -t buildme:5 .
[+] Building 7.9s (16/16) FINISHED
... 0.0s
=> CACHED [2/7] WORKDIR /src 0.0s
=> CACHED [3/7] COPY go.mod go.sum . 0.0s
=> CACHED [4/7] RUN go mod download 0.0s
=> [5/7] COPY . . 0.0s
=> [6/7] RUN go build -o /bin/client ./cmd/client 5.0s
=> [7/7] RUN go build -o /bin/server ./cmd/server 0.8s
...