Dockerfile 最佳實踐

【導讀】工作中經常遇到要打容器鏡像的任務,今天的文章介紹了在打容器鏡像方面的最佳實踐。

Dockerfile:Docker 特有的鏡像構建定義文件。

一、Dockerfile 簡介

Dockerfile 是 Docker 中用於定義鏡像自動化構建流程的配置文件,在 Dockerfile 中,包含了構建鏡像過程中需要執行的命令和其他操作。通過 Dockerfile 可以更加清晰、明確的給定 Docker 鏡像的製作過程,由於僅是簡單、小體積的文件,在網絡等介質中傳遞的速度快,能夠更快的實現容器遷移和集羣部署。

img

通常來說,對 Dockerfile 的定義就是針對一個名爲 Dockerfile 的文件,其雖然沒有擴展名,但本質就是一個文本文件,可以通過常見的文本編輯器或者 IDE 創建和編輯它。

Dockerfile 的內容很簡單,主要以兩種形式呈現,一種是註釋行,另一種是指令行。Dockerfile 中,擁有一套獨立的指令語法,用於給出鏡像構建過程中所要執行的過程。Dockerfile 裏的指令行,就是由指令與其相應的參數所組成。

用開發中的常見流程來類比 Dockerfile。

在一個完整的開發、測試、部署過程中,程序運行環境的定義通常是由開發人員來進行的,因爲開發更熟悉程序運轉的各個細節,更適合搭建適合程序的運行環境。

以此爲前提,爲了方便測試和運維搭建相同的程序運行環境,常用的做法是由開發人員編寫一套環境搭建手冊,幫助測試人員和運維人員瞭解環境搭建的流程。

Dockerfile 就像這樣一個環境搭建手冊,因爲其中包含的就是一個構建容器的過程。

而比環境搭建手冊更好的是,Dockerfile 在容器體系下能夠完成自動構建,既不需要測試和運維人員深入理解環境中各個軟件的具體細節,也不需要人工執行每一個搭建流程。

相對於提交容器修改再進行鏡像遷移的方式相比,使用 Dockerfile 有很多優勢:

實際開發使用中很少會選擇容器提交這種方法來構建鏡像,而是幾乎採用 Dockerfile 來製作鏡像。

二、環境搭建與鏡像構建

A、Dockerfile 編寫

以下爲完整的 Dockerfile,用於構建 Docker 官方所提供的 Redis 鏡像。

FROM debian:stretch-slim

RUN groupadd -r redis && useradd -r -g redis redis

ENV GOSU_VERSION 1.10
RUN set -ex; \
	\
	fetchDeps=" \
		ca-certificates \
		dirmngr \
		gnupg \
		wget \
	"; \
	apt-get update; \
	apt-get install -y --no-install-recommends $fetchDeps; \
	rm -rf /var/lib/apt/lists/*; \
	\
	dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \
	wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \
	wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch.asc"; \
	export GNUPGHOME="$(mktemp -d)"; \
	gpg --keyserver ha.pool.sks-keyservers.net --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \
	gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \
	gpgconf --kill all; \
	rm -r "$GNUPGHOME" /usr/local/bin/gosu.asc; \
	chmod +x /usr/local/bin/gosu; \
	gosu nobody true; \
	\
	apt-get purge -y --auto-remove $fetchDeps

ENV REDIS_VERSION 3.2.12
ENV REDIS_DOWNLOAD_URL http://download.redis.io/releases/redis-3.2.12.tar.gz
ENV REDIS_DOWNLOAD_SHA 98c4254ae1be4e452aa7884245471501c9aa657993e0318d88f048093e7f88fd

RUN set -ex; \
	\
	buildDeps=' \
		wget \
		\
		gcc \
		libc6-dev \
		make \
	'; \
	apt-get update; \
	apt-get install -y $buildDeps --no-install-recommends; \
	rm -rf /var/lib/apt/lists/*; \
	\
	wget -O redis.tar.gz "$REDIS_DOWNLOAD_URL"; \
	echo "$REDIS_DOWNLOAD_SHA *redis.tar.gz" | sha256sum -c -; \
	mkdir -p /usr/src/redis; \
	tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1; \
	rm redis.tar.gz; \
	\
# disable Redis protected mode [1] as it is unnecessary in context of Docker
# (ports are not automatically exposed when running inside Docker, but rather explicitly by specifying -p / -P)
# [1]: https://github.com/antirez/redis/commit/edd4d555df57dc84265fdfb4ef59a4678832f6da
	grep -q '^#define CONFIG_DEFAULT_PROTECTED_MODE 1$' /usr/src/redis/src/server.h; \
	sed -ri 's!^(#define CONFIG_DEFAULT_PROTECTED_MODE) 1$!\1 0!' /usr/src/redis/src/server.h; \
	grep -q '^#define CONFIG_DEFAULT_PROTECTED_MODE 0$' /usr/src/redis/src/server.h; \
# for future reference, we modify this directly in the source instead of just supplying a default configuration flag because apparently "if you specify any argument to redis-server, [it assumes] you are going to specify everything"
# see also https://github.com/docker-library/redis/issues/4#issuecomment-50780840
# (more exactly, this makes sure the default behavior of "save on SIGTERM" stays functional by default)
	\
	make -C /usr/src/redis -j "$(nproc)"; \
	make -C /usr/src/redis install; \
	\
	rm -r /usr/src/redis; \
	\
	apt-get purge -y --auto-remove $buildDeps

RUN mkdir /data && chown redis:redis /data
VOLUME /data
WORKDIR /data

COPY docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT ["docker-entrypoint.sh"]

EXPOSE 6379
CMD ["redis-server"]

B、Dockerfile 結構

當調用構建命令讓 Docker 通過 Dockerfile 構建鏡像時,Docker 會逐一按順序解析 Dockerfile 中的指令,並根據它們不同的含義執行不同的操作。

可以將 Dockerfile 的指令簡單分爲五大類:

三、常見 Dockerfile 指令

以下常見的 Dockerfile 指令,基本包含常用的 90% 功能。

A、FROM

通常來說,不會從零開始搭建一個鏡像,而是會選擇一個已經存在的鏡像作爲新鏡像的基礎。

在 Dockerfile 裏,通過 FROM 指令指定一個基礎鏡像,之後所有的指令都是基於這個鏡像所展開的。在鏡像構建的過程中,Docker 也會先獲取到這個給出的基礎鏡像,再從這個鏡像上進行構建操作。

FROM 指令支持三種形式:

FROM <image> [AS <name>]
FROM <image>[:<tag>] [AS <name>]
FROM <image>[@<digest>] [AS <name>]

選擇一個基礎鏡像是構建新鏡像的根本,則 Dockerfile 中的第一條指令必須是 FROM 指令,因爲沒有了基礎鏡像,一切構建過程都無法開展。在 Dockerfile 中可以多次出現 FROM 指令,當 FROM 第二次或者之後出現時,表示在此刻構建時,要將當前指出鏡像的內容合併到此刻構建鏡像的內容裏。

B、RUN

鏡像的構建雖然是按照指令執行的,但指令只是引導,最終大部分內容還是控制檯中對程序發出的命令,而 RUN 指令就是用於向控制檯發送命令的指令。

在 RUN 指令之後,直接拼接上需要執行的命令,在構建時,Docker 就會執行這些命令,並將它們對文件系統的修改記錄下來,形成鏡像的變化。

RUN <command>
RUN ["executable", "param1", "param2"]

RUN 指令支持 **** 換行,如果單行的長度過長,可以對內容進行切割,方便閱讀。

C、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 中給出的命令。

D、EXPOSE

由於構建鏡像時更瞭解鏡像中應用程序的邏輯,也更加清楚它需要接收和處理來自哪些端口的請求,所以在鏡像中定義端口暴露顯然是更合理的做法。

通過 EXPOSE 指令就可以爲鏡像指定要暴露的端口。

EXPOSE <port> [<port>/<protocol>...]

通過 EXPOSE 指令配置了鏡像的端口暴露定義,基於這個鏡像所創建的容器,在被其他容器通過 **--link ** 選項連接時,就能夠直接允許來自其他容器對這些端口的訪問了。

E、VOLUME

在一些程序裏,需要持久化一些數據,比如數據庫中存儲數據的文件夾就需要單獨處理,可以通過數據捲來處理這些問題。

使用數據卷需要在創建容器時通過 **-v ** 選項來定義,而有時候由於鏡像的使用者對鏡像瞭解程度不高,會漏掉數據卷的創建,從而引起不必要的麻煩。

製作鏡像的人是最清楚鏡像中程序工作的各項流程的,所以製作人來定義數據卷也是最合適的。在 Dockerfile 裏,通過了 VOLUME 指令來定義基於此鏡像的容器所自動建立的數據卷。

VOLUME ["/data"]

在 VOLUME 指令中定義的目錄,在基於新鏡像創建容器時,會自動建立爲數據卷,不需要再單獨使用 **-v ** 選項來配置。

F、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 指令的定義方式完全一樣,兩者的區別主要在於 ADD 能夠支持使用網絡端的 URL 地址作爲 src 源,並且在源文件被識別爲壓縮包時,自動進行解壓,而 COPY 沒有這兩個能力。

雖然看上去 COPY 能力稍弱,但對於那些不希望源文件被解壓或沒有網絡請求的場景,COPY 更爲簡單。

四、構建鏡像

編寫 Dockerfile 後,通過 ** docker build ** 命令構建鏡像。

docker build ./webapp

docker build 的參數爲目錄路徑(本地路徑或 URL 路徑),該目錄會作爲構建的環境目錄,例如,使用 COPY 或是 ADD 拷貝文件到構建的新鏡像時,會以這個目錄作爲基礎目錄。

默認情況下,docker build 也會從這個目錄下尋找名爲 Dockerfile 的文件,將它作爲 Dockerfile 內容的來源。如果 Dockerfile 文件路徑不在這個目錄下,或者有另外的文件名,可以通過 **-f ** 選項單獨給出 Dockerfile 文件的路徑。

docker build -t webapp:latest -f ./webapp/a.Dockerfile ./webapp

在構建時帶上 **-t ** 選項,用它來指定新生成鏡像的名稱。

docker build -t webapp:latest ./webapp

A、構建中使用變量

在實際編寫 Dockerfile 時,與搭建環境相關的指令佔有大部分比例的指令。搭建程序所需運行環境時,難免涉及到一些可變量,例如依賴軟件的版本,編譯的參數等等。

可以直接將這些數據寫入到 Dockerfile 中,但是這些可變量會經常調整,在 Dockerfile 裏,可以用 ARG 指令來建立一個參數變量,可以在構建時通過構建指令傳入這個參數變量,並且在 Dockerfile 裏使用它。

例如,希望通過參數變量控制 Dockerfile 中某個程序的版本,在構建時安裝我們指定版本的軟件,可以通過 ARG 定義的參數作爲佔位符,替換版本定義的部分。

FROM debian:stretch-slim

ARG TOMCAT_MAJOR
ARG TOMCAT_VERSION

RUN wget -O tomcat.tar.gz "https://www.apache.org/dyn/closer.cgi?action=download&file

以上例子中,將 Tomcat 的版本號通過 ARG 指令定義爲參數變量,在調用下載 Tomcat 包時,使用變量替換掉下載地址中的版本號。通過這樣的定義,就可以在不對 Dockerfile 進行大幅修改的前提下,輕鬆實現對 Tomcat 版本的切換並重新構建鏡像了。

如果需要通過這個 Dockerfile 文件構建 Tomcat 鏡像,可以在構建時通過 ** docker build --build-arg ** 選項來設置參數變量。

docker build --build-arg TOMCAT_MAJOR=8 --build-arg TOMCAT_VERSION=8.0.53 -t tomcat:8.0 ./tomcat

B、環境變量

環境變量也是用來定義參數的東西,與 ARG 指令相類似,環境變量的定義是通過 ENV 指令來完成的。

FROM debian:stretch-slim

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&file

環境變量的使用方法與參數變量一樣,都是能夠直接替換指令參數中的內容。

與參數變量只能影響構建過程不同,環境變量不僅能夠影響構建,還能夠影響基於此鏡像創建的容器。環境變量設置的實質,其實就是定義操作系統環境變量,所以在運行的容器裏,一樣擁有這些變量,而容器中運行的程序也能夠得到這些變量的值。

另一個不同點是,環境變量的值不是在構建指令中傳入的,而是在 Dockerfile 中編寫的,所以如果要修改環境變量的值,需要到 Dockerfile 修改。不過即使這樣,只要我們將 ENV 定義放在 Dockerfile 前部容易查找的地方,其依然可以很快的幫助切換鏡像環境中的一些內容。

由於環境變量在容器運行時依然有效,所以運行容器時還可以對其進行覆蓋,在創建容器時使用 **-e 或是 --env ** 選項,可以對環境變量的值進行修改或定義新的環境變量。

docker run -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:5.7

這種用法在開發中非常常見,也正是因爲這種允許運行時配置的方法存在,環境變量和定義它的 ENV 指令,是更常使用的指令,會優先選擇它們來實現對變量的操作。

通過 ENV 指令和 ARG 指令所定義的參數,在使用時都是採用 **$ + NAME ** 這種形式來佔位的,所以它們之間的定義就存在衝突的可能性。對於這種場景,ENV 指令所定義的變量,永遠會覆蓋 ARG 所定義的變量,即使它們定時的順序是相反的。

C、合併命令

上文中 Redis 鏡像的 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 先會基於上條命令的結果啓動一個容器,在容器中運行這條指令的內容,之後將結果打包成一個鏡像層,如此反覆,最終形成鏡像。

img

基於這個原理,絕大多數鏡像會將命令合併到一條指令中,這種做法不但減少了鏡像層的數量,也減少了鏡像構建過程中反覆創建容器的次數,提高了鏡像構建的速度。

D、構建緩存

Docker 在鏡像構建的過程中,還支持緩存策略來提高鏡像的構建速度。

由於鏡像是多個指令所創建的鏡像層組合而得,如果判斷新編譯的鏡像層與已經存在的鏡像層未發生變化,則完全可以直接利用之前構建的結果,而不需要再執行這條構建指令,這就是鏡像構建緩存的原理。

基於這個原則,在條件允許的前提下,更建議將不容易發生變化的搭建過程放到 Dockerfile 的前部,充分利用構建緩存提高鏡像構建的速度。另外,指令的合併也不宜過度,而是將易變和不易變的過程拆分,分別放到不同的指令裏。

在另外一些時候,不希望 Docker 在構建鏡像時使用構建緩存,這時可以通過 **--no-cache ** 選項來禁用它。

docker build --no-cache ./webapp

五、臨摹案例

編寫 Dockerfile,閱讀和思考前人的作品是必不可少的。

Docker 官方提供的 Docker Hub 是 Docker 鏡像的中央倉庫,它除了鏡像豐富之外,帶來的另一項好處就是其大部分鏡像都是能夠直接提供 Dockerfile 文件,可以進行學習。

轉自:isisiwish

zhuanlan.zhihu.com/p/57335983

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/gdpI4W84YkAZNcbf_u_5Dg