來自官方映像的 6 個 Dockerfile 技巧 - 技術翻譯 - 開源中國社區 https://www.oschina.net/translate/6-dockerfile-tips-official-images php
本文是 Docker 官方文檔 docs/archive:v1.1 中 Best practices for writing Dockerfiles 的理解和翻譯。包含了 Docker 官方對編寫 Dockerfile 的最佳實踐和建議。這些建議是爲了讓你寫出高效易用的 Dockerfile。Docker 官方強烈建議你聽從這些建議(實際上,若是你是在建立官方鏡像,你必須得聽從這些建議)。python
閱讀該文檔須要你已經會經過Dockerfile
構建鏡像,並瞭解Dockerfile
中各條指令的用途。nginx
經過 Dockerfile 定義的鏡像所產生的容器應該儘量短暫(生命週期短)。「短暫」,意味着能夠中止和銷燬容器,而且建立一個新容器並部署好所需的設置和配置工做量應該是極小的。git
通常最好的方法是將 Dockerfile 放置在一個單獨地空目錄下。而後,將構建鏡像所須要的文件添加到目錄下。爲了提升構建的效率,你也能夠在目錄下建立一個 .dockerignore 文件來指定要忽略的文件和目錄。.dockerignore 文件的排除模式語法和 Git 的 .gitignore 文件相似。github
爲了下降複雜性、減小依賴、減少文件大小、節約構建時間,你應該避免安裝任何沒必要要的包,不要僅僅爲了「錦上添花」而安裝某個包。例如,不要在數據庫鏡像中包含一個文本編輯器。golang
在大多數狀況下,你應該保證在一個容器中只運行一個進程。將多個應用解耦到不一樣容器中,能夠保證應用的橫向擴展性和重用容器。若是你一個服務依賴於另外一個服務,能夠利用容器連接(link)。web
你須要在 Dockerfile 可讀性(也包括長期的可維護性)和減小層數之間作一個平衡。明智並謹慎地考慮你所使用的層數。sql
將多行參數按字母順序排序(好比要安裝多個包時)。這能夠幫助你避免重複包含同一個包,更新包列表時也更容易。也便於 PRs 閱讀和省察。建議在反斜槓符號\
以前添加一個空格,以增長可讀性。docker
下面來自buildpack-deps
鏡像的例子:shell
RUN apt-get update && apt-get install -y \ bzr \ cvs \ git \ mercurial \ subversion
在鏡像的構建過程當中,Docker 會遍歷 Dockerfile 文件中的指令,而後按順序執行。在執行每條指令以前,Docker 都會在緩存中查找是否已經存在可重用的鏡像,若是有就使用現存的鏡像,再也不重複建立。若是你不想在構建過程當中使用緩存,你能夠在docker build
命令中使用--no-cache=true
選項。
可是,若是你想在構建的過程當中使用緩存,你得明白何時會,何時不會找到匹配的鏡像。Docker 遵循的基本規則以下:
ADD
和COPY
指令,鏡像中對應文件的內容也會被檢查,每一個文件都會計算出一個校驗和。文件的最後修改時間和最後訪問時間不會歸入校驗。在緩存的查找過程當中,會將這些校驗和和已存在鏡像中的文件校驗和進行對比。若是文件有任何改變,好比內容和元數據,緩存失效。ADD
和COPY
指令,緩存匹配過程不會查看臨時容器中的文件來決定緩存是否匹配。例如,當執行完 RUN apt-get -y update
指令後,容器中一些文件被更新,但 Docker 不會檢查這些文件。這種狀況下,只有指令字符串自己被用來匹配緩存。一旦緩存失效,全部後續的 Dockerfile 指令都將產生新的鏡像,緩存不會被使用。
下面針對 Dockerfile 中各類指令的最佳編寫方式給出建議。
只要有可能,請使用當前官方倉庫做爲構建你鏡像的基礎。咱們推薦使用Debian image,由於它被嚴格控制並保持最小尺寸(當前小於 150 mb),但仍然是一個完整的發行版。
你能夠給鏡像添加標籤來幫助組織鏡像、記錄許可信息、輔助自動化構建,或者由於其餘的緣由。每一個標籤一行,由LABEL
開頭加上一個或多個標籤對。下面的示例展現了各類不一樣的可能格式。註釋內容是解釋。
注意:若是你的字符串中包含空格,將字符串放入引號中或者對空格使用轉義。若是字符串內容自己就包含引號,必須對引號使用轉義。
# 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"
關於標籤能夠接受的鍵值對,參考 Understanding object labels。關於查詢標籤信息,參考 Managing labels on objects。
一如往常,保持你的 Dockerfile
文件更具可讀性,可理解性,以及可維護性,將長的或複雜的RUN
聲明用反斜槓分割成多行。
也許RUN
指令最多見的用例是安裝包用的apt-get
。由於RUN apt-get
指令會安裝包,因此有幾個問題須要注意。
不要使用RUN apt-get upgrade
或dist-upgrade
,由於許多基礎鏡像中的「必須」包不會在一個非特權容器中升級。若是基礎鏡像中的某個包過期了,你應該聯繫它的維護者。若是你肯定某個特定的包,好比foo
,須要升級,使用apt-get install -y foo
就行,該指令會自動升級foo
包。
永遠將RUN apt-get update
和apt-get install
組合成一條RUN
聲明,例如:
RUN apt-get update && apt-get install -y \ package-bar \ package-baz \ package-foo
將apt-get update
放在一條單獨的RUN
聲明中會致使緩存問題以及後續的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 發現修改後的RUN apt-get update
指令和以前的徹底同樣。因此,apt-get update
不會執行,而是使用以前的緩存鏡像。由於apt-get update
沒有運行,後面的apt-get install
可能安裝的是過期的curl
和nginx
版本。
使用RUN apt-get update && apt-get install -y
能夠確保你的 Dockerfiles 每次安裝的都是包的最新的版本,並且這個過程不須要進一步的編碼或額外干預。這項技術叫做「cache busting」。你也能夠顯示指定一個包的版本號來達到 cache-busting。這就是所謂的固定版本,例如:
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.0*
。若是以前的鏡像使用的是更舊的版本,指定新的版本會致使apt-get udpate
緩存失效並確保安裝的是新版本。
另外,清理掉 apt 緩存,刪除var/lib/apt/lists
能夠減少鏡像大小。由於RUN
指令的開頭爲apt-get udpate
,包緩存老是會在apt-get install
以前刷新。
注意:官方的 Debian 和 Ubuntu 鏡像會自動運行
apt-get clean
,因此不須要顯示的調用apt-get clean
。
CMD
指令用於執行目標鏡像中包含的軟件,能夠包含參數。CMD
大多數狀況下都應該以CMD ["executable", "param1", "param2"…]
的形式使用。所以,若是建立鏡像的目的是爲了部署某個服務(好比 Apache、Rails…),你可能會執行相似於CMD ["apache2","-DFOREGROUND"]
形式的命令。實際上,咱們建議任何服務鏡像都使用這種形式的命令。
多數狀況下,CMD
都須要一個交互式的 shell(bash,python,perl,etc),例如,CMD ["perl","-de0"]
,CMD ["php","-a"]
。使用這種形式意味着,當你執行相似docker run -it python
時,你會進入一個準備好的 shell 中。CMD
應該在極少的狀況下才能以CMD ["param","param"]
的形式與ENTRYPOINT
協同使用,除非你和你的預期用戶都對ENTRYPOINT
的工做方式十分熟悉。
EXPOSE
指令用於指定容器將要監聽鏈接的端口。所以,你應該爲你的應用程序使用常見熟知的端口。例如,提供 Apache web 服務的鏡像將使用EXPOSE 80
,而提供 MongoDB 服務的鏡像使用EXPOSE 27017
,等等。
對於外部訪問,鏡像用戶能夠在執行docker run
時使用一個標誌來指示如何將指定的端口映射到所選擇的端口。對於容器 連接,Docker 提供環境變量從接收容器回溯到源容器(例如,MYSQL_PORT_3306_TCP
)。
爲了便於新程序運行,你可使用ENV
來爲容器中安裝的程序更新PATH
環境變量。例如,ENV PATH /usr/local/nginx/bin:$PATH
將確保CMD ["nginx"]能正確運行
。
ENV
指令也可用於爲你想要容器化的服務提供必要的環境變量,好比 Postgres 須要的PGDATA
。
最後,ENV
也能用於設置常見的版本號,以便維護 version bumps,參考下面的示例:
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
和COPY
功能相似,但通常優先使用COPY
。由於它比ADD
更透明。COPY
只支持簡單將本地文件拷貝到容器中,而ADD
有一些並不明顯的功能(好比本地 tar 提取和遠程 URL 支持)。所以,ADD
的最佳用例是將本地 tar 文件自動提取到鏡像中,例如ADD rootfs.tar.xz
。
若是你的Dockerfiles
有多個步驟須要使用上下文中不一樣的文件。單獨COPY
每一個文件,而不是一次性COPY
完。這將保證每一個步驟的構建緩存只在特定的文件變化時失效。
例如:
COPY requirements.txt /tmp/ RUN pip install --requirement /tmp/requirements.txt COPY . /tmp/
若是將COPY . /tmp/
放置在RUN
指令以前,只要.
目錄中任何一個文件變化,都會致使後續指令的緩存失效。
爲了讓鏡像儘可能小,最好不要使用ADD
指令從遠程 URL 獲取包,而是使用curl
和wget
。這樣你能夠在文件提取完以後刪掉再也不須要的文件,能夠避免在鏡像中額外添加一層。(譯者注:ADD
指令不能和其餘指令合併,因此前者ADD
指令會單獨產生一層鏡像。然後者能夠將獲取、提取、安裝、刪除合併到同一條RUN
指令中,只有一層鏡像。)好比,你應該儘可能避免下面這種用法:
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
的最佳用處是設置鏡像的主命令,容許將鏡像當成命令自己來運行(用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 "$@"
注意:該腳本使用了 Bash 的內置命令
exec
,因此最後運行的進程就是容器的 PID 爲1的進程。這樣,進程就能夠接收到任何發送給容器的 Unix 信號了。
該輔助腳本被拷貝到容器,並在容器啓動時經過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
來管理鏡像中的可變部分和鏡像用戶能夠改變部分。
若是某個服務不須要特權執行,建議使用USER
指令切換到非 root 用戶。先在Dockerfile
中使用相似RUN groupadd -r postgres && useradd -r -g postgres postgres
的指令建立用戶和用戶組。
注意:在鏡像中,用戶和用戶組每次被分配的 UID/GID 都是不肯定的,下次從新構建鏡像時被分配到的 UID/GID 可能會不同。若是要依賴肯定的 UID/GID,你應該顯示的指定一個 UID/GID。
你應該避免使用sudo
,由於它不可預期的 TTY 和信號轉發行爲可能形成的問題比解決的還多。若是你真的須要和sudo
相似的功能(例如,以 root 權限初始化某個守護進程,以非 root 權限執行它),你可使用「gosu」。
最後,爲了減小層數和複雜度,避免頻繁地使用USER
來回切換用戶。
爲了清晰性和可靠性,你應該老是在WORKDIR
中使用絕對路徑。另外,你應該使用WORKDIR
來替代相似於RUN cd ... && do-something
的指令,後者難以閱讀、排錯和維護。
ONBUILD
中的命令會在當前鏡像的子鏡像構建時執行。能夠把ONBUILD
命令當成父鏡像的Dockerfile
傳遞給子鏡像的Dockerfile
的指令。
在子鏡像的構建過程當中,Docker 會在執行Dockerfile
中的任何指令以前,先執行父鏡像經過ONBUILD
傳遞的指令。
當從給定鏡像構建新鏡像時,ONBUILD
指令頗有用。例如,你可能會在一個語言棧鏡像中使用ONBUILD
,語言棧鏡像用於在Dockerfile
中構建用戶使用相應語言編寫的任意軟件,正如 Ruby 的ONBUILD
變體
使用ONBUILD
構建的鏡像應用一個單獨的標籤,例如:ruby:1.9-onbuild
或ruby:2.0-onbuild
。
在ONBUILD
中使用ADD
或COPY
時要格外當心。若是新的構建上下文中缺乏對應的資源,「onbuild」鏡像會災難性地失敗。添加一個單獨的標籤,容許Dockerfile
的做者作出選擇,將有助於緩解這種狀況。
這些官方倉庫的Dockerfile
都是參考典範: