摘要:比如和指令,鏡像中的文件內容被檢查并且為每個文件計算校驗和。這些文件的最終修改和訪問時間將不被考慮到校驗和內。在查找緩存期間,校驗和將被用于與已存在的鏡像校驗和進行對比。
Docker 可以從 Dockerfile 中讀取指令自動構建鏡像,Dockerfile是一個包含構建指定鏡像所有命令的文本文件。Docker堅持使用特定的格式并且使用特定的命令。你可以在 Dockerfile參考 頁面學習基本知識。如果你剛接觸Dockerfile 你應該從哪里開始學習。
這個文檔囊括了Docker公司和Docker社區(qū)推薦的創(chuàng)建易于使用且實用的Dockerfile 的最佳實踐和方法。我們強烈建議你遵循這些規(guī)范(事實上,如果你創(chuàng)建一個官方鏡像,你必須堅持這些實踐。)
你可以從 buildpack-deps Dockerifle看到許多這種實踐和建議。
注:本文檔提到的Dockerfile命令的更詳細的解釋見Dockerfile參考 頁面。通用參考和建議 容器應該是臨時性的
從你的Dockerfile定義的鏡像啟動的容器應該盡可能短暫。這里的『短暫』我們是說它可以被停止和銷毀并且一個新容器的構建和替換可以絕對最小化的變更和配置下完成。你可能想看下 應用方法論的12個事實中進程 一節(jié)來了解以無狀態(tài)方式運行容器的動機。
使用 .dockerignore文件在大多數(shù)情況下,最好把Dockerfile放在一個空目錄里。然后,只把構建Dockerfile需要的文件追加到該目錄中。為了改進構建性能,你也可以增加一個.dockerignore 文件來排除文件和目錄。該文件支持與 .gitignore 類似的排除模式。更多創(chuàng)建.dockerignore信息,見 .dockerignore
避免安裝不需要的包為了減少復雜性,依賴,文件大小,和構建時間,你應該避免僅僅因為他們很好用而安裝一些額外或者不必要的包。例如,你不需要在一個數(shù)據(jù)庫鏡像中包含一個文本編輯器。
每個容器只關心一個問題解耦應用為多個容器使水平擴容和復用容器更容易。例如,一個web應用棧會包含3個獨立的容器,每個都有自己獨立的鏡像,以解耦的方式來管理web應用,數(shù)據(jù)庫。
你可能聽說過"一個容器一個進程"。這種說法有很好的意圖,一個容器應該有一個操作系統(tǒng)進程并非真的必要。除此之外,事實上現(xiàn)在容器可以 被init進程啟動, 一些程序可能會自己產(chǎn)生其他額外的進程。例如,Celery 可以產(chǎn)生多個工作進程,或者 Apache 可能為每個請求創(chuàng)建一個進程。當然"一個容器一個進程"通常是一個很好的經(jīng)驗法則,??但它不是一個很難和快速的規(guī)則(it is not a hard and fast rule)?? 用你最好的判斷來保持容器盡可能的干凈和模塊化。
如果容器之間相關依賴,你可以使用 Docker容器網(wǎng)絡 來取吧哦容器之間可以通信。
最小化層數(shù)你需要在Dockerfile可讀性(從而可以長時間維護)和它用的層數(shù)最小化之間找到平衡。Be strategic 關注你使用的層數(shù)(and cautious about the number of layers you use).
對多行參數(shù)排序無論何時,以排序多行參數(shù)來緩解以后的變化(Whenever possible, ease later changes by sorting multi-line arguments alphanumerically. )。這將幫助你避免重復的包并且使里列表更容易更新。這也使得PR更容易閱讀和審查。在反斜線()前加一個空格也很有幫助。
這里有個來自 buildpack-deps 鏡像的實例:
RUN apt-get update && apt-get install -y bzr cvs git mercurial subversion構建緩存
在構建鏡像的過程中,Docker會逐句讀取你Dockerfile中的指令按指定的順序執(zhí)行。因為每個指令都會被檢查Docker會在它的緩存中查找可以重用的現(xiàn)有鏡像(As each instruction is examined Docker will look for an existing image in its cache that it can reuse),而不是創(chuàng)建一個新的(重復的)鏡像。如果你根本不像使用緩存,你可以對 docker build 命令使用 --no-cache=ture參數(shù)。
然而,如果你使Docker使用緩存,那么理解它什么時候找到一個匹配的鏡像以及什么不找就非常重要了。Docker將遵循的基本規(guī)則如下:
以一個已經(jīng)在緩存中的付鏡像開始,下一個指令與所有源自該基礎鏡像的子鏡像做對比,來查看鏡像中是否有一個使用了完全相同的鏡像構建。如果沒有,緩存不可用。
大多數(shù)情況下簡單對比Dockfile中的指令與子鏡像就足夠了。然而,一些特定的指令需要更多的檢查和解釋。
比如ADD和COPY指令,鏡像中的文件內容被檢查并且為每個文件計算校驗和。這些文件的最終修改和訪問時間將不被考慮到校驗和內。在查找緩存期間,校驗和將被用于與已存在的鏡像校驗和進行對比。如果文件中有任何變化,比如內容或者元數(shù)據(jù),那么緩存失效。
除了ADD和COPY命令以外,緩存檢查將不會檢查容器中的文件來確定緩存匹配。比如,當處理一個RUN apt-get -y update容器中的文件更新將不會被檢查來確定是否命中已存在緩存。在這種情況下只有命令字符串自己將被用來查找匹配。
一旦緩存失效,所有的后面的Dockerfile命令將會生成新的鏡像而且不會使用緩存。
Dcokerfile指令下面你會找到寫Dockerfile里可用的各種指令的建議以及最佳方法。
FROMDockerfile參考之FROM指令
無論何時只要可能使用當前官方倉庫鏡像作為你的基礎鏡像。我們推薦Debian鏡像, 因為它被嚴格控制并且保持最?。壳靶∮?MB),同時是一個完整的發(fā)行版。
LABEL理解labels對象
你可以給你的鏡像增加標簽(labels)來協(xié)助通過項目組織鏡像,記錄授權信息,幫助自動化,或者其他原因。每一個標簽都以LABEL開頭并且跟著一對或多對鍵值對。以下實例展示了可接受的不同格式。解釋性意見也包括在內(Explanatory comments are included inline.)。
注:如果你的字符串包含空格,它必須被引號引起來或者空格必須被轉義。如果你的字符串包含內部引號字符("),他們需要轉義。
# Set one or more individual labels LABEL com.example.version="0.0.1-beta" LABEL vendor="ACME Incorporated" LABEL com.example.release-date="2015-02-12" LABEL com.example.version.is-production="" # Set multiple labels on one line LABEL com.example.version="0.0.1-beta" com.example.release-date="2015-02-12" # Set multiple labels at once, using line-continuation characters to break long lines LABEL vendor=ACME Incorporated com.example.is-beta= com.example.is-production="" com.example.version="0.0.1-beta" com.example.release-date="2015-02-12"
查看 理解labels對象 獲取可接受的標簽鍵和值指導。
For information about querying labels, refer to the items related to filtering in Managing labels on objects.
Dockerfile參考 之 RUN 指令
跟之前一樣,為了讓你的Dockerfile具有更高的可讀性,更易于理解和維護,使用反斜線()將較長的或者復雜的RUN語句拆分為多行。
APT-GET可能RUN最常見的使用場景就是apt-get的應用程序了。RUN apt-get命令,因為使用它安裝軟件包有幾個需要注意的問題。
你應該避免使用RUN apt-get upgrade或者dis-upgrade, 因為父鏡像中許多"基本的"(essential)包不能在容器中升級。如果父鏡像中有個軟件包過期了,你應該聯(lián)系它的維護者。如果你知道有個特定的軟件包,foo,需要升級,使用apt-get install -y foo來自動升級。
通常把RUN apt-get update 和 apt-get install合并到一個相同的RUN語句中,例如:
RUN apt-get update && apt-get install -y package-bar package-baz package-foo
在一個RUN語句中多帶帶試用apt-get update會引起緩存問題并且導致后面的apt-get install指令執(zhí)行失敗。例如,你現(xiàn)在有個Dockerfile:
FROM ubuntu:14.04 RUN apt-get update RUN apt-get install -y curl
鏡像構建完成以后,所有的層都在Docker緩存中。假設你后來修改apt-get install增加了其他的軟件包:
FROM ubuntu:14.04 RUN apt-get update RUN apt-get install -y curl nginx
Docker將最初的指令和修改后的指令視為相同的指令(指apt-get update這行)并且使用上一步的緩存。結果就是apt-get update沒有執(zhí)行因為使用了緩存的版本進行構建。因為apt-get update沒有執(zhí)行,你的構建可能會安裝一個過時版本的curl和ngin。
使用RUN apt-get update && apt-get install -y可以確保你的Dockerfile安裝最新版本的軟件包而無需編碼或手動干預。這個技巧被稱為"緩存破解"。你也可以通過指定軟件包版本來破解緩存。這被稱為固定版本,例如:
RUN apt-get update && apt-get install -y package-bar package-baz package-foo=1.3.*
固定版本在構建時強制查找指定版本的軟件包而不管緩存有什么。這個技巧可以減少因為依賴包的未知變更導致的失敗。
下面是一個格式規(guī)范的RUN指令,實踐了apt-get的所有建議。
RUN apt-get update && apt-get install -y aufs-tools automake build-essential curl dpkg-sig libcap-dev libsqlite3-dev mercurial reprepro ruby1.9.1 ruby1.9.1-dev s3cmd=1.1.* && rm -rf /var/lib/apt/lists/*
s3cmd指令指定了版本1.1.*。 如果前一個鏡像使用了一個老版本,指定新版本會引起apt-get update的緩存破解以確保安裝新版本。每行列出一個軟件包可以避免包重復錯誤。
另外,你可以通過刪除 /var/lib/apt/lists 清理apt緩存來減小鏡像大小,因為apt緩存不會保存在層里。由于RUN語句以apt-get update開頭,所以在緩存apt-get之前,包緩存將始終被刷新。
注:Debian和Ubuntu的鏡像自動運行apt-get clean,所以不需要顯式調用。USING PIPES
一些RUN命令依賴使用管道符號(|)把一個命令的輸出到另外一個命令的能力,比如以下實例:
RUN wget -O - https://some.site | wc -l > /number
Docker試用/bin/sh -c解釋器執(zhí)行這些命令,它只計算管道最后一個操作的退出代碼來確定是否成功。在上面這個例子中只要wc -l命令執(zhí)行成功這一步就構建成功并且生成一個新的鏡像,即使wget命令失敗也是如此。
如果你想讓管道中出現(xiàn)任意錯誤命令都返回錯誤,在命令前加上set -o pipefail &&來確保避免出現(xiàn)未知錯誤時鏡像也能構建成功。例如:
RUN set -o pipefail && wget -O - https://some.site | wc -l > /number
注:并非所有的shell都支持-o pipefaile選項。在這種情況下(比如dash shell, 它是基于Debian鏡像的默認shell),考慮使用RUN的exec形式來顯式選擇一個支持pipefail選項的shell。例如:
RUN ["/bin/bash", "-c", "set -o pipefail && wget -O - https://some.site | wc -l > /number"]CMD
Dockerfile參考 之 CMD指令
CMD 指令用于運行你鏡像包含中的軟件,連同任意參數(shù)。CMD應該盡可能都是用這種形式 CMD [“executable”, “param1”, “param2”…]。然而,如果是一個作為服務的鏡像,比如Apache和Rails,你應該像這樣執(zhí)行CMD ["apache2","-DFOREGROUND"]。實際上,實際上,這種形式的指令是推薦用于任何基于服務的鏡像。
在其他大多數(shù)情況下,CMD應該給一個交互式Shell,比如bash,python 和 perl。例如,CMD ["perl", "-de0"], CMD ["python"], 或者 CMD [“php”, “-a”]。試用這種形式就意味著當你執(zhí)行類似docker run -it python的一些東西,你將得到一個可用的shell(you’ll get dropped into a usable shell, ready to go)。CMD應該很少以CMD [“param”, “param”]的形式和 ENTRYPOINT一起試用,除非你和你的目標用戶已經(jīng)非常熟悉ENTRYPOINT工作原理。
EXPOSEDockerfile參考之 EXPOSE 指令
EXPOSE指令指示容器將監(jiān)聽鏈接的端口。因此,你應該為你的應用程序試用通用的傳統(tǒng)的端口。例如,一個包含Apache Web服務器的鏡像應該EXPOSE 80, 而一個包含MongoDB的鏡像應該使用EXPOSE 27017等。
對于外部訪問,您的用戶可以使用指示如何將指定端口映射到所選端口的標志來執(zhí)行docker run。
???For container linking, Docker provides environment variables for the path from the recipient container back to the source (ie, MYSQL_PORT_3306_TCP).???
Dockerfile參考 之 ENV指令
為了讓軟件更便于運行,你可以使用ENV來修改環(huán)境變量將軟件安裝目錄加到PATH。例如:ENV PATH /usr/local/nginx/bin:$PATH將使 CMD [“nginx”] 可以工作。
ENV指令也可用于給要容器化的服務所需的環(huán)境變量,比如Postgre的PGDATA。
最后,ENV也可用于指定通用版本號,這樣版本易于維護,如下實例所示:
ENV PG_MAJOR 9.3 ENV PG_VERSION 9.3.4 RUN curl -SL http://example.com/postgres-$PG_VERSION.tar.xz | tar -xJC /usr/src/postgress && … ENV PATH /usr/local/postgres-$PG_MAJOR/bin:$PATH
和程序中的常量變量類似(和硬編碼值相反),這種方法讓你可以修改一個多帶帶的ENV指令在容器中自動更新容器中的軟件版本。
ADD or COPYDockerfile參考之 ADD指令
Dockerfile參考之 COPY指令
盡管ADD和COPY指令功能相似,一般而言,最好使用COPY。是因為它比ADD更透明。COPY只支持最基本的從本地復制文件到容器中,而ADD有更多功能(比如本地tar解壓和遠程URL支持)并不是即刻課件的。因此,用ADD最好的方式是本地tar文件自動提取到鏡像,比如:ADD rootfs.tar.xz /。
如果你有多個Dockerfile步驟在你的上下文使用不同的文件,多帶帶COPY他們,而不是一次復制所有。這將確保每一步的構建緩存(強制這一步重新運行)只有當它特定的依賴文件變化時失效。
例如:
COPY requirements.txt /tmp/ RUN pip install --requirement /tmp/requirements.txt COPY . /tmp/
結論就是如果把COPY . /tmp/放在RUN之前失效緩存更少。
因為鏡像大小很重要,使用ADD來獲取遠程URLs是強烈反對的;你應該使用curl和wget替代。這種方式你可以在解壓后不需要時刪除這些文件并且你不會在你的鏡像增加額外一層。例如,你應該避免這么做:
ADD http://example.com/big.tar.xz /usr/src/things/ RUN tar -xJf /usr/src/things/big.tar.xz -C /usr/src/things RUN make -C /usr/src/things all
并且以此種方式替代:
RUN mkdir -p /usr/src/things && curl -SL http://example.com/big.tar.xz | tar -xJC /usr/src/things && make -C /usr/src/things all
對于不需要ADD tar自動提取功能的其他項目(文件,目錄),應始終使用COPY。
ENTRYPOINTDockerfile參考 之 ENTRYPOINT指令
使用ENTRYPOINT最好的方式是設置鏡像主命令,允許鏡像把它作為命令運行(然后使用CMD作為默認標識)。
我們從一個命令行工具s3cmd鏡像的例子開始:
ENTRYPOINT ["s3cmd"] CMD ["--help"]
現(xiàn)在可以像這樣運行鏡像來顯示命令的幫助:
$ docker run s3cmd
或者使用正確的參數(shù)來執(zhí)行一個命令:
$ docker run s3cmd ls s3://mybucket
這樣有用,因為鏡像名稱可以復用為二進制文件的引用,如上面命令所示。
ENTRYPOINT指令也可以與輔助腳本組合使用,允許其以類似于上述命令的方式運行,即使啟動工具可能需要多于一個步驟。
例如,Postgres官方鏡像 使用以下腳本作為它的 ENTRYPOINT:
#!/bin/bash set -e if [ "$1" = "postgres" ]; then chown -R postgres "$PGDATA" if [ -z "$(ls -A "$PGDATA")" ]; then gosu postgres initdb fi exec gosu postgres "$@" fi exec "$@"
注:這個腳本使用exec Bash命令 以便最終運行的應用程序成為容器PID 1。這樣做允許應用程序接受發(fā)送給容器的Unix信號。查看ENTRYPOINT幫助獲取更多細節(jié)。
幫助腳本被拷貝到容器并且當容器啟動時通過ENTRYPOINT運行。
COPY ./docker-entrypoint.sh / ENTRYPOINT ["/docker-entrypoint.sh"]
此腳本允許用戶以多種方式與Postgres進行交互。
它可以簡單的啟動Postgres:
$ docker run postgres
或者,可以運行Postgres并且傳遞參數(shù)給服務器:
$ docker run postgres postgres --help
最后,它也可以被用于啟動一個完全不同的工具,比如Bash:
$ docker run --rm -it postgres bashVOLUME
Dockerfile參考 之 VOLUME指令
VOLUME指令應該用于暴露任意數(shù)據(jù)庫存儲區(qū),配置存儲,或者docker容器創(chuàng)建的文件/目錄等。強烈建議您將VOLUME用于鏡像的任何可變和/或用戶可維護的部分。
USERDockerfile參考 之 USER指令
如果服務可以沒有權限運行,使用USER變?yōu)橐粋€非root用戶。像如下命令一樣開始在Dockerfile中創(chuàng)建用戶和組:
RUN groupadd -r postgres && useradd --no-log-init -r -g postgres postgres
注:??? (Users and groups in an image get a non-deterministic UID/GID in that the “next” UID/GID gets assigned regardless of image rebuilds. )???所以,如果很重要的話,你需要顯式指定UID/GID。注:由于Go存檔/ tar包處理稀疏文件中的一個未解決的bug, 在docker容器里創(chuàng)建一個UID足夠大的用戶會在容器層中將/var/log/faillog寫滿NUL (