Dockerfile最佳實踐

2021-12-09 網易遊戲運維平臺

在容器領域,docker 公司提出的容器鏡像已經成為目前容器打包交付的事實標準。構建鏡像需要編寫 Dockerfile,如何編寫一個優雅的 Dockerfile 呢?在 Docker 公司的官方文檔中給出了一篇  

Best practices for writing Dockerfiles。

(https://g.126.fm/03ncYHS)

本文在此基礎上做了一些刪減,力圖讓大家在短時間內寫出一份不錯的 Dockerfile。
本文分為三個部分,首先會直接給出一份 Dockerfile 的參考模板,然後說明如和構建高效的鏡像並解釋這個模板這樣組織的原因,最後會補充說明一些編寫過程中的常見問題。

一份簡單的Dockerfile參考模板

docker 官方給出的參考文檔中給出的 Dockerfile 指令接近 20 個,而我們平時在編寫的時候,經常用到的不超過 10 個。因此,這裡給出了一份 Dockerfile 的參考模板,幾乎可以覆蓋大部分的使用場景。

FROM base_image:tag    # 引用基礎鏡像 *必要*

ARG arg_key[=default_value1]     # 聲明變量
ENV env_key=value2     # 聲明環境變量

# 構建幾乎不變的部分,例如整體的目錄結構,build時依賴的文件和工具包等
COPY src dst
RUN command1 && command2 ...

WORKDIR /path/to/work/dir   # 設置工作目錄 

# 構建較少變動的部分,例如應用的依賴的文件、依賴的包等
COPY src dst
RUN command3 && command4 ...

# 構建經常變動的部分,例如應用的編譯生成
COPY src dst
RUN command5 && command6 ...

# 容器入口  *必要*
ENTRYPOINT ["/entry.app"]  # 指定容器啟動時默認執行的命令
CMD ["--options"] # 指定容器啟動時默認命令的默認參數

構建高效鏡像生命周期

容器的一個重要的特點就是能夠快速迭代,因此在容器鏡像迭代的各個環節也應該儘量做到簡潔高效。

1. 鏡像build

精簡 context:每次 build,context 都會複製給 docker daemon,因此要去掉 context 中無關的部分

多層鏡像:如果鏡像很複雜,通常將其分成基礎鏡像(適用於多種應用,內容基本不變的部分)和應用鏡像,應用鏡像通過 FROM 基礎鏡像來減少 build 的步驟

利用構建緩存(build cache):每次在 build 時,docker daemon 會默認從已在緩存中的父鏡像開始,將下一條指令與從該基本鏡像派生的所有子鏡像進行比較,以查看是否其中一個是使用完全相同的指令構建的。如果不是,則緩存無效。因此,為了能夠提高緩存的命中率,在編寫 Dockerfile 時,應該儘量按照變動的頻率來組織(如上文中的模板)

減少 layers:RUN, COPY, ADD 等指令會在 build 時產生對應的 layer,在較舊的 Docker 版本中,需要最小化鏡像中的層數以確保其性能。因此,使用&&來連接多個 RUN 命令是一個常用的方法(如上文中的模板)

使用 multi-stage builds:新特性,後文會詳細介紹

2. 鏡像pull

docker 官方詳細描述了 docker 鏡像和容器在宿主機上的存儲方式:https://docs.docker.com/storage/storagedriver/,簡單來說就是:

根據鏡像的存儲方式,我們也可以加快鏡像的 pull 過程:

常見問題 1. 注意Dockerfile中的指令是逐條執行,且相互獨立

# 下面這種寫法會報錯,第二個RUN執行時的WORKDIR依舊是原來的目錄,不是/some/dir
RUN cd /some/dir
RUN bash script.sh

# 改成下面兩種之一
RUN cd /some/dir && bash script.sh
RUN bash /some/dir/script.sh

2. 提防「過度」緩存

前文也提到過,Dockerfile 中每條指令逐條執行,且相互獨立。大部分的指令在 build 時會生成對應的一層(layer),並被緩存。這種機制在絕大部分的情況下都工作的很好,但是有時也會產生問題:

# Dockerfile1
FROM ubuntu:18.04
RUN apt-get update
RUN apt-get install -y nginx

# Dockerfile2
FROM ubuntu:18.04
RUN apt-get update
RUN apt-get install -y nginx curl

如上,原 Dockerfile1 使用一段時間之後修改成 Dockerfile2(只修改了install這一行)。由於緩存機制(假設之前 build 的緩存還存在),Dockerfile2 在 build 時,update這一行不會真的執行,而是直接拿之前的緩存。此時安裝的 nginx 和 curl 可能就不是當前的最新版本。

# 官方推薦的apt-get使用方式:
RUN apt-get update && apt-get install -y \
    curl \
    nginx=1.16.* \
    && rm -rf /var/lib/apt/lists/*

3. ARG與ENV

兩種指令都可以用來定義變量,但是使用上有很多要注意的點:

ARG key=value
FROM xxx${key}xxxx
ARG key # 這裡需要再次聲明才能使用

ARG 變量的作用範圍是 build 階段 ARG 之後的指令,不會帶入鏡像

ENV 環境變量作用範圍是 build 階段 ENV 聲明的指令,並且會編入鏡像,容器運行時也會這些環境變量也生效

CMD 和 ENTRYPOINT 中不能使用 ARG 和 ENV 定義的變量

當 ARG 和 ENV 變量同名時(無論是誰先定義),ENV 環境變量的值會覆蓋 ARG 變量

ENV 會產生中間層(layer),被編入鏡像,即使使用 unset 也無法去掉,例如:

FROM alpine
ENV ADMIN_USER="mark"  # 此時產生了layer
RUN echo $ADMIN_USER > ./mark
RUN unset ADMIN_USER # 使用unset只是去掉了build時的環境變量,但是最終生成的鏡像中還是會有這個變量

# 運行鏡像還是會列印環境變量
docker run --rm test sh -c 'echo $ADMIN_USER'
mark

# 如果想要消除這種影響,可以改成:
FROM alpine
RUN export ADMIN_USER="mark" \
    && echo $ADMIN_USER > ./mark \
    && unset ADMIN_USER
CMD sh

4. COPY與ADD

兩個指令幾乎相同,當你只想複製本地 context 中的文件到鏡像中時,請無腦用 COPY

COPY 與 ADD 使用時,注意以下規則:

注意文件的屬性,複製時可以同時修改屬主和屬組 COPY/ADD [--chown=<user>:<group>] <src> <dest>

如果不清楚目錄與反斜線對這兩個指令的影響,對所有目錄都加上反斜線就比較好理解了,如COPY <src_dir>/ <dest_dir>/,因為:

<src>必須在 context 下,不能使用../跳出 context

ADD 指令除了 COPY 的所有功能外,還有以下特性,如非必要,儘量少用:

5. CMD與ENTRYPOINT

又是一對很類似的指令,使用時需要注意:

CMD 單獨使用時,用來指定容器啟動時默認執行的命令

ENTRYPOINT 單獨使用時,可以完全取代 CMD

ENTRYPOINT 和 CMD 一起使用時,CMD 變成 ENTRYPOINT 的默認參數

推薦使用 ENTRYPOINT/CMD 的 exec 書寫形式:即ENTRYPOINT ["entry.app", "arg"],因為 shell 書寫形式(ENTRYPOINT entry.app arg)會額外啟動 shell 進程

下表列出了 CMD 與 ENTRYPOINT 的各種組合時的效果:

另外,通過在 docker run 最後的添加欄位,可以指定 ENTRYPOINT 的實際參數

  # 鏡像 test_entrypoint
  ENTRYPOINT ["./entry.app"]
  CMD ["--help"]

  # 運行 test_entrypoint
  docker run test_entrypoint # 即./entry.app --help
  # 帶參數運行
  docker run test_entrypoint -a -t  # 即 ./entry.app -a -t

6. multi-stage builds

Docker 17.05 之後的版本支持一種新的 build 方式:多階段構建(multi-stage builds)。與傳統方式的區別在與,多階段構建能夠使用多個 FROM 將整個 build 階段分成多個階段:

例如,上文提到的模板就可以通過多階段構建的方式來優化。假設我們最終只想得到 entry.app 及其運行環境,而不需要它的編譯環境,那麼可以通過如下方式優化最終生成的鏡像的大小:

# 使用多階段構建,這裡命名一個builder階段,生成編譯後的app
FROM base_image:tag AS builder   

ARG arg_key[=default_value1]     # 聲明變量
ENV env_key=value2     # 聲明環境變量

# 構建整體的目錄結構,build時依賴的文件和工具包等
COPY src dst
RUN command1 && command2 ...

WORKDIR /path/to/work/dir   # 設置工作目錄 

# 構建編譯環境
COPY src dst
RUN command3 && command4 ...

# 編譯生成entry.app
COPY src dst
RUN compile_entry_app

# 構建最終鏡像的階段,只保留應用和其運行環境,編譯的依賴都不需要
FROM base_image:tag
COPY src dest    # 複製運行環境
WORKDIR /path/to/work/dir   # 設置工作目錄 
COPY --from=builder entry.app . # 從builder階段複製app
# 容器入口
ENTRYPOINT ["/entry.app"]  # 指定容器啟動時默認執行的命令
CMD ["--options"] # 指定容器啟動時默認命令的默認參數

相關焦點

  • Docker指令詳解&最佳實踐&面試問題
    -it: 以交互模式運行container        其他常用的和container相關的指令還有:         a. docker ps -a:查看所有宿主機上的container以及其狀態等信息         b. docker stop [container-id]:停止一個容器         c. docker rm
  • docker容器dockerfile詳解
    1.3s => exporting to image 0.5s => => exporting layers
  • Dockerfile 中的 COPY 與 ADD 命令
    在使用 docker build 命令通過 Dockerfile 創建鏡像時,會產生一個 build 上下文(context)。所謂的 build 上下文就是 docker build 命令的 PATH 或 URL 指定的路徑中的文件的集合。在鏡像 build 過程中可以引用上下文中的任何文件,比如我們要介紹的 COPY 和 ADD 命令,就可以引用上下文中的文件。
  • Dockerfile與Docker構建流程解讀
    我希望你已經:1.用過Docker,熟悉docker commit命令2.自己動手編寫過Dockerfile3.自己動手build過一個鏡像,有親身的體驗我主要分享一些現在網上或者文檔中沒有的東西,包括我的理解和一些實踐,有誤之處也請大家指正。好了,正文開始:Dockerfile其實可以看做一個命令集。每行均為一條命令。
  • 學習Docker就應該掌握的dockerfile語法與指令
    在日常的工作中,常常需要製作自己的項目的鏡像
  • Dockerfile使用入門
  • Dockerfile官方文檔詳細介紹
    閱讀完此頁面後,請參考Dockerfile最佳實踐以獲取有關技巧的指南。用法該docker build命令根據Dockerfile和上下文構建圖像。構建的上下文是指定位置PATH或的文件集URL。這PATH是本地文件系統上的目錄。該URL是一個Git倉庫的位置。
  • Docker基礎篇(Dockerfile)-雲原生核心
    編寫Dockerfile文件)A --> D(2. docker build)A --> E(3. docker run)腳本文件的樣式,以centos為例在這裡插入圖片描述腳本文件內容FROM scratchADD centos-7-x86_64-docker.tar.xz /LABEL
  • Dockerfile常用使用方法
    詳解Dockerfile是一個配置文件,可以自動執行創建Docker鏡像的步驟。Docker從Dockerfile中讀取指令,以自動執行原本手動執行的步驟來創建映像。要構建鏡像,請創建一個名為Dockerfile的文件。Dockerfile描述了組裝映像所需的步驟。創建Dockerfile後,調用docker build命令,使用包含Dockerfile的目錄的路徑作為參數。
  • Dockerfile 文件全面詳解
    收錄於話題 #Docker實踐案例如果找不到該 tag 值,構建器將返回錯誤。--platform 標誌可用於在 FROM 引用多平臺鏡像的情況下指定平臺。例如,linux/amd64、linux/arm64、 或 windows/amd64。將在當前鏡像之上的新層中執行命令,在 docker build時運行。
  • 現代程式設計師必須掌握的:Dockerfile 與 Compose 環境搭建學習筆記(二)
    其實 https://hub.docker.com/ 上面各種基礎鏡像非常完善,特別是官方的鏡像質量非常之高,而我再搗騰一次完全是為了讓自己掌握 Dockerfile 方面的技能而已。在選擇基礎鏡像方面,推薦使用 Alpine ,然後再它上面進行定製,因為它非常的小僅3M。
  • 優化 .net core 應用的 dockerfile
    優化 .net core 應用的 dockerfileIntro
  • DockerFile 命令總結
    進來少使用RUN,因為沒執行一次 docker就會增加一層只讀層。可以使用docker run --env =修改環境變量ENV myName John DoeENV myDog Rex The DogENV myCat fluffyADD [--chown=:] ... \ [--chown=:] ["",... ""]: 拷貝一個新文件,或者文件夾或者遠程文件的 URLS,把他們添加到
  • 雲計算核心技術Docker教程:Dockerfile指令詳解
    CMD類似於 RUN 指令,用於運行程序,但二者運行的時間點不同:CMD 在docker run 時運行。RUN 是在 docker build。CMD 指令指定的程序可被 docker run 命令行參數中指定要運行的程序所覆蓋。注意:如果 Dockerfile 中如果存在多個 CMD 指令,僅最後一個生效。格式:CMDCMD ["","","",...]
  • Docker 快速入門之 Dockerfile
    答案當然是可以的,在 Docker 中我們可以從名為 Dockerfile 的文件中讀取指令並且自動構建鏡像。在本文中,將介紹 Dockerfile 的基本語法以及基本知識。1、Dockerfile 是什麼?Dockerfile 其實是一份文本文檔,裡面包含了用戶可以用來操作鏡像的一些指令。
  • dockerfile中ENTRYPOINT與CMD的結合
    因為這兩個命令是掌握dockerfile編寫的核心,所以這邊還是單獨拿出來再講一講。二、CMD 與 ENTRYPOINT主要區別我們直接進入主題,CMD 與 ENTRYPOINT都是用於指定啟動容器執行的命令,區別在於:•當docker run 命令中有參數時,守護進程會忽略CMD命令。
  • 從安全到鏡像流水線,Docker 最佳實踐與反模式一覽
    在本文中,我們將探討Docker的最佳實踐和反模式。反模式是人們對於反覆出現的問題的一般解決方案,這些方案沒有效率,甚至會完全抵消Docker技術棧帶來的好處。下面我們來看看我們的哪些做法不可取。我們需要的標籤標籤是必不可少的,我們需要通過標籤傳達有關Docker鏡像的信息。
  • Dockerfile文件全面詳解
    將在當前鏡像之上的新層中執行命令,在 docker build時運行。所以過多無意義的層,會造成鏡像膨脹過大,可以使用 && 符號連接命令,這樣執行後,只會創建 1 層鏡像運行程序,在 docker run 時運行,但是和 run 命令不同,RUN 是在 docker build 時運行。
  • Docker小白到實戰之Dockerfile解析及實戰演示,果然順手
    Dockerfile簡介在日常開發過程中,需要編寫對應的程序文件,最後通過編譯打包生成對應的可執行文件或是類庫;這裡的Dockerfile文件就好比平時我們編寫的程序文件,但內部的語法和關鍵字並沒有程序那麼複雜和繁多,相對來說還是很簡單的,最後通過docker build命令就可以將對應的程序、文件、環境等構建成鏡像啦。
  • 雲計算核心技術Docker教程:Dockerfile文件ARG命令詳解
    Dockerfile 中的 ARG 指令是定義參數名稱,以及定義其默認值。該默認值可以在構建命令 docker build 中用 --build-arg 參數名=值 來覆蓋。