兩個奇技淫巧,將 Docker 映象體積減小 99%
原文連結:Docker Images : Part I - Reducing Image Size
對於剛接觸容器的人來說,他們很容易被自己構建的 Docker 映象體積嚇到,我只需要一個幾 MB 的可執行檔案而已,為何映象的體積會達到 1 GB
以上?本文將會介紹幾個奇技淫巧來幫助你精簡映象,同時又不犧牲開發人員和運維人員的操作便利性。本系列文章將分為三個部分:
第一部分著重介紹多階段構建(multi-stage builds),因為這是映象精簡之路至關重要的一環。在這部分內容中,我會解釋靜態連結和動態連結的區別,它們對映象帶來的影響,以及如何避免那些不好的影響。中間會穿插一部分對 Alpine
第二部分將會針對不同的語言來選擇適當的精簡策略,其中主要討論 Go
,同時也涉及到了 Java
,Node
,Python
,Ruby
和 Rust
。這一部分也會詳細介紹 Alpine 映象的避坑指南。什麼?你不知道 Alpine
映象有哪些坑?我來告訴你。
第三部分將會探討適用於大多數語言和框架的通用精簡策略,例如使用常見的基礎映象、提取可執行檔案和減小每一層的體積。同時還會介紹一些更加奇特或激進的工具,例如 Bazel
,Distroless
,DockerSlim
和 UPX
,雖然這些工具在某些特定場景下能帶來奇效,但大多情況下會起到反作用。
本文介紹第一部分。
1. 萬惡之源
我敢打賭,每一個初次使用自己寫好的程式碼構建 Docker 映象的人都會被映象的體積嚇到,來看一個例子。
讓我們搬出那個屢試不爽的 hello world
C 程式:
/* hello.c */
int main () {
puts("Hello, world!");
return 0;
}
並通過下面的 Dockerfile 構建映象:
FROM gcc
COPY hello.c .
RUN gcc -o hello hello.c
CMD ["./hello"]
然後你會發現構建成功的映象體積遠遠超過了 1 GB
。。。因為該映象包含了整個 gcc
映象的內容。
如果使用 Ubuntu
映象,安裝 C 編譯器,最後編譯程式,你會得到一個大概 300 MB
大小的映象,比上面的映象小多了。但還是不夠小,因為編譯好的可執行檔案還不到 20 KB
$ ls -l hello
-rwxr-xr-x 1 root root 16384 Nov 18 14:36 hello
類似地,Go 語言版本的 hello world
會得到相同的結果:
package main
import "fmt"
func main () {
fmt.Println("Hello, world!")
}
使用基礎映象 golang
構建的映象大小是 800 MB
,而編譯後的可執行檔案只有 2 MB
大小:
$ ls -l hello
-rwxr-xr-x 1 root root 2008801 Jan 15 16:41 hello
還是不太理想,有沒有辦法大幅度減少映象的體積呢?往下看。
為了更直觀地對比不同映象的大小,所有映象都使用相同的映象名,不同的標籤。例如:hello:gcc
,hello:ubuntu
,hello:thisweirdtrick
等等,這樣就可以直接使用命令 docker images hello
列出所有映象名為 hello 的映象,不會被其他映象所幹擾。
2. 多階段構建
要想大幅度減少映象的體積,多階段構建是必不可少的。多階段構建的想法很簡單:“我不想在最終的映象中包含一堆 C 或 Go 編譯器和整個編譯工具鏈,我只要一個編譯好的可執行檔案!”
多階段構建可以由多個 FROM
指令識別,每一個 FROM
語句表示一個新的構建階段,階段名稱可以用 AS
引數指定,例如:
FROM gcc AS mybuildstage
COPY hello.c .
RUN gcc -o hello hello.c
FROM ubuntu
COPY --from=mybuildstage hello .
CMD ["./hello"]
本例使用基礎映象 gcc
來編譯程式 hello.c
,然後啟動一個新的構建階段,它以 ubuntu
作為基礎映象,將可執行檔案 hello
從上一階段拷貝到最終的映象中。最終的映象大小是 64 MB
,比之前的 1.1 GB
減少了 95%
: