建立儘量小的 Docker 容器

注:本文由 Adriaan de Jonge 編寫,本文的原文地址爲 Create The Smallest Possible Docker Containerlinux

當咱們在使用 Docker 的時候,你會很快注意到你正在下載不少 MB 做爲你的預先配置的容器。一個簡單的 Ubuntu 容器很容易超過 200 MB,而且隨着在上面安裝軟件,尺寸在逐漸增大。在某些狀況下,你不須要任何事情都使用 Ubuntu 。例如,若是你只是簡單的想運行一個 web 服務,使用 GO 編寫的,沒有必要圍繞它使用任何工具。git

我一直在尋找儘量小的容器入手,而且發現了一個:github

docker pull scratch

scratch 鏡像是完美的,真正的完美!它簡潔,小巧以及快速。它不包含任何 bug,安全泄漏,慢的代碼或是技術債務。這是由於它是一個空的鏡像。除了一點由 Docker 加入的元數據。事實上,你可使用以下命令按照 Docker 文檔描述的那樣建立一個本身的 scratch 鏡像。golang

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

因此這可能就是最小的 Docker 鏡像。web

或者咱們能夠說說關於這個的更多東西?好比,你怎樣使用 scratch 鏡像。這給本身帶來了一些挑戰。docker

爲 scratch 鏡像建立內容

咱們能夠在一個空鏡像中運行什麼?一個沒有依賴的可執行程序。你是否有沒有依賴的可執行程序?shell

我過去經常使用 Python,Java 和 Javascript 編寫代碼。每個這樣的語言/平臺都須要一個運行時的安裝。最近,我開始涉及 Go(或是 golang 若是你喜歡)平臺。看起來 Go 是靜態鏈接的。所以我嘗試編譯一個簡單的 web 服務輸出 Hello World 而且運行在 scratch 容器中。下面是這個 Hello World web 服務的代碼:安全

package main

import (
    "fmt"
    "net/http"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Hello World from Go in minimal Docker container")
}

func main() {
    http.HandleFunc("/", helloHandler)

    fmt.Println("Started, serving at 8080")
    err := http.ListenAndServe(":8080", nil)
    if err != nil {
        panic("ListenAndServe: " + err.Error())
    }
}

明顯地,我不能在 scratch 容器中編譯個人 web 服務,由於容器中沒有 Go 編譯器。正如我在 Mac 上工做,我也沒法編譯 Linux 的二進制文件同樣(實際上,是能夠在不一樣的平臺上交叉編譯 Go 的源碼的,但這會在另一篇博客中介紹)。bash

所以,我首先須要一個有 Go 編譯器的 Docker 容器。讓咱們開始:服務器

docker run -ti google/golang /bin/bash

在這個容器裏面,我能夠構建一個 Web 服務,經過我已經提交到一個 GitHub 倉庫的代碼。

go get github.com/adriaandejonge/helloworld

go get 命令是 go build 命令的變種,運行獲取和構建遠程的依賴。你能夠運行可執行的結果:

$GOPATH/bin/helloworld

它工做了,可是這不是咱們想要的。咱們須要 hello world 容器運行在 scratch 容器裏面。所以,實際上,咱們須要一個 Dockerfile :

FROM scratch
ADD bin/helloworld /helloworld
CMD ["/helloworld"]

而後啓動它,不幸的是,咱們開始 google/golang 容器的這個方法, 沒有辦法構建這個 Dockerfile 。所以,首先,咱們須要一種方法從這個容器內部訪問到 Docker。

從 Docker 內部調用 Docker

當你使用 Dokcer 時,你早晚會遇到須要從 Docker 內部訪問 Docker。能夠有多種方法實現它。你可使用遞歸和在 Docker 中運行 Docker。儘管如此,這樣看起來會很複雜而且致使容器很大。你還可使用一些額外的命令選項在實例外訪問 Docker 服務器:

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

在你繼續前,你從新運行 Go 編譯器,因爲在重啓動過程當中 Docker 忘記了咱們之前編譯過。

go get github.com/adriaandejonge/helloworld

當咱們啓動這個容器, -v 參數在 Docker 容器中建立一個卷而且容許你從 Docker 的機器提供一個文件做爲輸入。/var/run/docker.sock 是 UNIX socket,經過這個容許你訪問 Docker 服務。 (which docker) 部分是一個很是聰明的方法,它提供了一個在 容器中的 Docker 可執行文件的路徑,而不是硬編碼。儘管如此,當你在 Mac 上經過 boot2docker 使用這個命令的時候須要當心。若是 Docker 的可執行文件與 boot2docker 虛擬機的在不一樣的位置,將致使不匹配。所以,你或許想使用 /usr/local/bin/docker 硬編碼的方式替換 $(which docker),若是你運行在不一樣的系統,/var/run/docker.sock 有在不一樣位置的機會,你須要作相應的調整。

如今你能夠在 google/golang 容器的 $GOPATH 目錄使用 Dockerfile ,在這個示例中指向 /gopath。實際上,我已經在 github 上檢查過了這個 Dockerfile,所以,你能夠從 Go build 目錄複製它到所需的位置,像這樣:

cp $GOPATH/src/github.com/adriaandejonge/helloworld/Dockerfile $GOPATH

你須要複製這個做爲二進制的編譯文件,如今位於 $GOPATH/bin,而且它不可能從父目錄包含文件當構建一個 Dockerfile 的時候。所以複製後,下一步是:

docker build -t adejonge/helloworld $GOPATH

全部的都完成之後, Docker 給出以下響應:

Successfully built 6ff3fd5a381d

容許你運行這個容器:

docker run -ti --name hellobroken adejonge/helloworld

可是不幸的是, Docker 此次響應以下:

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

那麼究竟是怎麼回事?咱們在 scratch 容器中有可執行的靜態連接。難道咱們犯了一個錯誤?

事實證實,Go 不是靜態連接庫。或者至少不是全部的庫。在 Linux 下,咱們可使用 ldd 命令來看到動態連接庫:

ldd $GOPATH/bin/helloworld

獲得以下響應:

linux-vdso.so.1 => (0x00007fff039fe000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f61df30f000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f61def84000)
/lib64/ld-linux-x86-64.so.2 (0x00007f61df530000)

所以,在咱們運行咱們的 web 服務以前,我須要告訴 go 編譯器實際的靜態連接。

建立在 Go 中的可執行靜態連接

爲了建立可執行的靜態連接,咱們須要告訴 Go 使用 cgo 編譯器而不是 go 編譯器。命令以下:

CGO_ENABLED=0 go get -a -ldflags '-s' github.com/adriaandejonge/helloworld

CGO_ENABLED 環境變量告訴 Go 使用 cgo 編譯器而不是 go 編譯器。-a 參數告訴 GO 重薪構建全部的依賴。不然的話你將以動態連接依賴結束。最後的 -ldflags '-s' 參數是一個很是好的擴展。它大概下降了可執行文件 50% 的文件大小。你也能夠不經過 cgo 使用這個。尺寸縮小是去除了調試信息的結果。

爲了肯定,運行 ldd 命令:

ldd $GOPATH/bin/helloworld

返回是:

not a dynamic executable

你也能夠從新運行步驟,圍繞着從 scratch 建立 Docker 容器的可執行文件。

docker build -t adejonge/helloworld $GOPATH

若是一切順利,Docker 將響應以下:

Successfully built 6ff3fd5a381d

容許你運行這個容器:

docker run -ti --name helloworld adejonge/helloworld

響應以下:

Started, serving at 8080

到目前爲止,有許多手動的步驟和不少錯誤的地方。讓咱們退出 google/golang 容器而且從周邊服務器繼續:

<Press Ctrl-C>
exit

你能夠檢查 Docker 容器和鏡像存在不存在:

docker ps -a
docker images -a

你可使用以下命令清理:

docker rm -f helloworld
docker rmi -f adejonge/helloworld

建立一個 Docker 容器來建立一個 Docker 容器

目前爲止,咱們花了那麼多步驟,咱們還能夠記錄在 Dockerfile 中而且 Docker 會爲咱們作這些工做:

FROM google/golang
RUN CGO_ENABLED=0 go get -a -ldflags '-s' github.com/adriaandejonge/helloworld
RUN cp /gopath/src/github.com/adriaandejonge/helloworld/Dockerfile /gopath
CMD docker build -t adejonge/helloworld gopath

我在 一個單獨的稱爲 adriaandejonge/hellobuild 的 GitHub 倉庫檢查了 Dockerfile。它可使用下面的命令構建:

docker build -t adejonge/hellobuild github.com/adriaandejonge/hellobuild

提供 -t 參數命名 adejonge/hellobuild 鏡像而且它的最新的隱式的標籤。這些名字讓你之後更容易去除鏡像。下一步,你可使用就像咱們在這篇文章前面看到的那樣提供一個參數從這個鏡像中建立一個容器:

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

提供 --name hellobuild 參數使得在運行後更容易移除容器。事實上,你能夠這樣作,由於運行這個命令後,你已經建立了一個 adejonge/helloworld 鏡像:

docker rm -f hellobuild
docker rmi -f adejonge/hellobuild

如今你能夠建立一個基於 adejonge/helloworld 鏡像的名爲 helloworld 的新容器,就像你之前作的那樣:

docker run -ti --name helloworld adejonge/helloworld

由於全部的這些步驟都是從相同的命令中運行,不須要在 Docker 中打開一個 bash shell 。你能夠把這些步驟添加進一個 bash 腳本,自動運行它,爲了使你方便,我已經把這些腳本加入了 hellobuild GitHub 倉庫

另外,若是你想嘗試一個儘量小的容器,可是又不想遵循博客中的步驟,你可使用我檢入進 Docker Hub repository 的預先構建好的鏡像。

docker pull adejonge/helloworld

使用 docker images -a ,你能夠看到大小是 3.6MB。固然,若是你成功建立一個比我使用 Go 編寫的 web 服務還小的可執行文件,你可使得它更小。使用 C 語言或者是彙編,你能夠這樣作到。儘管如此,你不可能使得它比 scratch 鏡像還小

擴展閱讀

相關文章
相關標籤/搜索