
在構建服務期間,我們經常需要構建docker映象。我們每天都要做很多次。這可能是一個耗時的任務。在本地,我們只注意到一點,但在CI/CD管道中,這可能是一個問題。
在這篇文章中,我將告訴你如何加快構建Docker映象這一過程。我將向你展示如何使用快取,將你的Docker檔案分層,並使用多階段構建,以使你的構建更快。
為此,我將使用一個簡單的Go應用程式。你可以使用你的任何其他應用程式。你使用哪個堆疊、語言或框架並不重要。原則都是一樣的。
我所做的一切都在我的本地機器上執行。我不使用任何CI/CD工具。我使用Docker Desktop for Mac。
清理工作
為了確保我們從一個乾淨的狀態開始,我們可以刪除所有未使用的映象、容器、卷和網路:
WARNING! This will remove:
- all networks not used by at least one container
- all images without at least one container associated to them
Are you sure you want to continue? [y/N] y
$ docker system prune -a
WARNING! This will remove:
- all stopped containers
- all networks not used by at least one container
- all images without at least one container associated to them
- all build cache
Are you sure you want to continue? [y/N] y
...gone with the wind...
$ docker system prune -a
WARNING! This will remove:
- all stopped containers
- all networks not used by at least one container
- all images without at least one container associated to them
- all build cache
Are you sure you want to continue? [y/N] y
...gone with the wind...
起始點
我從一個簡單的Dockerfile(Dockerfile_1)開始:
ENTRYPOINT [ "/app/app" ]
FROM golang:buster
WORKDIR /app
COPY app /app/
ENTRYPOINT [ "/app/app" ]
FROM golang:buster
WORKDIR /app
COPY app /app/
ENTRYPOINT [ "/app/app" ]
為了能夠使用這個Docker檔案,我必須先建立一個應用程式:
$ go build -o app
然後再建立映象:
$ docker build . -f Dockerfile_1
Sending build context to Docker daemon 22.84MB
Step 1/4 : FROM golang:buster
---> Running in 62eb8791ace1
Removing intermediate container 62eb8791ace1
Step 3/4 : COPY app /app/
Step 4/4 : ENTRYPOINT [ "/app/app" ]
---> Running in 7853090f8c3b
Removing intermediate container 7853090f8c3b
Successfully built 0e3d3835a61b
$ docker build . -f Dockerfile_1
Sending build context to Docker daemon 22.84MB
Step 1/4 : FROM golang:buster
---> f8c6c6bf3e26
Step 2/4 : WORKDIR /app
---> Running in 62eb8791ace1
Removing intermediate container 62eb8791ace1
---> d586151d2813
Step 3/4 : COPY app /app/
---> 25b4f091cba7
Step 4/4 : ENTRYPOINT [ "/app/app" ]
---> Running in 7853090f8c3b
Removing intermediate container 7853090f8c3b
---> 0e3d3835a61b
Successfully built 0e3d3835a61b
$ docker build . -f Dockerfile_1
Sending build context to Docker daemon 22.84MB
Step 1/4 : FROM golang:buster
---> f8c6c6bf3e26
Step 2/4 : WORKDIR /app
---> Running in 62eb8791ace1
Removing intermediate container 62eb8791ace1
---> d586151d2813
Step 3/4 : COPY app /app/
---> 25b4f091cba7
Step 4/4 : ENTRYPOINT [ "/app/app" ]
---> Running in 7853090f8c3b
Removing intermediate container 7853090f8c3b
---> 0e3d3835a61b
Successfully built 0e3d3835a61b
我想啟動它,但我需要知道映象的名稱。我可以用 docker images
來找到它:
REPOSITORY TAG IMAGE ID CREATED SIZE
<none> <none> 0e3d3835a61b 48 seconds ago 739MB
excalidraw/excalidraw latest d6392f9c5191 2 days ago 34.8MB
golang buster f8c6c6bf3e26 4 days ago 720MB
moby/buildkit buildx-stable-1 4dc9f4d5bf89 2 weeks ago 168MB
slimdotai/dd-ext 0.8.2 56f11b815b6c 7 months ago 153MB
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
<none> <none> 0e3d3835a61b 48 seconds ago 739MB
excalidraw/excalidraw latest d6392f9c5191 2 days ago 34.8MB
golang buster f8c6c6bf3e26 4 days ago 720MB
moby/buildkit buildx-stable-1 4dc9f4d5bf89 2 weeks ago 168MB
slimdotai/dd-ext 0.8.2 56f11b815b6c 7 months ago 153MB
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
<none> <none> 0e3d3835a61b 48 seconds ago 739MB
excalidraw/excalidraw latest d6392f9c5191 2 days ago 34.8MB
golang buster f8c6c6bf3e26 4 days ago 720MB
moby/buildkit buildx-stable-1 4dc9f4d5bf89 2 weeks ago 168MB
slimdotai/dd-ext 0.8.2 56f11b815b6c 7 months ago 153MB
我可以看到映象的名稱是 <none>
。我可以用它來啟動容器:
$ docker run 0e3d3835a61b
exec /app/app: exec format error
$ docker run 0e3d3835a61b
exec /app/app: exec format error
$ docker run 0e3d3835a61b
exec /app/app: exec format error
會發生什麼?回到Dockerfile_1,看一下它。這裡面有幾個問題:
- 我正在為OSX構建應用程式,但我想在Linux中執行它。
- 我沒有指定我使用的是哪個Go版本。在本地,我可以使用Go 1.16,但映象上有最新的Go版本(目前是1.20)。
- 我的應用程式使用9999埠,但我沒有公開它。
- 我的映象沒有名稱和版本。
多階段構建
為了解決第一個問題,我可以使用多階段構建。我將建立一個新的Dockerfile(Dockerfile_2):
FROM golang:${GO_VERSION}-buster as builder
FROM debian:buster as final
COPY --from=builder /app/app /app/
ENTRYPOINT [ "/app/app" ]
ARG GO_VERSION=1.20.3
FROM golang:${GO_VERSION}-buster as builder
WORKDIR /app
COPY . /app/
RUN go mod tidy
RUN go build -o app
FROM debian:buster as final
WORKDIR /app
COPY --from=builder /app/app /app/
EXPOSE ${PORT:-9999}
ENTRYPOINT [ "/app/app" ]
ARG GO_VERSION=1.20.3
FROM golang:${GO_VERSION}-buster as builder
WORKDIR /app
COPY . /app/
RUN go mod tidy
RUN go build -o app
FROM debian:buster as final
WORKDIR /app
COPY --from=builder /app/app /app/
EXPOSE ${PORT:-9999}
ENTRYPOINT [ "/app/app" ]
在新的Docker檔案中,我用 ARG
指令處理Go版本。你不一定要這樣做。你也可以對版本進行硬編碼。但有了 ARG
,你可以在構建映象時覆蓋它。
構建一個應用程式被移到第一個或 builder
階段。當應用程式構建完成後,它被複制到第二階段或 final
階段。在這兩個階段,我都使用Debian Buster。它是一個小的映像,對我的應用程式來說已經足夠了。我還暴露了一個埠,設定預設值為9999。
現在我可以建立映象了:
$ docker build . -t rnemet/echo:0.0.1 -f Dockerfile_2
Sending build context to Docker daemon 22.84MB
Step 1/11 : ARG GO_VERSION=1.20.3
Step 2/11 : FROM golang:${GO_VERSION}-buster as builder
1.20.3-buster: Pulling from library/golang
Digest: sha256:413cd9e04db86fee3f5c667de293f37d9199b74880771c37dcfeb165cefaf424
Status: Downloaded newer image for golang:1.20.3-buster
Step 5/11 : RUN go mod tidy
---> Running in 2657122aa7fe
go: downloading github.com/prometheus/client_golang v1.14.0
go: downloading github.com/rogpeppe/go-internal v1.8.0
Removing intermediate container 2657122aa7fe
Step 6/11 : RUN go build -o app
---> Running in 7e593ea7ffb4
Removing intermediate container 7e593ea7ffb4
Step 7/11 : FROM debian:buster
buster: Pulling from library/debian
4e2befb7f5d1: Already exists
Digest: sha256:235f2a778fbc0d668c66afa9fd5f1efabab94c1d6588779ea4e221e1496f89da
Status: Downloaded newer image for debian:buster
---> Running in a79e19ed4815
Removing intermediate container a79e19ed4815
Step 9/11 : COPY --from=builder /app/app /app/
Step 10/11 : EXPOSE ${PORT:-9999}
---> Running in e5bf1bc188b9
Removing intermediate container e5bf1bc188b9
Step 11/11 : ENTRYPOINT [ "/app/app" ]
---> Running in 421008b145ee
Removing intermediate container 421008b145ee
Successfully built 159ca8b29354
Successfully tagged rnemet/echo:0.0.1
$ docker build . -t rnemet/echo:0.0.1 -f Dockerfile_2
Sending build context to Docker daemon 22.84MB
Step 1/11 : ARG GO_VERSION=1.20.3
Step 2/11 : FROM golang:${GO_VERSION}-buster as builder
1.20.3-buster: Pulling from library/golang
Digest: sha256:413cd9e04db86fee3f5c667de293f37d9199b74880771c37dcfeb165cefaf424
Status: Downloaded newer image for golang:1.20.3-buster
---> f8c6c6bf3e26
Step 3/11 : WORKDIR /app
---> Using cache
---> d586151d2813
Step 4/11 : COPY . /app/
---> 331d288c0f19
Step 5/11 : RUN go mod tidy
---> Running in 2657122aa7fe
go: downloading github.com/prometheus/client_golang v1.14.0
...snip...
go: downloading github.com/rogpeppe/go-internal v1.8.0
Removing intermediate container 2657122aa7fe
---> 48197d27f8ab
Step 6/11 : RUN go build -o app
---> Running in 7e593ea7ffb4
Removing intermediate container 7e593ea7ffb4
---> d086687f4f17
Step 7/11 : FROM debian:buster
buster: Pulling from library/debian
4e2befb7f5d1: Already exists
Digest: sha256:235f2a778fbc0d668c66afa9fd5f1efabab94c1d6588779ea4e221e1496f89da
Status: Downloaded newer image for debian:buster
---> 4591634d6289
Step 8/11 : WORKDIR /app
---> Running in a79e19ed4815
Removing intermediate container a79e19ed4815
---> b316081e2c13
Step 9/11 : COPY --from=builder /app/app /app/
---> 6fdc4f84223f
Step 10/11 : EXPOSE ${PORT:-9999}
---> Running in e5bf1bc188b9
Removing intermediate container e5bf1bc188b9
---> 8da39c1270c4
Step 11/11 : ENTRYPOINT [ "/app/app" ]
---> Running in 421008b145ee
Removing intermediate container 421008b145ee
---> 159ca8b29354
Successfully built 159ca8b29354
Successfully tagged rnemet/echo:0.0.1
$ docker build . -t rnemet/echo:0.0.1 -f Dockerfile_2
Sending build context to Docker daemon 22.84MB
Step 1/11 : ARG GO_VERSION=1.20.3
Step 2/11 : FROM golang:${GO_VERSION}-buster as builder
1.20.3-buster: Pulling from library/golang
Digest: sha256:413cd9e04db86fee3f5c667de293f37d9199b74880771c37dcfeb165cefaf424
Status: Downloaded newer image for golang:1.20.3-buster
---> f8c6c6bf3e26
Step 3/11 : WORKDIR /app
---> Using cache
---> d586151d2813
Step 4/11 : COPY . /app/
---> 331d288c0f19
Step 5/11 : RUN go mod tidy
---> Running in 2657122aa7fe
go: downloading github.com/prometheus/client_golang v1.14.0
...snip...
go: downloading github.com/rogpeppe/go-internal v1.8.0
Removing intermediate container 2657122aa7fe
---> 48197d27f8ab
Step 6/11 : RUN go build -o app
---> Running in 7e593ea7ffb4
Removing intermediate container 7e593ea7ffb4
---> d086687f4f17
Step 7/11 : FROM debian:buster
buster: Pulling from library/debian
4e2befb7f5d1: Already exists
Digest: sha256:235f2a778fbc0d668c66afa9fd5f1efabab94c1d6588779ea4e221e1496f89da
Status: Downloaded newer image for debian:buster
---> 4591634d6289
Step 8/11 : WORKDIR /app
---> Running in a79e19ed4815
Removing intermediate container a79e19ed4815
---> b316081e2c13
Step 9/11 : COPY --from=builder /app/app /app/
---> 6fdc4f84223f
Step 10/11 : EXPOSE ${PORT:-9999}
---> Running in e5bf1bc188b9
Removing intermediate container e5bf1bc188b9
---> 8da39c1270c4
Step 11/11 : ENTRYPOINT [ "/app/app" ]
---> Running in 421008b145ee
Removing intermediate container 421008b145ee
---> 159ca8b29354
Successfully built 159ca8b29354
Successfully tagged rnemet/echo:0.0.1
現在我可以看到映象有名稱和版本:
REPOSITORY TAG IMAGE ID CREATED SIZE
rnemet/echo 0.0.1 159ca8b29354 4 minutes ago 133MB
<none> <none> d086687f4f17 4 minutes ago 1.17GB
<none> <none> 0e3d3835a61b 40 minutes ago 739MB
excalidraw/excalidraw latest d6392f9c5191 2 days ago 34.8MB
golang 1.20.3-buster f8c6c6bf3e26 5 days ago 720MB
golang buster f8c6c6bf3e26 5 days ago 720MB
moby/buildkit buildx-stable-1 4dc9f4d5bf89 2 weeks ago 168MB
debian buster 4591634d6289 2 weeks ago 114MB
slimdotai/dd-ext 0.8.2 56f11b815b6c 7 months ago 153MB
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
rnemet/echo 0.0.1 159ca8b29354 4 minutes ago 133MB
<none> <none> d086687f4f17 4 minutes ago 1.17GB
<none> <none> 0e3d3835a61b 40 minutes ago 739MB
excalidraw/excalidraw latest d6392f9c5191 2 days ago 34.8MB
golang 1.20.3-buster f8c6c6bf3e26 5 days ago 720MB
golang buster f8c6c6bf3e26 5 days ago 720MB
moby/buildkit buildx-stable-1 4dc9f4d5bf89 2 weeks ago 168MB
debian buster 4591634d6289 2 weeks ago 114MB
slimdotai/dd-ext 0.8.2 56f11b815b6c 7 months ago 153MB
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
rnemet/echo 0.0.1 159ca8b29354 4 minutes ago 133MB
<none> <none> d086687f4f17 4 minutes ago 1.17GB
<none> <none> 0e3d3835a61b 40 minutes ago 739MB
excalidraw/excalidraw latest d6392f9c5191 2 days ago 34.8MB
golang 1.20.3-buster f8c6c6bf3e26 5 days ago 720MB
golang buster f8c6c6bf3e26 5 days ago 720MB
moby/buildkit buildx-stable-1 4dc9f4d5bf89 2 weeks ago 168MB
debian buster 4591634d6289 2 weeks ago 114MB
slimdotai/dd-ext 0.8.2 56f11b815b6c 7 months ago 153MB
而且我可以執行這個容器:
$ docker run rnemet/echo:0.0.1
2021/12/05 20:56:05 Starting server on port 9999
$ docker run rnemet/echo:0.0.1
2021/12/05 20:56:05 Starting server on port 9999
$ docker run rnemet/echo:0.0.1
2021/12/05 20:56:05 Starting server on port 9999
如果你想覆蓋Go版本,你可以這樣做:
$ docker build . -t rnemet/echo:0.0.1 -f Dockerfile_2 --build-arg GO_VERSION=1.16.10
$ docker build . -t rnemet/echo:0.0.1 -f Dockerfile_2 --build-arg GO_VERSION=1.16.10
$ docker build . -t rnemet/echo:0.0.1 -f Dockerfile_2 --build-arg GO_VERSION=1.16.10
分層和快取
再看一下Dockerfile_2。Dockerfile中的每個條目都建立了一個新的層,每個層都被快取了。如果你改變了Dockerfile中的內容,Docker將重建被改變的層和所有後續層。
看一下 docker build
命令的輸出:
Sending build context to Docker daemon 22.84MB
Step 1/11 : ARG GO_VERSION=1.20.3
Step 2/11 : FROM golang:${GO_VERSION}-buster as builder
1.20.3-buster: Pulling from library/golang
Digest: sha256:413cd9e04db86fee3f5c667de293f37d9199b74880771c37dcfeb165cefaf424
Status: Downloaded newer image for golang:1.20.3-buster
---> Using cache <=== here cache is used
Step 5/11 : RUN go mod tidy
---> Running in 2657122aa7fe
go: downloading github.com/prometheus/client_golang v1.14.0
go: downloading github.com/rogpeppe/go-internal v1.8.0
Removing intermediate container 2657122aa7fe
Sending build context to Docker daemon 22.84MB
Step 1/11 : ARG GO_VERSION=1.20.3
Step 2/11 : FROM golang:${GO_VERSION}-buster as builder
1.20.3-buster: Pulling from library/golang
Digest: sha256:413cd9e04db86fee3f5c667de293f37d9199b74880771c37dcfeb165cefaf424
Status: Downloaded newer image for golang:1.20.3-buster
---> f8c6c6bf3e26
Step 3/11 : WORKDIR /app
---> Using cache <=== here cache is used
---> d586151d2813
Step 4/11 : COPY . /app/
---> 331d288c0f19
Step 5/11 : RUN go mod tidy
---> Running in 2657122aa7fe
go: downloading github.com/prometheus/client_golang v1.14.0
...snip...
go: downloading github.com/rogpeppe/go-internal v1.8.0
Removing intermediate container 2657122aa7fe
---> 48197d27f8ab
Sending build context to Docker daemon 22.84MB
Step 1/11 : ARG GO_VERSION=1.20.3
Step 2/11 : FROM golang:${GO_VERSION}-buster as builder
1.20.3-buster: Pulling from library/golang
Digest: sha256:413cd9e04db86fee3f5c667de293f37d9199b74880771c37dcfeb165cefaf424
Status: Downloaded newer image for golang:1.20.3-buster
---> f8c6c6bf3e26
Step 3/11 : WORKDIR /app
---> Using cache <=== here cache is used
---> d586151d2813
Step 4/11 : COPY . /app/
---> 331d288c0f19
Step 5/11 : RUN go mod tidy
---> Running in 2657122aa7fe
go: downloading github.com/prometheus/client_golang v1.14.0
...snip...
go: downloading github.com/rogpeppe/go-internal v1.8.0
Removing intermediate container 2657122aa7fe
---> 48197d27f8ab
我的目標是編寫基本相同的圖層。這樣一來,我就可以使用快取,更快地建立映象。在第4步,我把所有檔案從我的本地目錄複製到映象上。乍一看,這的確有道理。但是,如果我改變了一個README檔案,或者任何其他與應用程式無關的檔案,我將重建整個映象。這就不妙了。所以,我要麼指定複製什麼,要麼不復制什麼。
對於第二個選擇,我可以使用 .dockerignore
檔案。它類似於 .gitignore
檔案。它包含一個不應該被複制到映象中的檔案列表:
.gitignore
.dockerignore
**/compose*
Dockerfile
License
Makefile
Readme.md
.gitignore
.dockerignore
**/compose*
Dockerfile
License
Makefile
Readme.md
那麼 COPY . /app/
將只複製檔案,不在 .dockerignore
檔案中。
讓我們再考慮一件事。在第5步,我正在執行 go mod tidy
。它下載了所有的依賴項。這些依賴項並不經常改變。當它們被改變時,我應該重建這個應用程式。對於Go應用程式來說,下載依賴項並不是一個大問題,但對於其他語言來說,這可能是一個問題(想想NodeJS)。所以,讓我們先處理依賴關係,然後再複製原始碼。這樣一來,我就用一個快取來處理依賴關係,而不是在每次改變原始碼時都重建它們。
FROM golang:${GO_VERSION}-buster as builder
COPY --from=builder /app/app /app/
ENTRYPOINT [ "/app/app" ]
ARG GO_VERSION=1.20.3
FROM golang:${GO_VERSION}-buster as builder
WORKDIR /app
COPY go.mod go.sum /app/
RUN go mod download -x
COPY . /app/
RUN go build -o app
FROM debian:buster
WORKDIR /app
COPY --from=builder /app/app /app/
EXPOSE ${PORT:-9999}
ENTRYPOINT [ "/app/app" ]
ARG GO_VERSION=1.20.3
FROM golang:${GO_VERSION}-buster as builder
WORKDIR /app
COPY go.mod go.sum /app/
RUN go mod download -x
COPY . /app/
RUN go build -o app
FROM debian:buster
WORKDIR /app
COPY --from=builder /app/app /app/
EXPOSE ${PORT:-9999}
ENTRYPOINT [ "/app/app" ]
當最初執行 docker build . -t rnemet/echo:0.0.1 -f Dockerfile_3
會花一些時間來下載依賴項。因為我使用了選項 -x
,我可以看到所有下載的依賴項。如果你覺得麻煩,你可以刪除 -x
選項。如果你重新執行它,它將會快得多。而且,你會注意到,依賴關係是被快取的。
如果你改變了原始碼,依賴項就不會被再次下載。所以構建映象的速度會快很多。
自己試試吧。比較Dockerfile_2和Dockerfile_3的構建時間。
遠端快取
在使用CI/CD時,你要麼依靠CI/CD快取的實現,要麼依靠遠端快取。遠端快取是一個儲存在遠端位置的快取,因此,你可以用它來加快構建速度,在不同的機器和不同的使用者之間共享。
為此,我不得不使用BuildKit。它是Docker的一個新的構建工具箱。你可以像這樣使用它:
docker buildx build -t rnemet/echo:0.0.1 . -f Dockerfile_3 --cache-to type=registry,ref=rnemet/echo:test --cache-from type=registry,ref=rnemet/echo:test --cache-from type=registry,ref=rnemet/echo:main [--push|--load]
docker buildx build -t rnemet/echo:0.0.1 . -f Dockerfile_3 --cache-to type=registry,ref=rnemet/echo:test --cache-from type=registry,ref=rnemet/echo:test --cache-from type=registry,ref=rnemet/echo:main [--push|--load]
docker buildx build -t rnemet/echo:0.0.1 . -f Dockerfile_3 --cache-to type=registry,ref=rnemet/echo:test --cache-from type=registry,ref=rnemet/echo:test --cache-from type=registry,ref=rnemet/echo:main [--push|--load]
如果你想使用遠端快取,請指定 --cache-to
和 --cache-from
選項。選項 --cache-to
指定了儲存快取的位置。選項 --cache-from
指定從哪裡獲得快取。你可以為這兩個選項指定多個位置。如果你為 --cache-from
指定了多個位置,它將嘗試從所有的位置獲取快取。如果它在其中一個地方找到了快取,它就會使用它。
一個好的做法是為分支和主幹建立一個快取。在上面的例子中,我有 test
和 main
兩個分支。我把 test
分支用於測試, main
用於生產。所以,我為這兩個分支都建立了快取。如果我正在建立一個 test
分支,它將嘗試從 test
分支獲取快取,如果失敗,它將嘗試從 main
分支獲取快取。
如果你想把映象推送到登錄檔,使用 --push
選項。如果你要把映象載入到你的本地機器上,你可以使用 --load
選項。
總結
在這篇文章中,我向你展示瞭如何構建Dockerfile以加快構建過程。我希望你覺得這篇文章對你有幫助。
參考文獻
評論留言