Docker優(yōu)秀實(shí)踐:5個(gè)方法精簡(jiǎn)你的鏡像
創(chuàng)建短生命周期容器
基于Dockerfile生成鏡像,使用這個(gè)鏡像生成的容器,我們要盡可能的縮短容器的生命周期。這里我的理解是,不要將容器當(dāng)做vm 來(lái)使用, 這個(gè)容器可以被停止或者銷(xiāo)毀, 然后可以根據(jù)設(shè)置和配置的變動(dòng)重新生成新的容器。
理解構(gòu)建上下文
當(dāng)你觸發(fā)docker build 命令時(shí),當(dāng)前目錄就被稱(chēng)為構(gòu)建上下文(build context)。默認(rèn)情況下 Dockerfile文件就在這個(gè)目錄下, 但是可以通過(guò) -f 參數(shù)來(lái)指定Dockerfile的位置。不管Dockerfile在哪里,當(dāng)前目錄中的所有文件和目錄都會(huì)作為構(gòu)建上下文發(fā)送到 docker daemon 進(jìn)程。
構(gòu)建上下文示例
創(chuàng)建一個(gè)目錄并且使用cd進(jìn)入該目錄。在hello文件中寫(xiě)”hello”,同時(shí)創(chuàng)建 Dockerfile文件并且cat hello文件。在當(dāng)前上下文(.)中構(gòu)建鏡像:
- mkdir myproject && cd myproject
- echo "hello" > hello
- echo -e "FROM busyboxnCOPY /hello /nRUN cat /hello" > Dockerfile
- docker build -t helloapp:v1 .
將Dockerfile 和 hello 文件移動(dòng)到另一個(gè)目錄中。并且再構(gòu)建一個(gè)鏡像(不使用上個(gè)鏡像構(gòu)建緩存)。使用-f來(lái)指定 Dockerfile 并且明確上下文目錄:
- mkdir -p dockerfiles context
- mv Dockerfile dockerfiles && mv hello context
- docker build --no-cache -t helloapp:v2 -f dockerfiles/Dockerfile context
在構(gòu)建過(guò)程中導(dǎo)入了不必要的文件將會(huì)導(dǎo)致更大的構(gòu)建上下文,從而會(huì)構(gòu)建出更大的鏡像。這會(huì)增加構(gòu)建鏡像的時(shí)間,拉取和上傳鏡像的時(shí)間以及容器的大小。當(dāng)你使用Dockerfile構(gòu)建鏡像時(shí),可通過(guò)如下信息查看你的構(gòu)建上下文的大小:
Sending build context to Docker daemon 187.8MB
使用.dockerignore 排除不需要加入鏡像的文件
有的時(shí)候我們會(huì)需要排除一些與我們構(gòu)建鏡像不相關(guān)的文件,這時(shí)候我們可以通過(guò)編寫(xiě).dockerignore在不改變代碼結(jié)構(gòu)的情況下達(dá)到這一目的。這個(gè)文件的實(shí)現(xiàn)方式與.gitignore很像,關(guān)于如何創(chuàng)建一個(gè).dockerignore,可以參考.dockerignore file
使用多階段構(gòu)建
multi-stage builds 技術(shù)可以大幅度減少最終鏡像的大小,而不是想辦法去減少構(gòu)建過(guò)程中的層級(jí)數(shù)和文件。
因?yàn)殓R像是在構(gòu)建過(guò)程最后階段生成的,因此我們可以通過(guò)leveraging build cache來(lái)最小化鏡像層。
舉個(gè)例子來(lái)說(shuō),如果構(gòu)建一個(gè)鏡像,這個(gè)鏡像有很多層,可以按照鏡像層的修改頻率來(lái)排序(就是將不經(jīng)常更新的層作為最底層,這樣可以復(fù)用構(gòu)建緩存):
- 安裝工具
- 安裝或者更新依賴(lài)
- 生成你的應(yīng)用
一個(gè) Go 應(yīng)用的 Dockerfile示例:
- FROM golang:1.11-alpine AS build
- # Install tools required for project
- # Run `docker build --no-cache .` to update dependencies
- RUN apk add --no-cache git
- RUN go get github.com/golang/dep/cmd/dep
- # List project dependencies with Gopkg.toml and Gopkg.lock
- # These layers are only re-built when Gopkg files are updated
- COPY Gopkg.lock Gopkg.toml /go/src/project/
- WORKDIR /go/src/project/
- # Install library dependencies
- RUN dep ensure -vendor-only
- # Copy the entire project and build it
- # This layer is rebuilt when a file changes in the project directory
- COPY . /go/src/project/
- RUN go build -o /bin/project
- # This results in a single layer image
- FROM scratch
- COPY --from=build /bin/project /bin/project
- ENTRYPOINT ["/bin/project"]
- CMD ["--help"]
不安裝不需要的包
為了減小鏡像的復(fù)雜度和大小, 我們應(yīng)當(dāng)避免安裝一些我們不需要的 packages。舉個(gè)例子來(lái)說(shuō),你不需要在數(shù)據(jù)庫(kù)鏡像中安裝文本編輯器。
應(yīng)用解耦
每個(gè)容器應(yīng)當(dāng)只含有一個(gè)應(yīng)用實(shí)例, 將多個(gè)應(yīng)用解耦至多個(gè)容器可以很方便的對(duì)應(yīng)用進(jìn)行水平擴(kuò)展,并且可以復(fù)用容器。舉個(gè)例子來(lái)說(shuō),一個(gè) web 應(yīng)用應(yīng)當(dāng)包含三個(gè)容器(web容器, 數(shù)據(jù)庫(kù)容器, 緩存容器),每一個(gè)容器對(duì)應(yīng)一個(gè)鏡像。
每個(gè)容器中限制只能有一個(gè)進(jìn)程是一個(gè)很好的經(jīng)驗(yàn)法則, 但這也不是一個(gè)硬性的規(guī)定。容器中的進(jìn)程不僅可以由 init 創(chuàng)建, 一些程序可能會(huì)額外的生成一些他們自己的進(jìn)程。比如, Celery會(huì)生成多個(gè) worker 進(jìn)程, Apache 對(duì)每一個(gè)請(qǐng)求創(chuàng)建一個(gè)進(jìn)程。
每種場(chǎng)景不一樣,規(guī)則也不一樣。但是應(yīng)該盡可能的保證我們的容器功能明確和模塊化。如果容器之間相互依賴(lài)(容器之間可能需要通信), 你可以使用Docker container networks 確保容器間通信。
減小鏡像層數(shù)
減少鏡像層數(shù)對(duì)于鏡像構(gòu)建非常重要。在更老的版本的 docker 中需要特別注意,現(xiàn)在通過(guò)下面的這些特性我們可以方便的對(duì)鏡像層數(shù)進(jìn)行限制:
- 只有 ONLY, COPY,ADD這三個(gè)命令增加層數(shù),其他的命令只會(huì)創(chuàng)建一些臨時(shí)的鏡像,并不會(huì)增加構(gòu)建的鏡像的層數(shù)
- 使用 multi-stage builds只拷貝真正需要的artifaces(制品) 到最終的鏡像。這可以使你在構(gòu)建過(guò)程中使用工具和打印調(diào)試信息,但不會(huì)增加最終的鏡像大小。
對(duì)多行參數(shù)排序
只要有可能, 將參數(shù)按照字母進(jìn)行排序是一種非常好的實(shí)踐,這種方式可以避免重復(fù)安裝包(特指apt-get命令),也可以是開(kāi)發(fā)人員更加容易的閱讀和審查。
下面是 buildpack-deps鏡像的例子 images:
- RUN apt-get update && apt-get install -y
- bzr
- cvs
- git
- mercurial
- subversion
借助構(gòu)建緩存
在構(gòu)建鏡像的時(shí)候,docker 會(huì)按照dockerfile中的指令順序來(lái)一次執(zhí)行。每一個(gè)指令被執(zhí)行的時(shí)候 docker 都會(huì)去緩存中檢查是否有已經(jīng)存在的鏡像可以復(fù)用,而不是去創(chuàng)建一個(gè)新的鏡像復(fù)制。
如果不想使用構(gòu)建緩存,可以使用docker build參數(shù)選項(xiàng)—no-cache=true來(lái)禁用構(gòu)建緩存。在使用鏡像緩存時(shí),要弄清楚緩存合適生效,何時(shí)失效。構(gòu)建緩存最基本規(guī)則如下:
- 如果引用的父鏡像在構(gòu)建緩存中,下一個(gè)命令將會(huì)和所有從該父進(jìn)程派生的子鏡像做比較,如果有子鏡像使用相同的命令,那么緩存命中,否則緩存失效。
- 在大部分情況下,通過(guò)比較Dockerfile中的指令和子鏡像已經(jīng)足夠了。但是有些指令需要進(jìn)一步的檢查。
- 對(duì)于ADD和COPY指令, 文件的內(nèi)容會(huì)被檢查,并且會(huì)計(jì)算每一個(gè)文件的校驗(yàn)碼。但是文件最近一次的修改和訪問(wèn)時(shí)間不在校驗(yàn)碼的考慮范圍內(nèi)。在構(gòu)建過(guò)程中,docker 會(huì)比對(duì)已經(jīng)存在的鏡像,只要有文件內(nèi)容和元數(shù)據(jù)發(fā)生變動(dòng),那么緩存就會(huì)失效。
- 除了ADD和COPY指令,鏡像緩存不會(huì)檢查容器中文件來(lái)判斷是否命中緩存。例如,在處理RUN apt-get -y update命令時(shí),不會(huì)檢查容器中的更新文件以確定是否命中緩存,這種情況下只會(huì)檢查命令字符串是否相同。