Docker筆記(十一):Dockerfile詳解與最佳實踐


Dockerfile是一個文本文件,包含了一條條指令,每條指令對應構建一層鏡像,Docker基於它來構建一個完整鏡像。本文介紹Dockerfile的常用指令及相應的最佳實踐建議。

1. 理解構建上下文(build context)

Docker鏡像通過docker build指令構建,該指令執行時當前的工作目錄就是docker構建的上下文,即build context,上下文中的文件及目錄都會作為構建上下文內容發送給Docker Daemon。

docker build --no-cache -t helloapp:v2 -f dockerfiles/Dockerfile context

 

如上 –no-cache 表示鏡像構建時不使用緩存,-f 指定Dockerfile文件位置, context 指定build context目錄。

 

將一些非必要的文件包含到build context中,會導致build context過大,從而導致鏡像過大,會增加鏡像構建、推送及拉取的時間,以及容器運行時的大小。

 

執行docker build時會顯示build context的大小,

Sending build context to Docker daemon  187.8MB

 

最佳實踐建議

  1. 使用.dockerignore來排除不需要加入到build context中的文件,類似於.gitignore

  2. 不要安裝不必要的包,所有包含的東西都是鏡像必須的,非必須的不要包含。

  3. 解耦應用,如果應用有分層,解耦應用到多個容器,便於橫向擴展,如web應用程序棧包含web服務應用,數據庫,緩存等。

  4. 最少化鏡像層數:只有RUN、COPY、ADD指令會創建鏡像層,其它指令創建臨時的中間鏡像,不會增大鏡像構建的大小

  5. 如果可能,盡可能使用多階段構建,只復制你需要的組件到最終鏡像,這使得你可以在中間構建階段包含工具與debug信息,同時又不會增大最終鏡像的大小。

  6. 排序多行參數:將參數按字母排序,有利於避免包重復,及后續的維護與提高易讀性

 

2. FROM

作用
FROM指定基礎鏡像,每一個定制鏡像,必須以一個現有鏡像為基礎。因此一個Dockerfile中FROM是必須的指令,並且必須是第一條。使用格式,

FROM <image>:<tag>
# 注釋以#開頭。基礎鏡像的tag可不指定,默認使用latest
# 示例:FROM mysql:5.7

 

最佳實踐建議

  1. 如果不想以任何鏡像為基礎,則可以使用FROM scratch

  2. 盡量使用官方鏡像作為基礎鏡像

  3. 推薦使用Alpine鏡像,因為它足夠輕量級(小於5MB),但麻雀雖小五臟俱全,基本具有Linux的基礎功能

     

3. RUN

作用
用來執行命令行命令,是最常用的指令之一。使用格式,

# shell格式,跟直接在命令行輸入命令一行
RUN <命令>
# 示例:RUN mkdir -p /usr/src/redis

# exec格式,類似於函數調用
RUN ["可執行文件", "參數1", "參數2"]

RUN指令創建的中間鏡像會被緩存,並會在下次構建中使用。如果不想使用這些緩存鏡像,可以在構建指令中指定–no-cache參數,如:docker build --no-cache

 

最佳實踐建議

  1. 將比較長的復雜的指令通過 \ 分為多行,讓Dockerfile文件可讀性、可理解性、可維護性更高,將多個指令通過 && 連接,減少鏡像的層數

  2. 確保每一層只添加必需的東西,任何無關的東西都應該清理掉,如所有下載、展開的文件,apt 緩存文件等,以盡可能減少鏡像各層的大小

  3. RUN apt-get update 與 RUN apt-get install 組合成一條RUN指令(將apt-get update單獨作為一條指令會因為緩存問題導致后續的apt-get install 指令失敗)

 

比如先按如下Dockerfile創建了一個鏡像

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

一段時間后,再按以下Dockerfile創建另一個鏡像

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

 

因為RUN指令創建的鏡像層會被緩存,所以下面鏡像的RUN apt-get update並不會執行,直接使用了前面構建的鏡像層,這樣,curl、nginx就可能安裝已經過時的版本。

因此 在 apt-get update 之后立即接 && apt-get install -y ,這叫做“ cache busting”(緩存破壞),也可以通過指定包的版本,來達到同樣的目的,這叫“ version pinning” (版本指定)示例:

RUN apt-get update && apt-get install -y \
reprepro \
ruby1.9.1 \
ruby1.9.1-dev \
#刪除apt 緩存減少鏡像層的大小
&& rm -rf /var/lib/apt/lists/*

 

  1. 使用管道(pipes)。一些RUN指令依賴於從一個指令管道輸出到另一個,如

    RUN wget -O - https://some.site | wc -l > /number

Docker使用/bin/sh -c 解釋器來執行這些指令,只會評估管道最后一條命令的退出碼來確定是否成功,如上例中只要wc -l成功了就算wget失敗,也會認為是成功的。
如果要使管道命令的任何一步報錯都導致指令失敗,則可通過加 set -o pipefile && 來實現,如

RUN set -o pipefail && wget -O - https://some.site | wc -l > /number

 

不是所有的shell都支持-o pipefail選項,如果不支持的話可以使用如下形式,顯式地指定一個支持的shell

RUN ["/bin/bash", "-c", "set -o pipefail && wget -O - https://some.site | wc -l > /number"]

 

4. COPY | ADD

作用
COPY從構建上下文的目錄中復制文件/目錄到鏡像層的目標路徑。使用格式,

COPY [--chown=<user>:<group>] <源路徑>... <目標路徑>
COPY [--chown=<user>:<group>] ["<源路徑1>",... "<目標路徑>"]

同RUN一樣,也有兩種格式。源文件可以多個,甚至可以是通配符,目標路徑是容器的絕對路徑,可以是相對工作目錄(WORKDIR指定)的相對路徑,目標路徑不存在時會自動創建。使用--chown=<user>:<group>來改變文件的所屬用戶與組。
ADD與COPY的使用格式與性質差不多,但功能更豐富,如源路徑可以是URL(下載后放到目標路徑下,文件權限為600),也可以為tar壓縮包,壓縮格式為gzip,bzip2及xz的情況下,ADD 指令將會自動解壓縮這個壓縮文件到目標路徑去

 

最佳實踐建議

  1. 如果在Dockerfile中有多處需要使用不同的文件,分別使用COPY,而不是一次性COPY所有的,這可以保證每一步的構建緩存只會在對應文件改變時,才會失效。比如

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

如果把COPY . /tmp/ 放在RUN上面,將使RUN層鏡像緩存失效的場景更多——因為 . 目錄(當前目錄)中任何一個文件的改變都會導致緩存失效。

  1. 因為鏡像大小的原因, 使用ADD來獲取遠程包是非常不推薦的,應該使用curl或wget,這種方式可以在不再需要使用時刪除對應文件,而不需要增加額外的層,如,應避免如下用法

    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

 

  1. 如果不需要使用ADD的自動解壓特性,盡量使用COPY(語義更清晰)

 

5. CMD

作用
CMD指定容器的啟動命令。容器實質就是進程,進程就需要啟動命令及參數,CMD指令就是用於指定默認的容器主進程的啟動命令的。使用格式

# shell格式
CMD <命令>
# exec格式
CMD ["可執行文件", "參數1", "參數2"...]
# 參數列表格式,在指定了ENTRYPOINT指令后,用CMD來指定具體的參數
CMD ["參數1", "參數2"...]

 

在容器運行時可以指定新的命令來覆蓋Dockerfile中設置的這個默認命令

 

最佳實踐建議

  1. 服務類鏡像建議:CMD ["apache2","-DFOREGROUND"]CMD ["nginx", "-g", "daemon off;"] 容器進程都應以前台運行,不能以后台服務的形式運行,否則啟動就退出了。

  2. 其它鏡像,建議給一個交互式的shell,如bash,python,perl等:CMD ["python"]CMD ["php", "-a"]

 

6. ENTRYPOINT

作用
ENTRYPOINT的目的和CMD一樣,都是在指定容器啟動時要運行的程序及參數。ENTRYPOINT在運行時也可以替代,不過比CMD要略顯繁瑣,需要通過docker run的參數 –entrypoint 來指定。如果指定了ENTRYPOINT,則CMD將只是提供參數,傳遞給ENTRYPOINT。使用ENTRYPOINT可以在容器運行時直接為默認啟動程序添加參數。與RUN指令格式一樣,ENTRYPOINT也分為exec格式和shell格式。

 

最佳實踐建議

  1. ENTRYPOINT可用來指定鏡像的主命令,允許鏡像能像命令一樣運行,可以使用CMD來作為默認的標志(參數),如

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

直接run時,相當於執行了s3cmd --help。也可以使用shell腳本,在腳本中做一些預處理的工作,如

COPY ./docker-entrypoint.sh /
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["postgres"]

 

7. LABEL

作用
為鏡像添加label以方便組織鏡像,記錄licensce信息,幫助自動化實現等等。字符串中包含空格需要轉義或包含在引號中, 如

# Set one or more individual labels
LABEL com.example.version="0.0.1-beta"
LABEL vendor1="ACME Incorporated"
LABEL com.example.release-date="2019-09-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="2019-09-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="2019-09-12"

 

8. ENV

作用
ENV設置環境變量,無論是后面的其它指令,如 RUN(使用 $環境變量key 的形式) ,還是運行時的應用,都可以直接使用這里定義的環境變量。使用格式有兩種,

#只能設置一個key value
ENV <key> <value>
#可以設置多個,value中如果包含空格可以使用\來進行轉義,也可以通過""括起來;也可以用反斜線來續行
ENV <key1>=<value1> <key2>=<value2>...

 

除了RUN,還有這些指令可以引用環境變量:ADD 、 COPY 、 ENV 、 EXPOSE 、 LABEL 、 USER 、 WORKDIR 、 VOLUME 、STOPSIGNAL 、 ONBUILD

 

最佳實踐建議

  1. 定義環境變量,更新PATH環境變量,如要使 CMD [“nginx”] 運行,可設置環境變量 ENV PATH /usr/local/nginx/bin:$PATH

  2. ENV也可以用於定義常量,便於維護

 

9. ARG

作用
ARG設置構建參數,即docker build命令時傳入的參數。和ENV的效果差不多,都是設置環境變量,不同的是,ARG設置的是構建環境的環境變量,在容器運行時是不會存在這些環境變量的。
Dockerfile中的ARG指令是定義參數名稱,以及默認值(可選)。該默認值可以在執行構建命令docker build時用 –build-arg <參數名>=<值> 來覆蓋。使用格式,

ARG <參數名>[=<默認值>]
```

**最佳實踐建議**
1. 不要使用ARG來保存密碼之類的信息,因為通過docker history還是可以看到docker build執行時的所有值
2. 使用ARG,對於使用CI系統(持續集成),用同樣的構建流程構建不同的 Dockerfile 的時候比較有幫助,避免構建命令必須根據每個 Dockerfile 的內容修改

## 10. WORKDIR

**作用**
WORKDIR用於指定工作目錄(或當前目錄),以后各層的當前目錄就被改為指定的目錄,如該目錄不存在,會自動創建。使用格式,
```shell
WORKDIR <工作目錄路徑>
```

**最佳實踐建議**
1. WORKDIR應該使用絕對路徑,顯得更為清楚、可靠
2. 使用WORKDIR,避免使用`RUN cd … && do-something`,可讀性差,難以維護

## 11. VOLUME

**作用**
VOLUME用於定義匿名卷。容器運行時應該盡量保持容器存儲層不發生寫操作,應該將數據寫入存儲卷。VOLUME就是為了防止運行時用戶忘記將動態文件所保存的目錄掛載為卷,我們事先在Dockerfile中指定某些目錄掛載為匿名卷,這樣在運行時如果用戶不指定掛載,其應用也可以正常運行,不會向容器存儲層寫入大量數據。使用格式,
```shell
VOLUME ["<路徑1>", "<路徑2>"...]
VOLUME <路徑>

如 VOLUME /data, 任何向/data目錄寫入的數據都會寫入匿名卷。可以運行容器時覆蓋這個掛載設置 docker run -d -v host-path:/data xxxx

 

最佳實踐建議

  1. VOLUME應該被用來暴露所有的數據存儲,配置存儲,或者被容器創建的文件、目錄

  2. 如果數據動態變化,強烈建議使用VOLUME

 

12. EXPOSE

作用
EXPOSE指令是聲明運行時容器提供的服務端口,也只是一個聲明,在容器運行時並不會因為這個聲明應用就一定會開啟這個端口的服務,容器啟動時,還是需要通過 -p host-port:container-port來實現映射。EXPOSE主要是幫助鏡像使用者了解這個鏡像服務的監聽端口,以方便進行映射配置,另一個用處是在運行時如果是使用隨機端口映射,也就是通過 docker run -P的形式時,會自動隨機映射EXPOSE聲明的端口。使用格式,

EXPOSE <端口1> [<端口2>...]
```

**最佳實踐建議**
1. 應該使用常用的慣用的端口,如nginx 80,mongoDB 27017

## 13. USER

**作用**
USER指令和WORKDIR相似,都是改變環境狀態並影響以后的層。WORKDIR是改變工作目錄, USER則是改變之后的層在執行RUN , CMD以及ENTRYPOINT這類命令時的身份。USER幫助你切換到指定的用戶,這個用戶必
須是事先建立好的,否則無法切換。使用格式
```shell
USER <用戶名>[:<用戶組>]

 

最佳實踐建議

  1. 如果一個服務不需要權限也能運行,則使用USER來切換到非root用戶,如RUN groupadd -r postgres && useradd --no-log-init -r -g postgres postgres

  2. 避免使用sudo,因為可能存在一些不可預見的TTY與信號轉發行為導致問題,如果實在需要,考慮使用“gosu”。為了減少鏡像層數,應避免不斷切換USER
    使用gosu示例

    # 建立 redis 用戶,並使用 gosu 換另一個用戶執行命令
    RUN groupadd -r redis && useradd -r -g redis redis
    # 下載 gosu
    RUN wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/
    releases/download/1.7/gosu-amd64" \
    && chmod +x /usr/local/bin/gosu \
    && gosu nobody true
    # 設置 CMD,並以另外的用戶執行
    CMD [ "exec", "gosu", "redis", "redis-server" ]

 

14. HEALTHCHECK

作用
HEALTHCHECK用於檢查容器的健康狀態,Docker可通過健康狀態來決定是否對容器進行重新調度。使用格式

HEALTHCHECK [選項] CMD <命令>

 

支持的選項為

  • –interval=<間隔> :兩次健康檢查的間隔,默認為30秒

  • –timeout=<時長> :執行健康檢查命令的超時時間,如果超時,則本次健康檢查就被視為失敗,默認30秒

  • –retries=<次數> :當連續失敗指定的次數后,將容器狀態置為unhealthy ,默認3次

 

命令的返回值決定了該次健康檢查的成功與否—— 0 :成功;1 :失敗;2 :保留(不要使用這個值),如:

FROM nginx
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib
/apt/lists/*
HEALTHCHECK --interval=5s --timeout=3s \
CMD curl -fs http://localhost/ || exit 1
```
可以使用docker ps 或docker inspect來查看容器的健康狀態。

**最佳實踐建議**
1. 如果基礎鏡像有健康檢查指令,想要屏蔽掉其健康檢查,可以使用`HEALTHCHECK NONE`
2. 對一些可能造成假死(進程還在, 但提供不了服務了)的服務建議提供健康檢查,以便及時重新調度恢復服務


## 15. ONBUILD

**作用**
ONBUILD后跟的指令,只有當以當前鏡像為基礎鏡像,去構建下一級鏡像的時候才會被執行。使用格式
```shell
ONBUILD <其它指令>

 

它后面跟的是其它指令,比如 RUN , COPY 等,這些指令在當前鏡像構建時並不會被執行。
ONBUILD命令在本鏡像的子鏡像中執行,把ONBUILD想象為父鏡像為子鏡像聲明的一條指令,Docker會在子鏡像所有命令之前執行ONBUILD指令。

 

最佳實踐建議

  1. 當在ONBUILD指令中使用ADD或COPY時要注意,如果build context中沒有指定的資源,可能導致災難性的錯誤。

 

16. 用緩存鏡像提高效率

Docker在構建鏡像時會復用緩存中已經存在的鏡像,如果明確不使用緩存,則可加參數docker build --no-cache=true
使用緩存鏡像的規則

  1. 從一個已存在於緩存的父鏡像開始構建,則會將當前鏡像的下一行指令與所有繼承於那個父鏡像的子鏡像比較,如果其中沒有一個是使用相同的指令構建的,則緩存失效

  2. 大部分情況下,將Dockerfile中的指令與其中一個子鏡像簡單比較就夠了,但是某些指令需要更多的檢查與說明:對於ADD,COPY指令,文件內容會被檢查,會計算每一個文件的checksum,checksum中不會考慮最后修改及最后訪問時間,在緩存中查找時,checksum會與已經存在的鏡像進行比較,如果文件中有修改,則緩存失效。除了ADD,COPY命令,緩存檢查不會查看容器中的文件來決定緩存匹配,如處理RUN apt-get -y update命令時,容器中文件的更新不會進行檢查來確定緩存是否命中, 這種情況下, 只會檢查指令字符串本身是否匹配。

  3. 一旦緩存失效,所有后續的指令都會產生新的鏡像,不會再使用緩存。

 

17. 其它鏡像構建方式

  1. 通過標准輸入來生成Dockerfile構建,不會發送build context(從stdin讀取build context,只包含Dockerfile),適用於一次性構建,不需要寫Dockerfile

    # 將會構建一個名稱與tag均為none的鏡像
    echo -e 'FROM busybox\nRUN echo "hello world"' | docker build -
    #或
    docker build - <<EOF
    FROM busybox
    RUN echo "hello world"
    EOF

    # 構建一個命名的鏡像
    docker build -t myimage:latest - <<EOF
    FROM busybox
    RUN echo "hello world"
    EOF

連字符 - 作為文件名告訴Docker從stdin讀取Dockerfile

  1. 使用stdin來生成Dockerfile, 但是使用當前目錄作為build context

    # build an image using the current directory as context, and a Dockerfile passed through stdin
    docker build -t myimage:latest -f- . <<EOF
    FROM busybox
    COPY somefile.txt .
    RUN cat /somefile.txt
    EOF
  2. 使用遠程git倉庫構建鏡像,從stdin生成Dockerfile

    docker build -t myimage:latest -f - https://github.com/docker-library/hello-world.git <<EOF
    FROM busybox
    COPY hello.c .
    EOF


END

 

相關閱讀

Docker筆記(一):什么是DockerDocker筆記(二):Docker管理的對象
Docker筆記(三):Docker安裝與配置
Docker筆記(四):Docker鏡像管理
Docker筆記(五):整一個自己的鏡像
Docker筆記(六):容器管理Docker筆記(七):常用服務安裝——Nginx、MySql、Redis

Docker筆記(八):數據管理

Docker筆記(九):網絡管理
Docker筆記(十):使用Docker來搭建一套ELK日志分析系統

 

作者:空山新雨
歡迎關注我的微信公眾號:jboost-ksxy

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM