10 個 Dockerfile 的最佳實踐

Posted by Alan Zhan on Sunday, January 16, 2022

最近剛好在整理 docker 的技術給自己,剛好看到 dockerfile 的最佳實踐,想順手整理上來,不料看到官方已經有最佳實踐了,那我就順手做做翻譯的工作與添增自己的見解。

容器應該是短暫的

通過 Dockerfile 構建的鏡像所啟動的容器,應該盡可能的短暫,這邊的短暫意思是:很快速的啟動,並且很快速的終止。

了解 Build Context (構建的上下文)

建構包含建構鏡像不需要的文件會導致更大的建構上下文與更大的鏡像。這樣會增加建構的時間以及 pull 和 push 鏡像的時間和運行容器時的大小。

  • 建構時需要注意 context 的目錄,通常為 .,目前位置。
  • 善用 .dockerignore,會在下面再做詳細介紹。

使用 .dockerignore 文件

用法和 git 中的 .gitignore 很像,以 git 來說,是為了避免無意義的文件被提交到 git 鍊上,而 .dockerignore 是為了確保 context 越小越好。

借助 stdin 管道傳輸 dockerfile

如果你的鏡像只是臨時性的話,你又懶得撰寫 Dockerfile,你可以通過 stdin 管道,建構鏡像。

echo -e 'FROM busybox\nRUN echo "hello world"' | docker build -
docker build -<<EOF
FROM busybox
RUN echo "hello world"
EOF

使用多階段建構

使用多階段建構,能夠大幅度減少你最終的鏡像大小,而且你就可以不需要考慮優化非最終鏡像的 dockerfile 內的聲明語句。

以下依 golang 為例,在建構的過程中,我們需要 SDK 的鏡像來協助我們打包成二進位文件,而打包完畢之後,我們只需要把文件放到一個極度輕量的鏡像即可,這樣就可以確保最終鏡像是很輕量的。

# build stage
FROM golang:alpine AS build
RUN apk --no-cache add build-base git bzr mercurial gcc
ADD . /src
RUN cd /src && go build -o main

# final stage
FROM alpine
WORKDIR /app
COPY --from=build /src/main /app/
ENTRYPOINT ./main

減少安裝不必要的包

為了降低複雜性、依賴關係、文件大小與建構時間,在非必要情況下,應避免安裝不必要的軟件包。

一個容器指運行一個進程

每個容器應該只有一個關注點,所以容器中最好是只有一個進程,盡可能地把其他應用程序解耦到其他容器中。

但是事情往往沒有那麼簡單,就是有機會用一個容器內會跑起多個進程,那麼這邊會推薦你以下的進程管理工具,這邊列舉了幾個比較有名氣的管理工具:

  • supervisord: 輕易使用的進程管理工具
  • tini: Docker 默認的進程管理工具
  • systemd: 大而全的解決方案,但是相對的有點笨重
  • s6: 非常著名的進程管理工具

鏡像層數盡可能減少

在舊版的 Docker 中,盡量減少鏡像中的層數,以確保他們的性能非常重要:

  • 只有指令 RUN 、 COPY 、 ADD 創建層,其他指令創建臨時中間鏡像,並不會增加建構大小。
  • 在可以的情況下,善用多階段建構,可以參考上方 使用多階段建構

將多行參數排序

將多行參數按照字母排序,譬如:要安裝多個包時。可以避免重複安裝到同一個包,更新包列表時也更容易維護。

RUN apt-get update && apt-get install -y \
  bzr \
  cvs \
  git \
  mercurial \
  subversion \
  && rm -rf /var/lib/apt/lists/*

利用建構緩存

在鏡像的建構過中,Docker 會遍歷 Dockerfile 文堅中的指令,然後按照順序執行。每條指令在執行前, Docker 都會在緩存中尋找是否已經存在可用的重複鏡像,如果有就使用現有的鏡像,不重覆創建。

可以在 docker build 命令中,使用 --no-cache=true 選項來禁止使用緩存。

  • 從已經存在緩存中的父鏡像開始,將下一條指令與該父鏡像的所有子鏡像進行比較,檢查這蠍子鏡像,在被創建時的指令是否和被檢查的指令完全一樣。如果不是,則緩存無效。
    • 補充:越是不容易被改動的文件或指令,應該優先執行,這樣就可以最大程度的保證快取被重複執行。
  • 在大多數的情況下,只需要簡單的比對 Dockefile 中的指令與子鏡像。
  • 對於 ADDCOPY 指令,鏡像中對應文件的內容也會被檢查,每隔文件都會計算出一個較驗和 (但是最後修改時間與最後訪問時間不會被納入較驗)。在緩存尋找的過程中,會將這些較驗和與已存在的鏡像中的文件較驗和進行比較,如果文件有改變,則緩存無效。
  • 除了 ADDCOPY 指令,緩存匹配過程,不會查看臨時容器的文件來決定緩存是否匹配。譬如:當執行 RUN apt-get -y update 指令後,容器的一些文件會被更新,但是 Docker 不會去檢查這些文件,而是紀錄這些指令字串,來匹配緩存。

參考

Dockerfile Best Practices

歡迎到我的 Facebook Alan 的筆記本 留言,順手給我個讚吧!你的讚將成為我持續更新的動力,感謝你的閱讀,讓我們一起學習成為更好的自己。