Dockerfile最佳實踐

容器的OCI標準定義了容器鏡像規範,容器鏡像包與傳統的壓縮包(zip/tgz等)相比有兩個關鍵區別點:1)分層存儲;2)打包即部署。html

分層存儲能夠極大減小鏡像更新時候拉取鏡像包的時間,一般應用程序更新升級都只是更新業務層(如Java程序的jar包),而鏡像中的操做系統Lib層、運行時(如Jre)層等文件不會頻繁更新。所以新版本鏡像實質有變化的只有很小的一部分,在更新升級時候也只會從鏡像倉庫拉取很小的文件,因此速度很快。java

打包即部署是指在容器鏡像製做過程包含了傳統軟件包部署的過程(安裝依賴的操做系統庫或工具、建立用戶、建立運行目錄、解壓、設置文件權限等等),這麼作的好處是把應用及其依賴封裝到了一個相對封閉的環境,減小了應用對外部環境的依賴,加強了應用在各類不一樣環境下的行爲一致性,同時也減小了應用部署時間。node

基於分層存儲與打包即部署的特性,容器能夠更快的部署、運行、保持環境一致性,這些特性都是容器相對於傳統虛擬機打包部署的優點。這兩個點都是經過dockerfile來承載的,所以如何編寫高效、安全、規範易用的dockerfile是容器實踐中關鍵的一個環節。
<!--more-->python

Dockerfile 最佳實踐概覽

<iframe width='853' height='480' src='https://embed.coggle.it/diagr...' frameborder='0' allowfullscreen></iframe>nginx

1. 規範與安全

1.1. FROM

1.1.1 優先使用最小功能集的基礎鏡像

不少基礎鏡像都提供不一樣功能集的版本,根據實際狀況選擇最小功能集的版本,功能越少體積越小,引入安全漏洞的風險越低。
Node鏡像,最小的纔不到30M,最大的超過200M。git

1.1.2 顯示指定基礎鏡像的版本,禁用latest

latest 是一個不肯定的版本號,依賴 latest 會致使每次構建出的鏡像是不可預知的。github

1.1.3 指定依賴最具體的鏡像版本

依賴基礎鏡像版本時候,儘可能指定到最具體的版本,這樣不肯定性最小。如Node鏡像,版本號有 12, 12.4, 12.4.0,依賴 12.4.0 是不肯定性最小的。redis

1.1.4 條件容許建議將依賴的基礎鏡像在本地倉庫mirror一份防止相同tag的鏡像內容不一樣

容器鏡像倉庫中的tag是能夠被複寫的,同一個tag在不一樣的時間對應內容可能不一樣,所以若是條件容許,將依賴的鏡像mirror一份到本地,這樣能夠保證
每次構建時候依賴的基礎鏡像是不變的。docker

1.1.5 顯示指定基礎鏡像的平臺架構

Docker 1.10 版本開始,Docker官方鏡像倉庫與Docker-EE/Docker-CE都支持鏡像多平臺功能,這裏的多平臺包含不一樣類型操做系統(Windows/Linux),不一樣CPU體系(X86/ARM64)。緩存

利用鏡像多平臺功能,在docker pull ...命令中能夠不用顯示指定平臺類型,docker客戶端會根據當前的平臺架構與鏡像倉庫協商(基於manifest list特性),拉取對應平臺的鏡像。

經過這個特性能夠保持用戶界面的簡潔性,客戶端執行 docker pull node:12 命令,在ARM64機器上,則會拉取 arm64v8/node:12 鏡像,在Linux機器上,則拉取 node:12 鏡像。

可是這種方式也引入了不肯定性,不肯定性表現爲2方面:第一方面爲不一樣平臺上版本存在差別,可能依賴的版本只在一個平臺有;第二個方面爲有些時間可能在Linux環境下構建ARM鏡像,這樣會致使構建出錯誤鏡像。

1.2. LABEL

1.2.1 經過LABEL指令增長鏡像元數據(做者,時間,描述等)

經過Label增長鏡像做者,構建時間,描述等信息,讓使用者獲得更多關於鏡像信息。

1.2.2 在Label中增長securitytxt規範

經過Label增長鏡像對應應用的 securitytxt 信息。
securitytxtIETF 組織起草的一份規範,目的定義一套互聯網服務提供者與安全漏洞發現者之間交互的規範,使得安全漏洞可以被閉環。

1.3. WORKDIR

1.3.1 使用WORKDIR指定工做目錄,避免絕對路徑擴散

RUN, COPY 命令都要使用到絕對路徑,定義好 WORKDIR 會使得 Dockerfile 移植性更好,更容易維護。

1.4 ENV與ARG

1.4.1 勿使用ENV與ARG傳遞敏感信息

ENV, ARG 的值都會被記錄下來,經過 docker image history 命令能夠查看到,所以不要將敏感信息傳遞給ENV與ARG。

若是須要在dockerfile中使用密鑰或憑證,使用 mount secret 方式。

1.5. RUN

1.5.1 上下文依賴的命令在同一層完成(一個RUN指令)

因爲 docker build cache 機制,有上下文依賴的命令放到同一個RUN指令中執行。

假若有以下dockefile片斷:

...
RUN apt-get update
RUN apt-get install -y nginx
...

過了一段時間以後,須要修改一下上述dockerfile,增長一個安裝包

...
RUN apt-get update
RUN apt-get install -y nginx python
...

此時構建鏡像,docker 比對緩存,RUN apt-get update 這一層已經存在,使用緩存。這時候apt倉庫中的python或nginx可能已經有新版本了,因爲沒有執行 apt-get update ,所以安裝的並非最新版本。

1.5.2 考慮build cache的時效性,使用--no-cache參數禁用cache

經過構建參數 build --no-cache 顯示使得緩存失效,不過緩存失效會增長構建時長,須要綜合考慮。

1.5.3 使用 set -o pipefail 避免管道錯誤被忽略

假如在dockerfile中執行命令 RUN wget -O - https://some.site | wc -l > /number ,若是 wget -O - https://some.site 執行失敗了,可是 wc -l 是成功的,所以並不會報錯退出。

使用 set -o pipefail 避免管道錯誤被忽略。上述命令修改成: RUN set -o pipefail && wget -O - https://some.site | wc -l > /number

1.6 ADD與COPY

1.6.1 優先使用COPY,比ADD更簡單明瞭

ADD 命令功能相對 COPY 更復雜,包含從internet下載,解壓等,優先使用 COPY 。

1.6.2 禁止使用ADD從遠程URL下載包,使用curl或wget先下載,使用ADD從本地解壓到鏡像內

不推薦使用 ADD 命令從遠程下載一個軟件包解壓到鏡像內,推薦使用 curl 下載到本地,而後再解壓到鏡像內,並將原始文件刪除。

1.7 USER

1.7.1 禁用ROOT用戶運行應用,爲應用建立用戶與用戶組

root用戶運行應用程序存在安全風險,爲每一個應用單首創建一個運行用戶。

1.7.2 若是應用依賴特定的UID/GID,則建立用戶/用戶組時候顯示指定

因爲dockerfile中建立用戶/用戶組的 UID/GID 是不固定的,若是應用程序依賴 UID/GID,建立用戶時候顯示指定。

1.7.4 應用運行用戶的Shell設置爲/sbin/nologin

應用程序運行用戶設置Shell爲 /sbin/nologin 。

1.8 EXPOSE

1.8.1 使用EXPOSE指令指明Listen端口與協議

經過 EXPOSE 指令申明應用 listen 的端口與協議,讓應用運維人員者簡單明瞭得知端口。

EXPOSE 80/tcp

1.9 VOLUME

1.9.1 使用VOLUME申明鏡像中須要寫入數據的目錄

程序持久化或臨時文件目錄,使用 VOLUME 指令申明,讓應用運維人員簡單明瞭得知須要掛載的卷。

1.10 日誌

1.10.1 將標準日誌與錯誤日誌分別輸出到stdout與stderr

日誌輸出到標準輸出與錯誤輸出,方便查看與採集日誌。參考 Nginx 的 dockerfile

# forward request and error logs to docker log collector
RUN ln -sf /dev/stdout /var/log/nginx/access.log \
    && ln -sf /dev/stderr /var/log/nginx/error.log

1.11 CMD與ENTRYPOINT

1.11.1 優先使用CMD/ENTRYPOINT指令的EXEC格式設置鏡像的默認執行程序,使得應用進程PID爲1

推薦將容器內的應用程序設置爲PID 1,這樣對容器內應用管理相對簡單,1號進程退出後整個容器也就退出了。

在dockerfile中使用 CMD 或 ENTRYPOINT 指令設置容器鏡像的默認啓動進程, CMD/ENTRYPOINT 有兩種執行方式:exec 與 /bin/sh ,使用 exec 方式直接執行應用程序可執行文件設置PID爲1,使用 /bin/sh 方式的話,1號進程就是 /bin/sh 。

exec 方式: CMD["java", "/same/args"], ENTRYPOINT ["java", "/same/args"] , /bin/sh 方式: CMD java /same/args, ENTRYPOINT java /same/args

1.11.2 使用ENTRYPOINT封裝鏡像的固定行爲,使用CMD配合輸入可變參數

ENTRYPOINT 與 CMD 功能相似,他們區別是 ENTRYPOINT 不能被 docker run 的命令行參數覆蓋,而 CMD 能夠; ENTRYPOINT 一般配合 CMD 使用,使用 CMD 設置 ENTRYPOINT 的默認參數,同時支持在 docker run 設置參數傳遞給 ENTRYPOINT。

參考Redis的ENTRYPOINT寫法。

對於切換用戶,推薦使用 gosu 替換 sudo 。

1.11.3 使用ENTRYPOINT執行運行前準備工做

若是在運行應用程序以前須要作一些準備工做,如檢查文件/目錄權限等,那麼能夠在 ENTRYPOINT 的腳本中完成。

1.11.4 正確理解K8S的command與args與docker鏡像的ENTRYPOINT與CMD的關係

經過K8S調度docker鏡像能夠覆蓋dockerfile中的 ENTRYPOINT 與 CMD,他們之間的關係參考以下文章

1.12 使用hadolint工具檢查dockerfile

hadolint 定義了一套規則與檢查工具,能夠在項目中使用。

2. 效率

2.1 體積小

2.1.1. 選擇最小的基礎鏡像

參考 1.1.1 。

2.1.2 禁止使用 chmod -R /a/root/path,更改具體某個/類文件的權限

因爲dockerfile構建鏡像使用的是分層只讀文件系統,若是使用 chmod 更改一個大目錄權限,至關於複製了這個目錄下全部文件,會致使鏡像體積變大。

2.1.3 分階段構建

Docker 17.05 版本中引入了分階段構建({Multi-stage builds](https://docs.docker.com/devel...),經過分階段構建能夠減小鏡像大小。

2.1.4 dockerfile書寫順序按照更新頻度升序排序

Docker鏡像的分層是子層依賴父層,對應到 Docker build cache 機制中,若是某一層未命中緩存,那麼其剩下的層都不會使用緩存。
所以若是須要最大利用緩存機制,推薦將變化頻度低的層儘可能放上層,變化頻度高的層放下層。

2.1.5 禁止使用相似apt-get upgrade系統級更新,更新具體須要的軟件包

apg-get upgrade 會使得鏡像大小不可控,不知道更新了多少軟件包。

2.1.6 相關度高/更新頻度一致的命令,寫到同一個RUN指令中

一樣是考慮緩存命中率,變化頻度一致的命令放到同一層。

2.2 構建快

2.2.1 使用獨立的目錄做爲build context

Docker在構建鏡像時候有一個 build context 概念,build context 在 docker build 指定一個目錄,docker 會將 build context 目錄內全部文件加載到內存,做爲build context。

build context 目錄內內容越少,加載速度越快,建議使用獨立的目錄做爲build context,只拷貝須要的文件到 build context 目錄。

2.2.2 使用.dockerignore文件

使用 .dockerignore 文件排除目錄下不須要加載到 build context 內的文件或目錄。

2.2.3 從stdin接收dockerfile會使得build context大小爲0

Docker 支持從stdin輸入 dockerfile ,這種方式不會加載任何本地文件到docker的build context中。

Reference

Best practices for writing Dockerfiles

「Allen 談 Docker 系列」docker build 的 cache 機制

理解Docker容器的進程管理

爲容器設置啓動時要執行的命令及其入參

ENTRYPOINT 入口點

hadolint規範與檢查工具

Build secrets and SSH forwarding in Docker 18.09

關注公衆號訂閱雲最佳實踐

相關文章
相關標籤/搜索