Dockerfile最佳實踐

注:
因水平有限,不免有 不許確或過期 以內容,點擊每節標題自動跳轉至原文該節位置,強烈建議閱讀官方文檔!!!
二流運維,三流英語,譯於2017年6月,發於2019年2月,偶爾更新。感謝閱讀,歡迎反饋~

Docker 能夠從 Dockerfile 中讀取指令自動構建鏡像,Dockerfile是一個包含構建指定鏡像全部命令的文本文件。Docker堅持使用特定的格式而且使用特定的命令。你能夠在 Dockerfile參考 頁面學習基本知識。若是你剛接觸Dockerfile 你應該從哪裏開始學習。php

這個文檔囊括了Docker公司和Docker社區推薦的建立易於使用且實用的Dockerfile 的最佳實踐和方法。咱們強烈建議你遵循這些規範(事實上,若是你建立一個官方鏡像,你必須堅持這些實踐。)python

你能夠從 buildpack-deps Dockerifle看到許多這種實踐和建議。nginx

注:本文檔提到的Dockerfile命令的更詳細的解釋見 Dockerfile參考 頁面。

通用參考和建議

容器應該是臨時性的

從你的Dockerfile定義的鏡像啓動的容器應該儘量短暫。這裏的『短暫』咱們是說它能夠被中止和銷燬而且一個新容器的構建和替換能夠絕對最小化的變動和配置下完成。你可能想看下 應用方法論的12個事實中進程 一節來了解以無狀態方式運行容器的動機。git

使用 .dockerignore文件

在大多數狀況下,最好把Dockerfile放在一個空目錄裏。而後,只把構建Dockerfile須要的文件追加到該目錄中。爲了改進構建性能,你也能夠增長一個.dockerignore 文件來排除文件和目錄。該文件支持與 .gitignore 相似的排除模式。更多建立.dockerignore信息,見 .dockerignoregithub

避免安裝不須要的包

爲了減小複雜性,依賴,文件大小,和構建時間,你應該避免僅僅由於他們很好用而安裝一些額外或者沒必要要的包。例如,你不須要在一個數據庫鏡像中包含一個文本編輯器。golang

每一個容器只關心一個問題

解耦應用爲多個容器使水平擴容和複用容器更容易。例如,一個web應用棧會包含3個獨立的容器,每一個都有本身獨立的鏡像,以解耦的方式來管理web應用,數據庫。web

你可能據說過"一個容器一個進程"。這種說法有很好的意圖,一個容器應該有一個操做系統進程並不是真的必要。除此以外,事實上如今容器能夠 被init進程啓動, 一些程序可能會本身產生其餘額外的進程。例如,Celery 能夠產生多個工做進程,或者 Apache 可能爲每一個請求建立一個進程。固然"一個容器一個進程"一般是一個很好的經驗法則,??但它不是一個很難和快速的規則(it is not a hard and fast rule)?? 用你最好的判斷來保持容器儘量的乾淨和模塊化。sql

若是容器之間相關依賴,你可使用 Docker容器網絡 來取吧哦容器之間能夠通訊。docker

最小化層數

你須要在Dockerfile可讀性(從而能夠長時間維護)和它用的層數最小化之間找到平衡。Be strategic 關注你使用的層數(and cautious about the number of layers you use).shell

對多行參數排序

不管什麼時候,以排序多行參數來緩解之後的變化(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中的指令與子鏡像就足夠了。然而,一些特定的指令須要更多的檢查和解釋。
  • 好比ADDCOPY指令,鏡像中的文件內容被檢查而且爲每一個文件計算校驗和。這些文件的最終修改和訪問時間將不被考慮到校驗和內。在查找緩存期間,校驗和將被用於與已存在的鏡像校驗和進行對比。若是文件中有任何變化,好比內容或者元數據,那麼緩存失效。
  • 除了ADDCOPY命令之外,緩存檢查將不會檢查容器中的文件來肯定緩存匹配。好比,當處理一個RUN apt-get -y update容器中的文件更新將不會被檢查來肯定是否命中已存在緩存。在這種狀況下只有命令字符串本身將被用來查找匹配。

一旦緩存失效,全部的後面的Dockerfile命令將會生成新的鏡像並且不會使用緩存。

Dcokerfile指令

下面你會找到寫Dockerfile裏可用的各類指令的建議以及最佳方法。

FROM

Dockerfile參考之FROM指令

不管什麼時候只要可能使用當前官方倉庫鏡像做爲你的基礎鏡像。咱們推薦Debian鏡像, 由於它被嚴格控制而且保持最小(目前小於5MB),同時是一個完整的發行版。

LABEL

理解labels對象

你能夠給你的鏡像增長標籤(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 in Managing labels on objects.

RUN

Dockerfile參考 之 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 updateapt-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沒有執行,你的構建可能會安裝一個過期版本的curlngin

使用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

Dockerfile參考 之 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

Dockerfile參考之 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

Dockerfile參考 之 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

Dockerfile參考之 ADD指令
Dockerfile參考之 COPY指令

儘管ADDCOPY指令功能類似,通常而言,最好使用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是強烈反對的;你應該使用curlwget替代。這種方式你能夠在解壓後不須要時刪除這些文件而且你不會在你的鏡像增長額外一層。例如,你應該避免這麼作:

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

Dockerfile參考 之 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

Dockerfile參考 之 VOLUME指令

VOLUME指令應該用於暴露任意數據庫存儲區,配置存儲,或者docker容器建立的文件/目錄等。強烈建議您將VOLUME用於鏡像的任何可變和/或用戶可維護的部分。

USER

Dockerfile參考 之 USER指令

若是服務能夠沒有權限運行,使用USER變爲一個非root用戶。像以下命令同樣開始在Dockerfile中建立用戶和組:

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容器裏建立一個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

Dockerfile參考之 WORKDIR

爲了清晰可靠,你應該在使用WORDDIR時應該一直使用絕對路徑。你也應該使用WORKDIR而不是使用像RUN cd .. && do-something這樣難以閱讀、調錯和維護的增量指令。

ONBUILD

Dockerfile參考 之 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):

其餘資源

附:如下相關內容爲本人補充非原文內容

清理緩存

  • 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

設置時區

不少鏡像默認使用UTC時間,可是面向中國用戶的的大多應用,在獲取系統時間時直接取系統時間並不會作一個校對,這個時候就會出現程序獲取的時間或者日誌時間和實際不一致的狀況。

分享個例子,曾接到研發同事反饋容器內時間不對致使的小問題,因而着手修復。完成後開始檢查其餘生產環境中容器時間和時區,發現生產環境中有多大31個應用時區不對(某些應用是同一個鏡像倉庫,大約涉及20多個鏡像,及十多個代碼倉庫),雖然其餘應用暫時沒有致使嚴重的問題,可是必然是個隱患,因而開始着手修復,修改Dockerfile -> 提交 -> 構建 -> 部署,老實說,這是純體力活。。。。

因此一開始就應該作這件事。

設置時區(v0.3)

針對全球環境而言,保證時間一致性仍是建議統一使用UTC時間,包括但不限於內容:

  • 操做系統,參考:設置時區(v0.1);
  • 數據庫,如經常使用的MySQLPostgreSQL;
  • 應用容器,通常默認爲UTC,沒必要修改;
  • 應用自己,建議應用自己對時間進行校對;

若是出現數據、系統日誌、應用日誌沒法對齊的狀況,有不少場景會讓人焦頭爛額,如:數據整合、統計、日誌分析等等。

設置時區(v0.2)

前提宿主機時區正確,詳情參考:設置時區(v0.1)

不建議修改容器及鏡像內時區,若須要保證容器時區與宿主機保持一致,經過如下參數將宿主機時區文件掛載到容器便可。

# docker run
docker run -v /etc/localtime:/etc/localtime:ro xxxx

# docker-compose
...
  volume:
    - /etc/localtime:/etc/localtime:ro
...

設置時區(v0.1)

  • 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

時區配置文件

  1. CentOS的時區配置文件是:/etc/sysconfig/clock,配置文件有以下幾個配置選項:

    • UTC,指定BIOS中保存的時間是不是GMT/UTC時間,true表示BIOS裏面保存的時間是UTC時間,false表示BIOS裏面保存的時間是本地時間
    • ZONE,指定時區,ZONE的值是一個文件的相對路徑名,這個文件是相對 /usr/share/zoneinfo 目錄下的一個時區文件。好比ZONE的值能夠是:Asia/Shanghai, US/Pacific, UTC
    • ARC,這個選項通常配置false,在一些特殊硬件(Alpha)下才配置該選項爲true
    • SRM,它同ARC,該選項通常配置false,在一下特殊硬件下才配置該選項爲false
  2. /etc/sysconfig/clock 的配置實例

    ZONE="Asia/Shanghai"
    UTC=true
    ARC=false
說明:這個配置文件裏面的參數和 hwclock 命令關係很大,系統在啓動的時候讀取 /etc/sysconfig/clock 文件的內容,根據這些內容調用 hwclock 命令
相關文章
相關標籤/搜索