一、Dockerfile 介紹
Dockerfile 是 docker 中用於定義鏡像自動化構建流程的配置文件,在 Dockerfile 中,包含了構建鏡像過程中需要執行的命令和其他操作。通過 Dockerfile 可以更加清晰、明確的給定 docker 鏡像的製作過程,而由於其僅是簡單、小體積的文件,在網絡等其他介質中傳遞的速度極快,能夠更快的幫助我們實現容器遷移和集羣部署。
簡單來說 Dockerfile 就是構建容器的過程。
Dockerfile 的定義就是針對一個名爲 Dockerfile 的文件,沒有擴展名,但本質就是一個文本文件,可以通過常見的文本編輯器或者 IDE 創建和編輯它。
Dockerfile 的內容很簡單,主要以兩種形式呈現,一種是註釋行(以 # 開頭),另一種是指令行,指令行擁有一套獨立的指令語法,其用於給出鏡像構建過程中所要執行的過程。Dockerfile 裏的指令行,就是由指令與其相應的參數所組成。
二、Dockerfile 比容器的優勢
- Dockerfile 的體積遠小於鏡像包,更容易進行快速遷移和部署。
- 環境構建流程記錄了 Dockerfile 中,能夠直觀的看到鏡像構建的順序和邏輯。
- 使用 Dockerfile 來構建鏡像能夠更輕鬆的實現自動部署等自動化流程。
- 在修改環境搭建細節時,修改 Dockerfile 文件要比從新提交鏡像來的輕鬆、簡單。
三、Dockerfile 的結構
總體上來說,可以將 Dockerfile 理解爲一個由上往下執行指令的腳本文件。當調用構建命令讓 Docker 通過 Dockerfile 構建鏡像時,Docker 會逐一按順序解析 Dockerfile 中的指令,並根據它們不同的含義執行不同的操作。
Dockerfile 的指令簡單分爲五大類。
- 基礎指令:用於定義新鏡像的基礎和性質。
- 控制指令:是指導鏡像構建的核心部分,用於描述鏡像在構建過程中需要執行的命令。
- 引入指令:用於將外部文件直接引入到構建鏡像內部。
- 執行指令:能夠爲基於鏡像所創建的容器,指定在啓動時需要執行的腳本或命令。
- 配置指令:對鏡像以及基於鏡像所創建的容器,可以通過配置指令對其網絡、用戶等內容進行配置。
這五類命令並非都會出現在一個 Dockerfile 裏,但卻對基於這個 Dockerfile 所構建鏡像形成不同的影響。
四、常見 Dockerfile 指令
1、FROM
通常來說,不會從零開始搭建一個鏡像,而是會選擇一個已經存在的鏡像作爲新鏡像的基礎。在 Dockerfile 裏,可以通過 FROM 指令指定一個基礎鏡像,接下來所有的指令都是基於這個鏡像所展開的。在鏡像構建的過程中,Docker 也會先獲取到這個給出的基礎鏡像,再從這個鏡像上進行構建操作。
FROM 指令支持三種形式:
FROM <image> [AS <name>]
FROM <image>[:<tag>] [AS <name>]
FROM <image>[@<digest>] [AS <name>]
既然選擇一個基礎鏡像是構建新鏡像的根本,那麼 Dockerfile 中的第一條指令必須是 FROM 指令,因爲沒有了基礎鏡像,一切構建過程都無法開展。但是一個 Dockerfile 以 FROM 指令作爲開始並不意味着 FROM 只能是 Dockerfile 中的第一條指令。在 Dockerfile 中可以多次出現 FROM 指令,當 FROM 第二次或者之後出現時,表示在此刻構建時,要將當前指出鏡像的內容合併到此刻構建鏡像的內容裏。
2、RUN
鏡像的構建雖然是按照指令執行的,但指令只是引導,最終大部分內容還是控制檯中對程序發出的命令,而 RUN 指令就是用於向控制檯發送命令的指令。
在 RUN 指令之後,直接拼接上需要執行的命令,在構建時,Docker 就會執行這些命令,並將它們對文件系統的修改記錄下來,形成鏡像的變化。
RUN <command>
RUN ["executable", "param1", "param2"]
RUN 指令是支持 \
換行的,如果單行的長度過長,建議對內容進行切割,方便閱讀。
3、ENTRYPOINT 和 CMD
基於鏡像啓動的容器,在容器啓動時會根據鏡像所定義的一條命令來啓動容器中進程號爲 1 的進程。而這個命令的定義,就是通過 Dockerfile 中的 ENTRYPOINT 和 CMD 實現的。
ENTRYPOINT ["executable", "param1", "param2"]
ENTRYPOINT command param1 param2
CMD ["executable","param1","param2"]
CMD ["param1","param2"]
CMD command param1 param2
ENTRYPOINT 指令和 CMD 指令的用法近似,都是給出需要執行的命令,並且它們都可以爲空,或者說是不在 Dockerfile 裏指出。
當 ENTRYPOINT 與 CMD 同時給出時,CMD 中的內容會作爲 ENTRYPOINT 定義命令的參數,最終執行容器啓動的還是 ENTRYPOINT 中給出的命令。
ENTRYPOINT 和 CMD 的區別在於,ENTRYPOINT 指令的優先級高於 CMD 指令。當 ENTRYPOINT 和 CMD 同時在鏡像中被指定時,CMD 裏的內容會作爲 ENTRYPOINT 的參數,兩者拼接之後,纔是最終執行的命令。
ENTRYPOINT | CMD | 實際執行 |
---|---|---|
ENTRYPOINT ["/bin/ep", “arge”] | /bin/ep arge | |
ENTRYPOINT /bin/ep arge | /bin/sh -c /bin/ep arge | |
CMD ["/bin/exec", “args”] | /bin/exec args | |
CMD /bin/exec args | /bin/sh -c /bin/exec args | |
ENTRYPOINT ["/bin/ep", “arge”] | CMD ["/bin/exec", “argc”] | /bin/ep arge /bin/exec argc |
ENTRYPOINT ["/bin/ep", “arge”] | CMD /bin/exec args | /bin/ep arge /bin/sh -c /bin/exec args |
ENTRYPOINT /bin/ep arge | CMD ["/bin/exec", “argc”] | /bin/sh -c /bin/ep arge /bin/exec argc |
ENTRYPOINT /bin/ep arge | CMD /bin/exec args | /bin/sh -c /bin/ep arge /bin/sh -c /bin/exec args |
ENTRYPOINT 和 CMD 設計的目的是不同的,ENTRYPOINT 指令主要用於對容器進行一些初始化,而 CMD 指令則用於真正定義容器中主程序的啓動命令。
比如 redis 的 Dockerfile 文件 : https://github.com/docker-library/redis/blob/d42494ab2d96070c8d83f37a7542fbbffd999988/5.0/Dockerfile
# ...
ENTRYPOINT ["docker-entrypoint.sh"]
EXPOSE 6379
CMD ["redis-server"]
可以很清晰的看到,CMD 指令定義的是啓動 Redis 的服務程序,而 ENTRYPOINT 使用的是一個外部引入的腳本文件。
創建容器時可以改寫容器主程序的啓動命令,而這個覆蓋只會覆蓋 CMD 中定義的內容,而不會影響 ENTRYPOINT 中的內容。
4、EXPOSE
在未做特殊定義的前提下,直接連接容器網絡只能訪問容器明確暴露的端口。可以在容器創建時通過選項來暴露這些端口,也可以在鏡像中定義端口暴露。
通過 EXPOSE 指令爲鏡像指定要暴露的端口:
EXPOSE <port> [<port>/<protocol>...]
當通過 EXPOSE 指令配置了鏡像的端口暴露定義,可以在被其他容器通過 --link
選項連接時,直接允許來自其他容器對這些端口的訪問了。
注意 : EXPOSE 僅僅是聲明容器打算使用什麼端口而已,並不會自動在宿主進行端口映射。
5、VOLUME
在 Dockerfile 裏可以通過 VOLUME 持久化數據:
VOLUME ["/data"]
在 VOLUME 指令中定義的目錄,在基於新鏡像創建容器時,會自動建立爲數據卷,不需要再使用 -v
選項來配置了。
6、COPY 和 ADD
在製作新的鏡像的時候,可能需要將一些軟件配置、程序代碼、執行腳本等直接導入到鏡像內的文件系統裏,使用 COPY 或 ADD 指令能直接從宿主機的文件系統裏拷貝內容到鏡像裏的文件系統中。
COPY [--chown=<user>:<group>] <src>... <dest>
ADD [--chown=<user>:<group>] <src>... <dest>
COPY [--chown=<user>:<group>] ["<src>",... "<dest>"]
ADD [--chown=<user>:<group>] ["<src>",... "<dest>"]
COPY 與 ADD 指令的定義方式完全一樣,需要注意的僅是當的目錄中存在空格時,可以使用後兩種帶引號格式避免空格產生歧義。
對比 COPY 與 ADD,兩者的區別主要在於 ADD 能夠支持使用網絡端的 URL 地址作爲 src 源,並且在源文件被識別爲壓縮包時,自動進行解壓,而 COPY 沒有這兩個能力。
雖然看上去 COPY 能力稍弱,但對於那些不希望源文件被解壓或沒有網絡請求的場景,COPY 指令是個不錯的選擇。
五、構建鏡像
在編寫好 Dockerfile 之後,就可以通過命令 docker build
構建鏡像了:
$ sudo docker build ./webapp
docker build
命令可以接收一個參數,需要特別注意的是,這個參數爲一個目錄路徑(本地路徑或 URL 路徑),而並非 Dockerfile 文件的路徑。在 docker build
命令裏,這個給出的目錄會作爲構建的環境目錄,很多的操作都是基於這個目錄進行的。例如,在我們使用 COPY 或是 ADD 拷貝文件到構建的新鏡像時,會以這個目錄作爲基礎目錄。
在默認情況下,docker build
也會從這個目錄下尋找名爲 Dockerfile 的文件,將它作爲 Dockerfile 內容的來源。如果的 Dockerfile 文件路徑不在這個目錄下,或者有另外的文件名,可以通過 -f
選項指定 Dockerfile 文件的路徑。
$ sudo docker build -t webapp:latest -f ./webapp/a.Dockerfile ./webapp
其中 -t
是指定新生成鏡像的名稱與版本。
六、變量
構建中使用參數變量
一些在構建時,需要在構建命令中傳入的變量,可以用 ARG 指令來定義一個參數變量作爲佔位符,構建時通過構建指令傳入這個參數變量,在 Dockerfile 裏使用它。例如:版本號。定義的變量需要通過 $NAME
這種形式來佔位。
# ...
ARG TOMCAT_MAJOR
ARG TOMCAT_VERSION
# ...
RUN wget -O tomcat.tar.gz "https://www.apache.org/dyn/closer.cgi?action=download&filename=tomcat/tomcat-$TOMCAT_MAJOR/v$TOMCAT_VERSION/bin/apache-tomcat-$TOMCAT_VERSION.tar.gz"
# ...
構建時通過 docker build
的 --build-arg
選項來設置參數變量:
sudo docker build --build-arg TOMCAT_MAJOR=8 --build-arg TOMCAT_VERSION=8.0.53 -t tomcat:8.0 ./tomcat
環境變量
在構建中經常用到的同一個值,可以用 ENV
指定爲環境變量,方便在後面做統一處理。定義的變量也是使用 $NAME
這種形式來佔位。
## ......
ENV TOMCAT_MAJOR 8
ENV TOMCAT_VERSION 8.0.53
## ......
RUN wget -O tomcat.tar.gz "https://www.apache.org/dyn/closer.cgi?action=download&filename=tomcat/tomcat-$TOMCAT_MAJOR/v$TOMCAT_VERSION/bin/apache-tomcat-$TOMCAT_VERSION.tar.gz"
## ......
環境變量與參數變量的區別是,環境變量不但可以影響構建,還會影響通過這個鏡像構建的容器,環境變量其實就是定義容器的系統變量,所以在系統中也是可以使用這些變量的。
由於環境變量是在 Dockerfile 中定義的,需要修改時,就需要修改鏡像,但是可以在創建對應容器時使用 -e
或者 --env
選項修改環境變量:
$ sudo docker run -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:5.7
對於環境變量與參數變量有同樣的變量名,ENV
指令定義的環境變量,永遠會覆蓋 ARG
所定義的參數變量,不管他們定義時順序是怎麼樣的。
七、合併命令
在 Dockerfile 中,在 RUN 指令裏面聚合了大量代碼,實際上,多條代碼聚合執行與逐一執行單條代碼是沒有太大區別的。下面兩種寫法基本上是一樣的:
RUN apt-get update; \
apt-get install -y --no-install-recommends $fetchDeps; \
rm -rf /var/lib/apt/lists/*;
RUN apt-get update
RUN apt-get install -y --no-install-recommends $fetchDeps
RUN rm -rf /var/lib/apt/lists/*
但是在實際中,其實是前一種聚合寫法居多。在 docker build
構建中,當一條能夠形成對文件系統改動的指令在被執行前,Docker 先會基於上條命令的結果啓動一個容器,在容器中運行這條指令的內容,之後將結果打包成一個鏡像層。每有一個命令執行都會進行一遍這個操作,最終形成鏡像。
鏡像是由多個鏡像層疊加而得,而這些鏡像層其實就是在 Dockerfile 中每條對文件系統改動的指令所生成的。那麼聚合指令的做法不但減少了鏡像層的數量,也減少了鏡像構建過程中反覆創建容器的次數,提高了鏡像構建的速度。
八、構建緩存
Docker 在鏡像構建的過程中,還支持使用緩存策略來提高鏡像的構建速度。其實緩存就是使用已經有的鏡像。
由於鏡像是多個指令所創建的鏡像層組合而得,如果知道新編譯的鏡像層與已有的鏡像層是一樣的,那麼完全可以直接利用之前構建的結果,而不需要再執行這條構建指令,這就是鏡像構建緩存的原理。
Docker 是如何判斷鏡像層與之前的鏡像間一樣的呢?這主要參考兩個維度:
- 所基於的鏡像層是否一樣
- 用於生成鏡像層的指令的內容是否一樣
基於這個原則,在條件允許的前提下,可以將不容易發生變化的搭建過程放到 Dockerfile 的前部,充分利用構建緩存提高鏡像構建的速度。另外,指令的合併也不宜過度,而是將易變和不易變的過程拆分,分別放到不同的指令裏。
在另外一些時候,可能不希望 Docker 在構建鏡像時使用構建緩存,可以通過 --no-cache
選項來禁用它:
$ sudo docker build --no-cache ./webapp