Dockerfile最佳實踐
Docker 可以從Dockerfile
中讀取指令自動構建映象,Dockerfile
是一個包含構建指定映象所有命令的文字檔案。Docker堅持使用特定的格式並且使用特定的命令。你可以在Dockerfile參考
頁面學習基本知識。如果你剛接觸Dockerfile 你應該從哪裡開始學習。
這個文件囊括了Docker
公司和Docker
社群推薦的建立易於使用且實用的Dockerfile
的最佳實踐和方法。我們強烈建議你遵循這些規範(事實上,如果你建立一個官方映象,你必須堅持這些實踐。)
你可以從buildpack-deps Dockerifle看到許多這種實踐和建議。
注:本文件提到的Dockerfile命令的更詳細的解釋見Dockerfile參考 頁面。
通用參考和建議
容器應該是臨時性的
從你的Dockerfile定義的映象啟動的容器應該儘可能短暫。這裡的『短暫』我們是說它可以被停止和銷燬並且一個新容器的構建和替換可以絕對最小化的變更和配置下完成。你可能想看下 應用方法論的12個事實中程序 一節來了解以無狀態方式執行容器的動機。
使用.dockerignore
檔案
在大多數情況下,最好把Dockerfile
放在一個空目錄裡。然後,只把構建Dockerfile
需要的檔案追加到該目錄中。為了改進構建效能,你也可以增加一個.dockerignore
檔案來排除檔案和目錄。該檔案支援與.gitignore
類似的排除模式。更多建立.dockerignore
資訊,見.dockerignore
避免安裝不需要的包
為了減少複雜性,依賴,檔案大小,和構建時間,你應該避免僅僅因為他們很好用而安裝一些額外或者不必要的包。例如,你不需要在一個數據庫映象中包含一個文字編輯器。
每個容器只關心一個問題
解耦應用為多個容器使水平擴容和複用容器更容易。例如,一個web應用棧會包含3個獨立的容器,每個都有自己獨立的映象,以解耦的方式來管理web應用,資料庫。
你可能聽說過"一個容器一個程序"。這種說法有很好的意圖,一個容器應該有一個作業系統程序並非真的必要。除此之外,事實上現在容器可以被init程序啟動 , 一些程式可能會自己產生其他額外的程序。例如,Celery 可以產生多個工作程序,或者Apache 可能為每個請求建立一個程序。當然"一個容器一個程序"通常是一個很好的經驗法則,??但它不是一個很難和快速的規則(it is not a hard and fast rule)?? 用你最好的判斷來保持容器儘可能的乾淨和模組化。
如果容器之間相關依賴,你可以使用Docker容器網路 來取吧哦容器之間可以通訊。
最小化層數
你需要在Dockerfile可讀性(從而可以長時間維護)和它用的層數最小化之間找到平衡。Be strategic 關注你使用的層數(and cautious about the number of layers you use).
對多行引數排序
無論何時,以排序多行引數來緩解以後的變化(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中的指令按指定的順序執行。因為每個指令都會被檢查Docker會在它的快取中查詢可以重用的現有映象(As each instruction is examined Docker will look for an existing image in its cache that it can reuse),而不是建立一個新的(重複的)映象。如果你根本不像使用快取,你可以對docker build
命令使用--no-cache=ture
引數。
然而,如果你使Docker使用快取,那麼理解它什麼時候找到一個匹配的映象以及什麼不找就非常重要了。Docker將遵循的基本規則如下:
- 以一個已經在快取中的付映象開始,下一個指令與所有源自該基礎映象的子映象做對比,來檢視映象中是否有一個使用了完全相同的映象構建。如果沒有,快取不可用。
- 大多數情況下簡單對比Dockfile中的指令與子映象就足夠了。然而,一些特定的指令需要更多的檢查和解釋。
-
比如
ADD
和COPY
指令,映象中的檔案內容被檢查並且為每個檔案計算校驗和。這些檔案的最終修改和訪問時間將不被考慮到校驗和內。在查詢快取期間,校驗和將被用於與已存在的映象校驗和進行對比。如果檔案中有任何變化,比如內容或者元資料,那麼快取失效。 -
除了
ADD
和COPY
命令以外,快取檢查將不會檢查容器中的檔案來確定快取匹配。比如,當處理一個RUN apt-get -y update
容器中的檔案更新將不會被檢查來確定是否命中已存在快取。在這種情況下只有命令字串自己將被用來查詢匹配。
一旦快取失效,所有的後面的Dockerfile命令將會生成新的映象而且不會使用快取。
Dcokerfile指令
下面你會找到寫Dockerfile裡可用的各種指令的建議以及最佳方法。
FROM
無論何時只要可能使用當前官方倉庫映象作為你的基礎映象。我們推薦Debian映象 , 因為它被嚴格控制並且保持最小(目前小於150mb),同時是一個完整的發行版。
LABEL
你可以給你的映象增加標籤(labels)來協助通過專案組織映象,記錄授權資訊,幫助自動化,或者其他原因。每一個標籤都以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 inManaging labels on objects .
RUN
跟之前一樣,為了讓你的Dockerfile
具有更高的可讀性,更易於理解和維護,使用反斜線()將較長的或者複雜的RUN語句拆分為多行。
APT-GET
可能RUN
最常見的使用場景就是apt-get
的應用程式了。RUN apt-get
命令,因為使用它安裝軟體包有幾個需要注意的問題。
你應該避免使用RUN apt-get upgrade
或者dis-upgrade
, 因為父映象中許多"基本的"(essential)包不能在容器中升級。如果父映象中有個軟體包過期了,你應該聯絡它的維護者。如果你知道有個特定的軟體包,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
指令執行失敗。例如,你現在有個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
沒有執行因為使用了快取的版本進行構建。因為apt-get update
沒有執行,你的構建可能會安裝一個過時版本的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.*
固定版本在構建時強制查詢指定版本的軟體包而不管快取有什麼。這個技巧可以減少因為依賴包的未知變更導致的失敗。
下面是一個格式規範的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
直譯器執行這些命令,它只計算管道最後一個操作的退出程式碼來確定是否成功。在上面這個例子中只要wc -l
命令執行成功這一步就構建成功並且生成一個新的映象,即使wget
命令失敗也是如此。
如果你想讓管道中出現任意錯誤命令都返回錯誤,在命令前加上set -o pipefail &&
來確保避免出現未知錯誤時映象也能構建成功。例如:
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
CMD
指令用於執行你映象包含中的軟體,連同任意引數。CMD
應該儘可能都是用這種形式CMD [“executable”, “param1”, “param2”…]
。然而,如果是一個作為服務的映象,比如Apache和Rails,你應該像這樣執行CMD ["apache2","-DFOREGROUND"]
。實際上,實際上,這種形式的指令是推薦用於任何基於服務的映象。
在其他大多數情況下,CMD
應該給一個互動式Shell,比如bash,python 和 perl。例如,CMD ["perl", "-de0"]
,CMD ["python"]
, 或者CMD [“php”, “-a”]
。試用這種形式就意味著當你執行類似docker run -it python
的一些東西,你將得到一個可用的shell(you’ll get dropped into a usable shell, ready to go)。CMD
應該很少以CMD [“param”, “param”]
的形式和ENTRYPOINT
一起試用,除非你和你的目標使用者已經非常熟悉ENTRYPOINT
工作原理。
EXPOSE
EXPOSE
指令指示容器將監聽連結的埠。因此,你應該為你的應用程式試用通用的傳統的埠。例如,一個包含Apache Web伺服器的映象應該EXPOSE 80
, 而一個包含MongoDB的映象應該使用EXPOSE 27017
等。
對於外部訪問,您的使用者可以使用指示如何將指定埠對映到所選埠的標誌來執行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).???
ENV
為了讓軟體更便於執行,你可以使用ENV
來修改環境變數將軟體安裝目錄加到PATH
。例如:ENV PATH /usr/local/nginx/bin:$PATH
將使CMD [“nginx”]
可以工作。
ENV
指令也可用於給要容器化的服務所需的環境變數,比如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 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。
ENTRYPOINT
使用ENTRYPOINT
最好的方式是設定映象主命令,允許映象把它作為命令執行(然後使用CMD
作為預設標識)。
我們從一個命令列工具s3cmd
映象的例子開始:
ENTRYPOINT ["s3cmd"] CMD ["--help"]
現在可以像這樣執行映象來顯示命令的幫助:
$ docker run s3cmd
或者使用正確的引數來執行一個命令:
$ 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。這樣做允許應用程式接受傳送給容器的Unix訊號。檢視ENTRYPOINT
幫助獲取更多細節。
幫助指令碼被拷貝到容器並且當容器啟動時通過ENTRYPOINT
執行。
COPY ./docker-entrypoint.sh / ENTRYPOINT ["/docker-entrypoint.sh"]
此指令碼允許使用者以多種方式與Postgres進行互動。
它可以簡單的啟動Postgres:
$ docker run postgres
或者,可以執行Postgres並且傳遞引數給伺服器:
$ docker run postgres postgres --help
最後,它也可以被用於啟動一個完全不同的工具,比如Bash:
$ docker run --rm -it postgres bash
VOLUME
VOLUME
指令應該用於暴露任意資料庫儲存區,配置儲存,或者docker容器建立的檔案/目錄等。強烈建議您將VOLUME用於映象的任何可變和/或使用者可維護的部分。
USER
如果服務可以沒有許可權執行,使用USER
變為一個非root使用者。像如下命令一樣開始在Dockerfile
中建立使用者和組:
RUN groupadd -r postgres && useradd --no-log-init -r -g postgres postgres
注:由於Go存檔/ tar包處理稀疏檔案中的一個未解決的bug
, 在docker容器裡建立一個UID足夠大的使用者會在容器層中將/var/log/faillog
寫滿NUL (\0)
而導致磁碟耗盡。傳--no-log--init
標記來建立使用者可以繞開這個問題。Debian/Ubuntu的adduser
包不支援--no-log-init
標記所以應該避免使用。
你應該避免安裝和使用sudo
,因為它不可預知的TTY和訊號轉發行為帶來的問題比解決的問題多。如果你確實需要類似sudo
的功能(例如:以root使用者初始化但是以非root使用者執行),你可以使用"gosu
"。
最後,減少你的層和複雜性,避免切換使用者(Lastly, to reduce layers and complexity, avoid switching USER back and forth frequently.)。
WORKDIR
為了清晰可靠,你應該在使用WORDDIR
時應該一直使用絕對路徑。你也應該使用WORKDIR
而不是使用像RUN cd .. && do-something
這樣難以閱讀、調錯和維護的增量指令。
ONBUILD
ONBUILD
命令在當前Dockerfile
構建完成之後執行。ONBUILD
會在任意一個從當前映象派生的子映象執行。可以把ONBUOLD
命令想象成為一個父級Dockerfile
賦予子Dockerfile
的指令。
Docker構建在子Dockerfile
中的任何命令之前執行ONBUILD
命令。
ONBUILD is useful for images that are going to be built FROM a given image. For example, you would use ONBUILD for a language stack image that builds arbitrary user software written in that language within the Dockerfile, as you can see in Ruby’s ONBUILD variants.
Images built from ONBUILD should get a separate tag, for example: ruby:1.9-onbuild or ruby:2.0-onbuild.
當在ONBUILD
中使用ADD
或者COPY
時要小心。如果新構建的上下文丟失了增加的資源,"onbuild"的映象將會嚴重失敗。如上所述,新增單獨的標籤,允許Dockerfile
的作者自己選擇有助於緩解這種情況。
官方倉庫例項
這些官方倉庫有典型的示範(These Official Repositories have exemplary Dockerfiles):
其他資源
- Dockerfile Reference
- More about Base Images
- More about Automated Builds
- Guidelines for Creating Official Repositories
以下內容為筆者補充
設定時區
很多映象預設使用UTC時間,但是面向中國使用者的的大多應用,在獲取系統時間時直接取系統時間並不會做一個校對,這個時候就會出現程式獲取的時間或者日誌時間和實際不一致的情況。
分享個例子,2017年12月12日,接到研發同事反饋容器內時間不對導致的小問題,於是著手修復。完成後開始檢查其他生產環境中容器時間和時區,發現生產環境中有多大31個應用時區不對(某些應用是同一個映象倉庫,大約涉及20多個映象,及十多個程式碼倉庫),雖然其他應用暫時沒有導致嚴重的問題,但是必然是個隱患,於是開始著手修復,修改Dockerfile->提交->構建->部署,老實說,這是純體力活。。。。
所以一開始就應該做這件事。
-
Alpine
修改時區apk update && add tzdata ca-certificates ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
-
Debian/Ubuntu修改時區
ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime dpkg-reconfigure -f noninteractive tzdata
-
Centos/RedHat
# CentOS的時區配置檔案是:/etc/sysconfig/clock ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime # CentOS/ RHEL 7 Only timedatectl set-timezone /etc/localtime
hwclock -w | --systohc或者# clock -w | --systohc即用系統時鐘同步硬體時鐘
hwclock -s | --hctosys 或者 # clock -s | --hctosyshc代表硬體時間,sys代表系統時間,即用硬體時鐘同步系統時鐘
系統時鐘和硬體時鐘同步:
hwclock -w | --systohc或者# clock -w | --systohc即用系統時鐘同步硬體時鐘
==================================================================
1)CentOS的時區配置檔案是:/etc/sysconfig/clock
這個配置檔案裡面支援UTC,ARC,SRM,ZONE這幾個配置選項,關於這幾個配置選項詳解如下:
(1)UTC
指定BIOS中儲存的時間是否是GMT/UTC時間,true表示BIOS裡面儲存的時間是UTC時間,false表示BIOS裡面儲存的時間是本地時間
(2)ZONE
指定時區,ZONE的值是一個檔案的相對路徑名,這個檔案是相對 /usr/share/zoneinfo 目錄下的一個時區檔案。比如ZONE的值可以是:“Asia/Shanghai", "US/Pacific", "UTC" 等
(3)ARC
這個選項一般配置false,在一些特殊硬體(Alpha)下才配置該選項為true
(4)SRM
它同ARC,該選項一般配置false,在一下特殊硬體下才配置該選項為false
說明:這個配置檔案裡面的引數和 hwclock 命令關係很大,系統在啟動的時候讀取/etc/sysconfig/clock 檔案的內容,根據這些內容呼叫hwclock 命令
2)/etc/sysconfig/clock的配置例項
ZONE="Asia/Shanghai"
UTC=true
ARC=false
https://www.zybuluo.com/zwh88...
清理快取
-
Alpine
apk cache clean rm -rf /var/cache/apk/* ~/.cache/* /usr/local/share/man
-
Debian/Ubuntu
apt-get autoremove rm -rf /var/lib/apt/lists/* ~/.cache/* /usr/local/share/man
-
RedHat/CentOS
yum clean all rm -rf /var/cache/yum/* ~/.cache/* /usr/local/share/man