Docker鏡像構成和定制
利用 commit 理解鏡像構成
docker commit 命令應用場合
docker commit 命令除了學習之外,還有一些特殊的應用場合,比如被***后保存現場等。但是,不要使用 docker commit 定制鏡像,定制鏡像應該使用 Dockerfile 來完成。
慎用 docker commit
使用 docker commit 意味著所有對鏡像的操作都是黑箱操作,生成的鏡像也被稱為黑箱鏡像,換句話說,就是除了制作鏡像的人知道執行過什么命令、怎么生成的鏡像,別人根本無從得知。而且,即使是這個制作鏡像的人,過一段時間后也無法記清具體在操作的。雖然 docker diff 或許可以告訴得到一些線索,但是遠遠不到可以確保生成一致鏡像的地步。這種黑箱鏡像的維護工作是非常痛苦的。
使用 Dockerfile 定制鏡像
Dockerfile 是一個文本文件,其內包含了一條條的指令(Instruction),每一條指令構建一層,因此每一條指令的內容,就是描述該層應當如何構建。
Dockerfile 指令介紹
COPY 復制文件
格式:
* COPY <源路徑>... <目標路徑>
* COPY ["<源路徑1>",... "<目標路徑>"]
比如:
COPY package.json /usr/src/app/
說明:
<源路徑> 可以是多個,甚至可以是通配符,其通配符規則要滿足 Go 的 filepath.Match 規則,如:
COPY hom* /mydir/
COPY hom?.txt /mydir/
<目標路徑> 可以是容器內的絕對路徑,也可以是相對于工作目錄的相對路徑(工作目錄可以用 WORKDIR 指令來指定)。目標路徑不需要事先創建,如果目錄不存在會在復制文件前先行創建缺失目錄。
注意:
使用 COPY 指令,源文件的各種元數據都會保留。比如讀、寫、執行權限、文件變更時間等。這個特性對于鏡像定制很有用。特別是構建相關文件都在使用 Git 進行管理的時候。
ADD 更高級的復制文件
ADD 指令和 COPY 的格式和性質基本一致。但是在 COPY 基礎上增加了一些功能。
如果 <源路徑> 為一個 tar 壓縮文件的話,壓縮格式為 gzip, bzip2 以及 xz 的情況下,ADD 指令將會自動解壓縮這個壓縮文件到 <目標路徑> 去。
最適合使用 ADD 的場合,就是當我們需要自動解壓縮的場合。如官方鏡像 ubuntu 中:
FROM scratch ##空白鏡像
ADD ubuntu-xenial-core-cloudimg-amd64-root.tar.gz /
...
由于ADD 則包含了更復雜的功能,其行為也不一定很清晰。它不像COPY 的語義很明確,就是復制文件而已。所以,我們還是盡可能使用COPY吧。
CMD 容器啟動命令
CMD 指令就是用于指定默認的容器主進程的啟動命令的。
CMD 指令的格式和 RUN 相似,也是兩種格式:
* shell 格式:CMD <命令>
* exec 格式:CMD ["可執行文件", "參數1", "參數2"...]
* 參數列表格式:CMD ["參數1", "參數2"...]。在指定了 ENTRYPOINT 指令后,用 CMD 指定具體的參數。
在運行時可以指定新的命令來替代鏡像設置中的這個默認命令,比如,nginx:1.7.9 鏡像默認的 CMD 是 /bin/bash ,如果我們直接使用 docker run -it nginx:1.7.9 的話,會直接進入 bash 。
我們也可以在運行時指定運行別的命令,如 docker run -it nginx:1.7.9 cat /etc/os-release。這就是用 cat /etc/os-release 命令替換了默認的 /bin/bash 命令了,輸出了系統版本信息。
在指令格式上,一般推薦使用 exec 格式,這類格式在解析時會被解析為 JSON 數組,因此一定要使用雙引號 ",而不要使用單引號。
shell格式:
CMD echo $HOME
exec格式:
CMD [ "sh", "-c", "echo $HOME" ]
這就是為什么我們可以使用環境變量的原因,因為這些環境變量會被 shell 進行解析處理。
容器中應用在前臺執行和后臺執行的問題
Docker 不是虛擬機,容器中的應用都應該以前臺執行,而不是像虛擬機、物理機里面那樣,用 upstart/systemd 去啟動后臺服務,容器內沒有后臺服務的概念。
比如,關于nginx的啟動,我們錯誤的寫成:
CMD service nginx start
或
CMD systemctl start nginx
然后發現容器執行后就立即退出了。對于容器而言,其啟動程序就是容器應用進程,容器就是為了主進程而存在的,主進程退出,容器就失去了存在的意義,從而退出,其它輔助進程不是它需要關心的東西。
而使用 service nginx start 命令,則是希望 upstart 來以后臺守護進程形式啟動 nginx 服務。通過上面內容我們了解到 CMD service nginx start 會被理解為 CMD [ "sh", "-c", "service nginx start"],因此主進程實際上是 sh。那么當 service nginx start 命令結束后,sh 也就結束了,sh 作為主進程退出了,自然就會令容器退出。
正確的做法是直接執行 nginx 可執行文件,并且以前臺形式運行,如:
CMD ["nginx", "-g", "daemon off;"]
ENTRYPOINT 入口點
ENTRYPOINT 的格式和 RUN 指令格式一樣,分為 exec 格式和 shell 格式。
ENTRYPOINT 的目的和 CMD 一樣,都是在指定容器啟動程序及參數。ENTRYPOINT 在運行時也可以替代,不過比 CMD 要略顯繁瑣,需要通過 docker run 的參數 --entrypoint 來指定。
當指定了 ENTRYPOINT 后,CMD 的含義就發生了改變,不再是直接的運行其命令,而是將 CMD 的內容作為參數傳給 ENTRYPOINT 指令,換句話說實際執行時,將變為:
<ENTRYPOINT> "<CMD>"
那么有了 CMD 后,為什么還要有 ENTRYPOINT 呢?這種 <ENTRYPOINT> "<CMD>" 有什么好處么?讓我們來看兩個場景。
場景一:讓鏡像變成像命令一樣使用
假設我們需要一個得知自己當前公網 IP 的鏡像,那么可以先用 CMD 來實現:
FROM ubuntu:16.04
RUN apt-get update \&& apt-get install -y curl \&& rm -rf /var/lib/apt/lists/*
CMD [ "curl", "-s", "http://ip.cn" ]
假如我們使用 docker build -t myip . 來構建鏡像的話,如果我們需要查詢當前公網 IP,只需要執行:
$ docker run myip
當前 IP:61.148.226.66 來自:北京市 聯通
從上面的 CMD 中可以看到實質的命令是 curl,那么如果我們希望顯示 HTTP 頭信息,就需要加上 -i 參數。那么我們可以直接加 -i 參數給 docker run myip 么?
docker run myip -i
docker: Error response from daemon: invalid header field value "oci runtime error: container_linux.go:247: starting container process caused \"exec: \\\"-i\\\": executable file not found in $PATH\"\n".
執行報錯,executable file not found。之前我們說過,跟在鏡像名后面的是 command,運行時會替換 CMD 的默認值。因此這里的 -i 替換了原來的 CMD,而不是添加在原來的 curl -s http://ip.cn 后面。而 -i 根本不是命令,所以自然找不到。
那么如果我們希望加入 -i 這參數,我們就必須重新完整的輸入這個命令:
docker run myip curl -s http://ip.cn -i
這顯然不是很好的解決方案,而使用 ENTRYPOINT 就可以解決這個問題。現在我們重新用 ENTRYPOINT 來實現這個鏡像:
FROM ubuntu:16.04
RUN apt-get update \&& apt-get install -y curl \&& rm -rf /var/lib/apt/lists/*
ENTRYPOINT [ "curl", "-s", "http://ip.cn" ]
這次我們再來嘗試直接使用 docker run myip -i:
docker run myip
當前 IP:61.148.226.66 來自:北京市 聯通docker run myip -i
HTTP/1.1 200 OK
...
這次成功了。這是因為當存在 ENTRYPOINT 后,CMD 的內容將會作為參數傳給 ENTRYPOINT,而這里 -i 就是新的 CMD,因此會作為參數傳給 curl,從而達到了我們預期的效果。
場景二:應用運行前的準備工作
啟動容器就是啟動主進程,但有些時候,啟動主進程前,需要一些準備工作。
比如 mysql 類的數據庫,可能需要一些數據庫配置、初始化的工作,這些工作要在最終的 mysql 服務器運行之前解決。
此外,可能希望避免使用 root 用戶去啟動服務,從而提高安全性,而在啟動服務前還需要以 root 身份執行一些必要的準備工作,最后切換到服務用戶身份啟動服務。或者除了服務外,其它命令依舊可以使用 root 身份執行,方便調試等。
這些準備工作是和容器 CMD 無關的,無論 CMD 為什么,都需要事先進行一個預處理的工作。這種情況下,可以寫一個腳本,然后放入 ENTRYPOINT 中去執行,而這個腳本會將接到的參數(也就是 <CMD>)作為命令,在腳本最后執行。比如官方鏡像 redis 中就是這么做的:
FROM alpine:3.4
...
RUN addgroup -S redis && adduser -S -G redis redis
...
ENTRYPOINT ["docker-entrypoint.sh"]EXPOSE 6379
CMD [ "redis-server" ]
可以看到其中為了 redis 服務創建了 redis 用戶,并在最后指定了 ENTRYPOINT 為 docker-entrypoint.sh 腳本。
#!/bin/sh
...
# allow the container to be started with `--user`
if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; thenchown -R redis .exec su-exec redis "$0" "$@"
fiexec "$@"
該腳本的內容就是根據 CMD 的內容來判斷,如果是 redis-server 的話,則切換到 redis 用戶身份啟動服務器,否則依舊使用 root 身份執行。比如:
docker run -it redis id
uid=0(root) gid=0(root) groups=0(root)
ENV 設置環境變量
這個指令很簡單,就是設置環境變量.
格式有兩種:
* ENV <key> <value>
* ENV <key1>=<value1> <key2>=<value2>...
實例如下:
ENV MYSQL_ROOT_PASSWORD="123456" \MYSQL_DATABASE="edusoho" \ MYSQL_USER="edusoho" \MYSQL_PASSWORD="edusoho"
這個例子中演示了如何換行,以及對含有空格的值用雙引號括起來的辦法,這和 Shell 下的行為是一致的。
定義了環境變量,那么在后續的指令中,就可以使用這個環境變量。比如在官方 node 鏡像 Dockerfile 中,就有類似這樣的代碼:
ENV NODE_VERSION 7.2.0RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \&& curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \&& gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \&& grep " node-v$NODE_VERSION-linux-x64.tar.xz\$" SHASUMS256.txt | sha256sum -c - \&& tar -xJf "node-v$NODE_VERSION-linux-x64.tar.xz" -C /usr/local --strip-components=1 \&& rm "node-v$NODE_VERSION-linux-x64.tar.xz" SHASUMS256.txt.asc SHASUMS256.txt \&& ln -s /usr/local/bin/node /usr/local/bin/nodejs
在這里先定義了環境變量 NODE_VERSION,其后的 RUN 這層里,多次使用 $NODE_VERSION 來進行操作定制。可以看到,將來升級鏡像構建版本的時候,只需要更新 7.2.0 即可,Dockerfile 構建維護變得更輕松了。
下列指令可以支持環境變量展開:
ADD、COPY、ENV、EXPOSE、LABEL、USER、WORKDIR、VOLUME、STOPSIGNAL、ONBUILD
可以從這個指令列表里感覺到,環境變量可以使用的地方很多,很強大。通過環境變量,我們可以讓一份 Dockerfile 制作更多的鏡像,只需使用不同的環境變量即可。
ARG 構建參數
格式:
- ARG <參數名>[=<默認值>]
構建參數和 ENV 的效果一樣,都是設置環境變量。所不同的是,ARG 所設置的構建環境的環境變量,在將來容器運行時是不會存在這些環境變量的。但是不要因此就使用 ARG 保存密碼之類的信息,因為 docker history 還是可以看到所有值的。
Dockerfile 中的 ARG 指令是定義參數名稱,以及定義其默認值。該默認值可以在構建命令 docker build 中用 --build-arg <參數名>=<值> 來覆蓋。
在 1.13 之前的版本,要求 --build-arg 中的參數名,必須在 Dockerfile 中用 ARG 定義過了,換句話說,就是 --build-arg 指定的參數,必須在 Dockerfile 中使用了。如果對應參數沒有被使用,則會報錯退出構建。
從 1.13 開始,這種嚴格的限制被放開,不再報錯退出,而是顯示警告信息,并繼續構建。報錯信息如下例所示:
[Warning] One or more build-args [foo] were not consumed.
VOLUME 定義匿名卷
格式為:
- VOLUME ["<路徑1>", "<路徑2>"...]
- VOLUME <路徑>
容器運行時應該盡量保持容器存儲層不發生寫操作,對于數據庫類需要保存動態數據的應用,其數據庫文件應該保存于卷(volume)中,關于Docker 卷的概念和使用,可參考本庫文章
“Docker基本介紹和操作.md”。
為了防止運行時用戶忘記將動態文件所保存目錄掛載為卷,在 Dockerfile 中,我們可以事先指定某些目錄掛載為匿名卷,這樣在運行時如果用戶不指定掛載,其應用也可以正常運行,不會向容器存儲層寫入大量數據。
比如:
VOLUME /data
這里的 /data 目錄就會在運行時自動掛載為匿名卷,任何向 /data 中寫入的信息都不會記錄進容器存儲層,從而保證了容器存儲層的無狀態化。
如果我們想把這個匿名卷中的內容掛載到主機上呢?
docker run -itd --name busytest --mount type=bind,source=/teng,target=/data busytest:v1
或
docker run -itd --name busytest -v /teng:/data busytest:v1
EXPOSE 聲明端口
EXPOSE <port> [<port>/<protocol>...]
該EXPOSE指令通知Docker容器在運行時偵聽指定的網絡端口。可以指定端口是偵聽TCP還是UDP,如果未指定協議,則默認為TCP。
EXPOSE 指令是聲明運行時容器提供服務端口,這只是一個聲明,在運行時并不會因為這個聲明應用就會開啟這個端口的服務。在 Dockerfile 中寫入這樣的聲明有兩個好處,一個是幫助鏡像使用者理解這個鏡像服務的守護端口,以方便配置映射;另一個用處則是在運行時使用隨機端口映射時,也就是 docker run -P 時,會自動隨機映射 EXPOSE 的端口。
比如,我這里編寫一個Dockerfile文件:
FROM busybox
VOLUME /data
EXPOSE 80docker build -t busytest:v2 .docker run -itd --name busytest -P busytest:v2docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
30614a66bff0 busytest:v2 "sh" 3 seconds ago Up 2 seconds 0.0.0.0:32771->80/tcp busytest
無論EXPOSE設置如何,您都可以使用-p標志在運行時覆蓋它們。例如
docker run -itd --name busytest -p 8080:80 busytest:v2docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b07c5575afa1 busytest:v2 "sh" 3 seconds ago Up 2 seconds 0.0.0.0:8080->80/tcp busytest
WORKDIR 指定工作目錄
格式為 WORKDIR <工作目錄路徑>。
使用 WORKDIR 指令可以來指定工作目錄(或者稱為當前目錄),以后各層的當前目錄就被改為指定的目錄,如該目錄不存在,WORKDIR 會幫你建立目錄。
在Dockerfile中可以多次使用WORKDIR指令。如果提供了相對路徑,則它將相對于前一條WORKDIR指令的路徑 。例如:
WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd最終pwd命令的輸出Dockerfile將是 /a/b/c。
該WORKDIR指令可以解析先前使用的環境變量 ENV。您只能使用顯式設置的環境變量Dockerfile。例如:
ENV DIRPATH /path
WORKDIR $DIRPATH/$DIRNAME
RUN pwd最終pwd命令的輸出Dockerfile將是 /path/$DIRNAME
USER 指定當前用戶
格式:
USER <user>[:<group>] or
USER <UID>[:<GID>]
USER 指令和 WORKDIR 相似,都是改變環境狀態并影響以后的層。WORKDIR 是改變工作目錄,USER 則是改變之后層的執行 RUN, CMD 以及 ENTRYPOINT 這類命令的身份。
當然,和 WORKDIR 一樣,USER 只是幫助你切換到指定用戶而已,這個用戶必須是事先建立好的,否則無法切換。
如:
RUN groupadd -r redis && useradd -r -g redis redis
USER redis
RUN [ "redis-server" ]
如果以 root 執行的腳本,在執行期間希望改變身份,比如希望以某個已經建立好的用戶來運行某個服務進程,不要使用 su 或者 sudo,這些都需要比較麻煩的配置,而且在 TTY 缺失的環境下經常出錯。建議使用 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" ]
參考文檔
- Dockerfile 最佳實踐文檔
- Dockerfie 官方文檔
轉載于:https://blog.51cto.com/wutengfei/2156797