Dockerfile

Dockerfile 最佳實踐

 

來自官方映像的 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

使用 .dockerignore 文件

通常最好的方法是將 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

構建緩存

在鏡像的構建過程當中,Docker 會遍歷 Dockerfile 文件中的指令,而後按順序執行。在執行每條指令以前,Docker 都會在緩存中查找是否已經存在可重用的鏡像,若是有就使用現存的鏡像,再也不重複建立。若是你不想在構建過程當中使用緩存,你能夠在docker build命令中使用--no-cache=true選項。

可是,若是你想在構建的過程當中使用緩存,你得明白何時會,何時不會找到匹配的鏡像。Docker 遵循的基本規則以下:

  • 從一個基礎鏡像開始(FROM 指令指定),下一條指令將和該基礎鏡像的全部子鏡像進行匹配,檢查這些子鏡像被建立時使用的指令是否和被檢查的指令徹底同樣。若是不是,則緩存失效。
  • 在大多數狀況下,只須要簡單地對比 Dockerfile 中的指令和子鏡像。然而,有些指令須要更多的檢查和解釋。
  • 對於ADDCOPY指令,鏡像中對應文件的內容也會被檢查,每一個文件都會計算出一個校驗和。文件的最後修改時間和最後訪問時間不會歸入校驗。在緩存的查找過程當中,會將這些校驗和和已存在鏡像中的文件校驗和進行對比。若是文件有任何改變,好比內容和元數據,緩存失效。
  • 除了ADDCOPY指令,緩存匹配過程不會查看臨時容器中的文件來決定緩存是否匹配。例如,當執行完 RUN apt-get -y update指令後,容器中一些文件被更新,但 Docker 不會檢查這些文件。這種狀況下,只有指令字符串自己被用來匹配緩存。

一旦緩存失效,全部後續的 Dockerfile 指令都將產生新的鏡像,緩存不會被使用。

Dockerfile 指令

下面針對 Dockerfile 中各類指令的最佳編寫方式給出建議。

FROM

只要有可能,請使用當前官方倉庫做爲構建你鏡像的基礎。咱們推薦使用Debian image,由於它被嚴格控制並保持最小尺寸(當前小於 150 mb),但仍然是一個完整的發行版。

LABEL

你能夠給鏡像添加標籤來幫助組織鏡像、記錄許可信息、輔助自動化構建,或者由於其餘的緣由。每一個標籤一行,由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"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

關於標籤能夠接受的鍵值對,參考 Understanding object labels。關於查詢標籤信息,參考 Managing labels on objects

RUN

一如往常,保持你的 Dockerfile 文件更具可讀性,可理解性,以及可維護性,將長的或複雜的RUN聲明用反斜槓分割成多行。

apt-get

也許RUN指令最多見的用例是安裝包用的apt-get。由於RUN apt-get指令會安裝包,因此有幾個問題須要注意。

不要使用RUN apt-get upgradedist-upgrade,由於許多基礎鏡像中的「必須」包不會在一個非特權容器中升級。若是基礎鏡像中的某個包過期了,你應該聯繫它的維護者。若是你肯定某個特定的包,好比foo,須要升級,使用apt-get install -y foo就行,該指令會自動升級foo包。

永遠將RUN apt-get updateapt-get install組合成一條RUN聲明,例如:

RUN apt-get update && apt-get install -y \         package-bar \         package-baz \         package-foo
  • 1
  • 2
  • 3
  • 4

apt-get update放在一條單獨的RUN聲明中會致使緩存問題以及後續的apt-get install失敗。好比,假設你有一個 Dockerfile 文件:

FROM ubuntu:14.04 RUN apt-get update RUN apt-get install -y curl
  • 1
  • 2
  • 3

構建鏡像後,全部的層都在 Docker 的緩存中。假設你後來又修改了其中的apt-get install,添加了一個包:

FROM ubuntu:14.04 RUN apt-get update RUN apt-get install -y curl nginx
  • 1
  • 2
  • 3

Docker 發現修改後的RUN apt-get update指令和以前的徹底同樣。因此,apt-get update不會執行,而是使用以前的緩存鏡像。由於apt-get update沒有運行,後面的apt-get install可能安裝的是過期的curlnginx版本。

使用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.*
  • 1
  • 2
  • 3
  • 4

固定版本會迫使構建過程檢索特定的版本,而無論緩存中有什麼。這項技術也能夠減小因所需包中未預料到的變化而致使的失敗。

下面是一個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/*
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

其中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大多數狀況下都應該以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

EXPOSE指令用於指定容器將要監聽鏈接的端口。所以,你應該爲你的應用程序使用常見熟知的端口。例如,提供 Apache web 服務的鏡像將使用EXPOSE 80,而提供 MongoDB 服務的鏡像使用EXPOSE 27017,等等。

對於外部訪問,鏡像用戶能夠在執行docker run時使用一個標誌來指示如何將指定的端口映射到所選擇的端口。對於容器 連接,Docker 提供環境變量從接收容器回溯到源容器(例如,MYSQL_PORT_3306_TCP)。

ENV

爲了便於新程序運行,你可使用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
  • 1
  • 2
  • 3
  • 4

相似於程序中的常量(與硬編碼的值相對),這種方法可讓你只需改變單條ENV指令來自動改變容器中的軟件版本。

ADD 和 COPY

雖然ADDCOPY功能相似,但通常優先使用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/
  • 1
  • 2
  • 3

若是將COPY . /tmp/放置在RUN指令以前,只要.目錄中任何一個文件變化,都會致使後續指令的緩存失效。

爲了讓鏡像儘可能小,最好不要使用ADD指令從遠程 URL 獲取包,而是使用curlwget。這樣你能夠在文件提取完以後刪掉再也不須要的文件,能夠避免在鏡像中額外添加一層。(譯者注: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
  • 1
  • 2
  • 3

而是使用下面這種:

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
  • 1
  • 2
  • 3
  • 4

上面使用的管道操做,因此沒有中間文件須要刪除。

對於其餘不須要ADD的自動提取(tar)功能的文件或目錄,你應該堅持使用COPY

ENTRYPOINT

ENTRYPOINT的最佳用處是設置鏡像的主命令,容許將鏡像當成命令自己來運行(用CMD提供默認選項)。

例如,下面的示例鏡像提供了命令行工具s3cmd:

ENTRYPOINT ["s3cmd"] CMD ["--help"]
  • 1
  • 2

如今該鏡像直接這麼運行,顯示命令幫助:

$ docker run s3cmd
  • 1

或者提供正確的參數來執行某個命令:

$ docker run s3cmd ls s3://mybucket
  • 1

這頗有用,由於鏡像名還能夠當成命令行的參考。

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 "$@"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

注意:該腳本使用了 Bash 的內置命令 exec,因此最後運行的進程就是容器的 PID 爲1的進程。這樣,進程就能夠接收到任何發送給容器的 Unix 信號了。

該輔助腳本被拷貝到容器,並在容器啓動時經過ENTRYPOINT執行:

COPY ./docker-entrypoint.sh / ENTRYPOINT ["/docker-entrypoint.sh"]
  • 1
  • 2

該腳本可讓用戶用幾種不一樣的方式和 Postgres 交互。

你能夠很簡單地啓動 Postgres:

$ docker run postgres
  • 1

也能夠執行 Postgres 並傳遞參數:

$ docker run postgres postgres --help
  • 1

最後,你還能夠啓動另一個徹底不一樣的工具,好比 Bash:

$ docker run --rm -it postgres bash
  • 1

VOLUME

VOLUME指令用於暴露任何數據庫存儲區域,配置文件,或容器建立的文件和目錄。強烈建議使用VOLUME來管理鏡像中的可變部分和鏡像用戶能夠改變部分。

USER

若是某個服務不須要特權執行,建議使用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中使用絕對路徑。另外,你應該使用WORKDIR來替代相似於RUN cd ... && do-something的指令,後者難以閱讀、排錯和維護。

ONBUILD

ONBUILD中的命令會在當前鏡像的子鏡像構建時執行。能夠把ONBUILD命令當成父鏡像的Dockerfile傳遞給子鏡像的Dockerfile的指令。

在子鏡像的構建過程當中,Docker 會在執行Dockerfile中的任何指令以前,先執行父鏡像經過ONBUILD傳遞的指令。

當從給定鏡像構建新鏡像時,ONBUILD指令頗有用。例如,你可能會在一個語言棧鏡像中使用ONBUILD,語言棧鏡像用於在Dockerfile中構建用戶使用相應語言編寫的任意軟件,正如 Ruby 的ONBUILD變體

使用ONBUILD構建的鏡像應用一個單獨的標籤,例如:ruby:1.9-onbuildruby:2.0-onbuild

ONBUILD中使用ADDCOPY時要格外當心。若是新的構建上下文中缺乏對應的資源,「onbuild」鏡像會災難性地失敗。添加一個單獨的標籤,容許Dockerfile的做者作出選擇,將有助於緩解這種狀況。

官方倉庫示例

這些官方倉庫的Dockerfile都是參考典範:

附加資源

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息