天天看點

建立盡可能小的 Docker 容器

當我們在使用 docker 的時候,你會很快注意到你正在下載下傳很多 mb 作為你的預先配置的容器。一個簡單的 ubuntu 容器很容易超過 200 mb,并且随着在上面安裝軟體,尺寸在逐漸增大。在某些情況下,你不需要任何事情都使用 ubuntu 。例如,如果你隻是簡單的想運作一個 web 服務,使用 go 編寫的,沒有必要圍繞它使用任何工具。

建立盡可能小的 Docker 容器

我一直在尋找盡可能小的容器入手,并且發現了一個:

<code>docker pull scratch</code>

<code>tar cv --files-from /dev/null | docker import - scratch</code>

是以這可能就是最小的 docker 鏡像。

或者我們可以說說關于這個的更多東西?比如,你怎樣使用 scratch 鏡像。這給自己帶來了一些挑戰。

<a target="_blank"></a>

我們可以在一個空鏡像中運作什麼?一個沒有依賴的可執行程式。你是否有沒有依賴的可執行程式?

我過去常常使用 python,java 和 javascript 編寫代碼。每一個這樣的語言/平台都需要一個運作時的安裝。最近,我開始涉及 go(或是 golang 如果你喜歡)平台。看起來 go 是靜态連接配接的。是以我嘗試編譯一個簡單的 web 服務輸出 hello world 并且運作在 scratch 容器中。下面是這個 hello world web 服務的代碼:

<code>package main</code>

<code></code>

<code>import (</code>

<code>"fmt"</code>

<code>"net/http"</code>

<code>)</code>

<code>func hellohandler(w http.responsewriter, r *http.request) {</code>

<code>fmt.fprintln(w, "hello world from go in minimal docker container")</code>

<code>}</code>

<code>func main() {</code>

<code>http.handlefunc("/", hellohandler)</code>

<code>fmt.println("started, serving at 8080")</code>

<code>err := http.listenandserve(":8080", nil)</code>

<code>if err != nil {</code>

<code>panic("listenandserve: " + err.error())</code>

明顯地,我不能在 scratch 容器中編譯我的 web 服務,因為容器中沒有 go 編譯器。正如我在 mac 上工作,我也無法編譯 linux 的二進制檔案一樣(實際上,是可以在不同的平台上交叉編譯 go 的源碼的,但這會在另外一篇部落格中介紹)。

是以,我首先需要一個有 go 編譯器的 docker 容器。讓我們開始:

<code>docker run -ti google/golang /bin/bash</code>

<code>go get github.com/adriaandejonge/helloworld</code>

go get 指令是 go build 指令的變種,運作擷取和建構遠端的依賴。你可以運作可執行的結果:

<code>$gopath/bin/helloworld</code>

它工作了,但是這不是我們想要的。我們需要 hello world 容器運作在 scratch 容器裡面。是以,實際上,我們需要一個 dockerfile :

<code>from scratch</code>

<code>add bin/helloworld /helloworld</code>

<code>cmd ["/helloworld"]</code>

然後啟動它,不幸的是,我們開始 google/golang 容器的這個方法, 沒有辦法建構這個 dockerfile 。是以,首先,我們需要一種方法從這個容器内部通路到 docker。

<code>docker run -v /var/run/docker.sock:/var/run/docker.sock -v $(which docker):$(which docker) -ti google/golang /bin/bash</code>

在你繼續前,你重新運作 go 編譯器,由于在重新開機動過程中 docker 忘記了我們以前編譯過。

當我們啟動這個容器, <code>-v</code> 參數在 docker 容器中建立一個卷并且允許你從 docker 的機器提供一個檔案作為輸入。<code>/var/run/docker.sock</code> 是 unix socket,通過這個允許你通路 docker 服務。 <code>(which docker)</code> 部分是一個非常聰明的方法,它提供了一個在 容器中的 docker 可執行檔案的路徑,而不是寫死。盡管如此,當你在 mac 上通過 boot2docker 使用這個指令的時候需要小心。如果 docker 的可執行檔案與 boot2docker 虛拟機的在不同的位置,将導緻不比對。是以,你或許想使用 <code>/usr/local/bin/docker</code> 寫死的方式替換 <code>$(which docker)</code>,如果你運作在不同的系統,<code>/var/run/docker.sock</code> 有在不同位置的機會,你需要做相應的調整。

現在你可以在 google/golang 容器的 $gopath 目錄使用 dockerfile ,在這個示例中指向 <code>/gopath</code>。實際上,我已經在 github 上檢查過了這個 <code>dockerfile</code>,是以,你可以從 go build 目錄複制它到所需的位置,像這樣:

<code>cp $gopath/src/github.com/adriaandejonge/helloworld/dockerfile $gopath</code>

你需要複制這個作為二進制的編譯檔案,現在位于 $gopath/bin,并且它不可能從父目錄包含檔案當建構一個 dockerfile 的時候。是以複制後,下一步是:

<code>docker build -t adejonge/helloworld $gopath</code>

所有的都完成以後, docker 給出如下響應:

<code>successfully built 6ff3fd5a381d</code>

允許你運作這個容器:

<code>docker run -ti --name hellobroken adejonge/helloworld</code>

但是不幸的是, docker 這次響應如下:

<code>2014/07/02 17:06:48 no such file or directory</code>

那麼到底是怎麼回事?我們在 scratch 容器中有可執行的靜态連結。難道我們犯了一個錯誤?

事實證明,go 不是靜态連結庫。或者至少不是所有的庫。在 linux 下,我們可以使用 ldd 指令來看到動态連結庫:

<code>ldd $gopath/bin/helloworld</code>

得到如下響應:

<code>linux-vdso.so.1 =&gt; (0x00007fff039fe000)</code>

<code>libpthread.so.0 =&gt; /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f61df30f000)</code>

<code>libc.so.6 =&gt; /lib/x86_64-linux-gnu/libc.so.6 (0x00007f61def84000)</code>

<code>/lib64/ld-linux-x86-64.so.2 (0x00007f61df530000)</code>

是以,在我們運作我們的 web 服務之前,我需要告訴 go 編譯器實際的靜态連結。

為了建立可執行的靜态連結,我們需要告訴 go 使用 cgo 編譯器而不是 go 編譯器。指令如下:

<code>cgo_enabled=0 go get -a -ldflags '-s' github.com/adriaandejonge/helloworld</code>

<code>cgo_enabled</code> 環境變量告訴 go 使用 cgo 編譯器而不是 go 編譯器。<code>-a</code> 參數告訴 go 重薪建構所有的依賴。否則的話你将以動态連結依賴結束。最後的 <code>-ldflags '-s'</code> 參數是一個非常好的擴充。它大概降低了可執行檔案 50% 的檔案大小。你也可以不通過 cgo 使用這個。尺寸縮小是去除了調試資訊的結果。

為了确定,運作 ldd 指令:

傳回是:

<code>not a dynamic executable</code>

你也可以重新運作步驟,圍繞着從 scratch 建立 docker 容器的可執行檔案。

如果一切順利,docker 将響應如下:

<code>docker run -ti --name helloworld adejonge/helloworld</code>

響應如下:

<code>started, serving at 8080</code>

到目前為止,有許多手動的步驟和很多錯誤的地方。讓我們退出 google/golang 容器并且從周邊伺服器繼續:

<code>&lt;press ctrl-c&gt;</code>

<code>exit</code>

你可以檢查 docker 容器和鏡像存在不存在:

<code>docker ps -a</code>

<code>docker images -a</code>

你可以使用如下指令清理:

<code>docker rm -f helloworld</code>

<code>docker rmi -f adejonge/helloworld</code>

目前為止,我們花了那麼多步驟,我們還可以記錄在 dockerfile 中并且 docker 會為我們做這些工作:

<code>from google/golang</code>

<code>run cgo_enabled=0 go get -a -ldflags '-s' github.com/adriaandejonge/helloworld</code>

<code>run cp /gopath/src/github.com/adriaandejonge/helloworld/dockerfile /gopath</code>

<code>cmd docker build -t adejonge/helloworld gopath</code>

<code>docker build -t adejonge/hellobuild github.com/adriaandejonge/hellobuild</code>

提供 <code>-t</code> 參數命名 adejonge/hellobuild 鏡像并且它的最新的隐式的标簽。這些名字讓你以後更容易去除鏡像。下一步,你可以使用就像我們在這篇文章前面看到的那樣提供一個參數從這個鏡像中建立一個容器:

<code>docker run -v /var/run/docker.sock:/var/run/docker.sock -v $(which docker):$(which docker) -ti --name hellobuild adejonge/hellobuild</code>

提供 <code>--name hellobuild</code> 參數使得在運作後更容易移除容器。事實上,你可以這樣做,因為運作這個指令後,你已經建立了一個 adejonge/helloworld 鏡像:

<code>docker rm -f hellobuild</code>

<code>docker rmi -f adejonge/hellobuild</code>

現在你可以建立一個基于 adejonge/helloworld 鏡像的名為 helloworld 的新容器,就像你以前做的那樣:

<code>docker pull adejonge/helloworld</code>

使用 docker images -a ,你可以看到大小是 3.6mb。當然,如果你成功建立一個比我使用 go 編寫的 web 服務還小的可執行檔案,你可以使得它更小。使用 c 語言或者是彙編,你可以這樣做到。盡管如此,你不可能使得它比 scratch 鏡像還小

<b>原文釋出時間為:2015-06-09</b>

<b>本文來自雲栖社群合作夥伴“linux中國”</b>