天天看點

Dockerfile 最佳實踐

一般性的指南和建議

容器應該是短暫的

通過

Dockerfile

建構的鏡像所啟動的容器應該盡可能短暫(生命周期短)。「短暫」意味着可以停止和銷毀容器,并且建立一個新容器并部署好所需的設定和配置工作量應該是極小的。

使用

.dockerignore

檔案

Dockerfile

建構鏡像時最好是将

Dockerfile

放置在一個建立的空目錄下。然後将建構鏡像所需要的檔案添加到該目錄中。為了提高建構鏡像的效率,你可以在目錄下建立一個

.dockerignore

檔案來指定要忽略的檔案和目錄。

.dockerignore

檔案的排除模式文法和 Git 的

.gitignore

檔案相似。

使用多階段建構

Docker 17.05

以上版本中,你可以使用 多階段建構 來減少所建構鏡像的大小。

避免安裝不必要的包

為了降低複雜性、減少依賴、減小檔案大小、節約建構時間,你應該避免安裝任何不必要的包。例如,不要在資料庫鏡像中包含一個文本編輯器。

一個容器隻運作一個程序

應該保證在一個容器中隻運作一個程序。将多個應用解耦到不同容器中,保證了容器的橫向擴充和複用。例如 web 應用應該包含三個容器:web應用、資料庫、緩存。

如果容器互相依賴,你可以使用 Docker 自定義網絡 來把這些容器連接配接起來。

鏡像層數盡可能少

你需要在

Dockerfile

可讀性(也包括長期的可維護性)和減少層數之間做一個平衡。

将多行參數排序

将多行參數按字母順序排序(比如要安裝多個包時)。這可以幫助你避免重複包含同一個包,更新包清單時也更容易。也便于

PRs

閱讀和審查。建議在反斜杠符号

\

之前添加一個空格,以增加可讀性。

下面是來自

buildpack-deps

鏡像的例子:

RUN apt-get update && apt-get install -y \  bzr \  cvs \  git \  mercurial \  subversion
           

建構緩存

在鏡像的建構過程中,Docker 會周遊

Dockerfile

檔案中的指令,然後按順序執行。在執行每條指令之前,Docker 都會在緩存中查找是否已經存在可重用的鏡像,如果有就使用現存的鏡像,不再重複建立。如果你不想在建構過程中使用緩存,你可以在

docker build

指令中使用

--no-cache=true

選項。

但是,如果你想在建構的過程中使用緩存,你得明白什麼時候會,什麼時候不會找到比對的鏡像,遵循的基本規則如下:

  • 從一個基礎鏡像開始(

    FROM

    指令指定),下一條指令将和該基礎鏡像的所有子鏡像進行比對,檢查這些子鏡像被建立時使用的指令是否和被檢查的指令完全一樣。如果不是,則緩存失效。
  • 在大多數情況下,隻需要簡單地對比

    Dockerfile

    中的指令和子鏡像。然而,有些指令需要更多的檢查和解釋。
  • 對于

    ADD

    COPY

    指令,鏡像中對應檔案的内容也會被檢查,每個檔案都會計算出一個校驗和。檔案的最後修改時間和最後通路時間不會納入校驗。在緩存的查找過程中,會将這些校驗和和已存在鏡像中的檔案校驗和進行對比。如果檔案有任何改變,比如内容和中繼資料,則緩存失效。
  • 除了

    ADD

    COPY

    指令,緩存比對過程不會檢視臨時容器中的檔案來決定緩存是否比對。例如,當執行完

    RUN apt-get -y update

    指令後,容器中一些檔案被更新,但 Docker 不會檢查這些檔案。這種情況下,隻有指令字元串本身被用來比對緩存。

一旦緩存失效,所有後續的

Dockerfile

指令都将産生新的鏡像,緩存不會被使用。

Dockerfile 指令

下面針對

Dockerfile

中各種指令的最佳編寫方式給出建議。

FROM

盡可能使用目前官方倉庫作為你建構鏡像的基礎。推薦使用 Alpine 鏡像,因為它被嚴格控制并保持最小尺寸(目前小于 5 MB),但它仍然是一個完整的發行版。

LABEL

你可以給鏡像添加标簽來幫助組織鏡像、記錄許可資訊、輔助自動化建構等。每個标簽一行,由

LABEL

開頭加上一個或多個标簽對。下面的示例展示了各種不同的可能格式。

#

開頭的行是注釋内容。

注意:如果你的字元串中包含空格,必須将字元串放入引号中或者對空格使用轉義。如果字元串内容本身就包含引号,必須對引号使用轉義。
# Set one or more individual labelsLABEL 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=""
           

一個鏡像可以包含多個标簽,但建議将多個标簽放入到一個

LABEL

指令中。

# Set multiple labels at once, using line-continuation characters to break long linesLABEL 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。

RUN

為了保持

Dockerfile

檔案的可讀性,可了解性,以及可維護性,建議将長的或複雜的

RUN

指令用反斜杠

\

分割成多行。

apt-get

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:18.04RUN apt-get updateRUN apt-get install -y curl
           

建構鏡像後,所有的層都在 Docker 的緩存中。假設你後來又修改了其中的

apt-get install

添加了一個包:

FROM ubuntu:18.04RUN apt-get updateRUN 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.*

。如果之前的鏡像使用的是更舊的版本,指定新的版本會導緻

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

),你可能會執行類似于

CMD ["apache2", "-DFOREGROUND"]

形式的指令。我們建議任何服務鏡像都使用這種形式的指令。

多數情況下,

CMD

都需要一個互動式的

shell

(bash, Python, perl 等),例如

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

時使用一個标志來訓示如何将指定的端口映射到所選擇的端口。

ENV

為了友善新程式運作,你可以使用

ENV

來為容器中安裝的程式更新

PATH

環境變量。例如使用

ENV PATH /usr/local/nginx/bin:$PATH

來確定

CMD ["nginx"]

能正确運作。

ENV

指令也可用于為你想要容器化的服務提供必要的環境變量,比如 Postgres 需要的

PGDATA

最後,

ENV

也能用于設定常見的版本号,比如下面的示例:

ENV PG_MAJOR 9.3ENV PG_VERSION 9.3.4RUN 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

雖然

ADD

COPY

功能類似,但一般優先使用

COPY

。因為它比

ADD

更透明。

COPY

隻支援簡單将本地檔案拷貝到容器中,而

ADD

有一些并不明顯的功能(比如本地 tar 提取和遠端 URL 支援)。是以,

ADD

的最佳用例是将本地 tar 檔案自動提取到鏡像中,例如

ADD rootfs.tar.xz

如果你的

Dockerfile

有多個步驟需要使用上下文中不同的檔案。單獨

COPY

每個檔案,而不是一次性的

COPY

所有檔案,這将保證每個步驟的建構緩存隻在特定的檔案變化時失效。例如:

COPY requirements.txt /tmp/RUN pip install --requirement /tmp/requirements.txtCOPY . /tmp/
           

如果将

COPY . /tmp/

放置在

RUN

指令之前,隻要

.

目錄中任何一個檔案變化,都會導緻後續指令的緩存失效。

為了讓鏡像盡量小,最好不要使用

ADD

指令從遠端 URL 擷取包,而是使用

curl

wget

。這樣你可以在檔案提取完之後删掉不再需要的檔案來避免在鏡像中額外添加一層。比如盡量避免下面的用法:

ADD http://example.com/big.tar.xz /usr/src/things/RUN tar -xJf /usr/src/things/big.tar.xz -C /usr/src/thingsRUN 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

的自動提取功能的檔案或目錄,你應該使用

COPY

ENTRYPOINT

ENTRYPOINT

的最佳用處是設定鏡像的主指令,允許将鏡像當成指令本身來運作(用

CMD

提供預設選項)。

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

s3cmd

:

ENTRYPOINT ["s3cmd"]CMD ["--help"]
           

現在直接運作該鏡像建立的容器會顯示指令幫助:

$ docker run s3cmd
           

或者提供正确的參數來執行某個指令:

$ docker run s3cmd ls s3://mybucket
           

這樣鏡像名可以當成指令行的參考。

ENTRYPOINT

指令也可以結合一個輔助腳本使用,和前面指令行風格類似,即使啟動工具需要不止一個步驟。

例如,

Postgres

官方鏡像使用下面的腳本作為

ENTRYPOINT

#!/bin/bashset -eif [ "$1" = 'postgres' ]; then    chown -R postgres "$PGDATA"    if [ -z "$(ls -A "$PGDATA")" ]; then        gosu postgres initdb    fi    exec gosu postgres "$@"fiexec "$@"
           
注意:該腳本使用了 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

指令用于暴露任何資料庫存儲檔案,配置檔案,或容器建立的檔案和目錄。強烈建議使用

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

的指令,後者難以閱讀、排錯和維護。

官方鏡像示例

這些官方鏡像的 Dockerfile 都是參考典範:https://github.com/docker-library/docs