前言:鏡像的定制實際上就是定制每一層所添加的配置文件,如果我們可以把每一層的修改、安裝、構建、操作的命令都寫入一個腳本,然后用這個腳本來構建、定制鏡像,那么鏡像構建透明性的問題、體積的問題就會得到解決,這個腳本就是 Dockerfile; Dockerfile 是一個文本文件,其內包含了一條條的指令,每一條指令構建一層,每一層指令的內容,就是描述該層應該如何構建,然后通過 commit 構成新的鏡像。
Dockerfile 參數
FROM
1,FROM:指定基礎鏡像,必須是第一條指令
# 定制 nginx 鏡像的 Dockerfile FROM nginx RUN echo '<h1>Hello,Docker!</h1>' > /usr/share/nginx/html/index.html
注: Docker Hub 上有很多高質量的服務類的官方鏡像可以拿來直接使用,比如:nginx 、redis 、mysql 、php 、mongo \ tomcat 等,可以在其中找最符合的一個進行定制
另外也有一些方便開發、構建、運行各種語言的鏡像,比如:node 、python 、golang 等
如果沒有找到對應服務的鏡像,官方鏡像中還提供了一些更為基礎的操作系統鏡像,比如:ubuntu 、debian 、fedora 、centos 等,也可以利用這些操作系統提供的軟件庫
RUN
2,RUN :用來執行命令行命令,格式有兩種:
1, shell 格式: RUN <命令>,就像直接在命令行中輸入的命令一樣
2, exec 格式:RUN ["可執行文件",“參數1”,“參數2”],更像是函數調用中的格式
warning:每一個RUN命令都會在 docker鏡像中新建一層,所以應該盡量少用 RUN 命令,而且要在RUN 的最后要做必要的清除工作

# 構建層次太多,未做清理工作 FROM debian:stretch RUN apt-get update RUN apt-get install -y gcc libc6-dev make wget RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz" RUN mkdir -p /usr/src/redis RUN tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 RUN make -C /usr/src/redis RUN make -C /usr/src/redis install

# 一層構建,並在最后清理壓縮包等緩存文件 FROM debian:stretch RUN buildDeps='gcc libc6-dev make wget' \ && apt-get update \ && apt-get install -y $buildDeps \ && wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz" \ && mkdir -p /usr/src/redis \ && tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \ && make -C /usr/src/redis \ && make -C /usr/src/redis install \ && rm -rf /var/lib/apt/lists/* \ && rm redis.tar.gz \ && rm -r /usr/src/redis \ && apt-get purge -y --auto-remove $buildDeps
Dockerfile 上下文
3, Dockerfile 上下文:

# 構建新的鏡像 # -t :指定鏡像名稱和 tag # . : 上下文,表示將本路徑下的所有文件打包上傳到 docker daemon,進行定制鏡像 $ docker build -t nginx:v3 .
COPY
4, COPY : 用來從構建上下文目錄中<原路徑>的文件/目錄復制到新一層鏡像內的 <目標路徑>位置,格式有兩種:
1,shell 格式:COPY [--chown=<user>:<group>] <原路徑>...<目標路徑>
2,exec 合適:COPY[--chown=<user>:<group>] ["原路徑1",... "<目標路徑>"]
原路徑:可以是多個,甚至可以是通配符
目標路徑:可以是容器內的絕對路徑,也可以是相對於工作目錄的相對路徑(工作目錄可以用 WORKDIR 指令來指定,不需要事先創建,會自動創建)

1 # 利用 通配符 進行復制 2 COPY hom* /mydir/ 3 COPY hom?.txt /mydir/
note : COPY 會將原文件的各種數據都保留,比如 讀、寫、執行權限,可以通過 --chown=<user>:<group> 選項來改變文件的所屬用戶及所屬組。
ADD
5,ADD : 和 COPY 指令的功能,性質基本一致,也可以通過 --chown 改變文件所屬用戶和所屬組,但是在 COPY 的基礎上增加了一些功能:
1,原路徑為 URL : Docker 會試圖下載這個文件放到 目標路徑去,默認下載后的文件權限為 600,如果想要修改權限或者下載的是壓縮包,需要解壓,則還需要額外的一層 RUN 進行調整,還不如直接用 RUN 指令用 wget 進行下載,處理權限,解壓縮,然后清理無用文件更合理,所以該命令不常用,而且不推薦使用。
2,原路徑為 tar 壓縮包 : 如果壓縮文件格式為 gzip , bzip2 以及 xz 的情況下,ADD 指令將自動解壓這個壓縮文件到 <目標路徑> 去,只有此種情況適合使用 ADD 指令。
note: ADD 指令可能會使鏡像構建緩存失效,從而可能會令鏡像的構建變的比較緩慢,鏡像構造緩存點擊這里查看
CMD
6,CMD : 和 RUN 指令相似,也是兩種格式:
1,shell 格式:CMD <命令>
2,exec 格式 : CMD ["可執行文件",“參數1”,“參數2” ...]
3,參數格式列表:在指定了 ENTRYPOINT 指令后,用 CMD 指定具體的參數
CMD 指令用於指定默認的容器主進程的啟動命令的,例如 ubuntu 默認的 CMD 是 bash ,我們也可以在容器運行時指定運行別的命令,如:
# 直接進入 bash $ docker run -it ubuntu # 修改默認的 CMD # docker run -it ubuntu cat /etc/os-release
note1: 在指令格式上,一般推薦使用 exec 格式,這類格式在解析時會被解析為 JSON 數組,因此一定要用 雙引號 “ 而不要使用單引號 。
# 如果執行 CMD echo $HOME # 實際執行會變更為: CMD ["sh" "-c" "echo $HOME"]
note2 : 容器的前台執行和后台執行問題
注:Docker 不是虛擬機,容器中的應用都應該以前台執行,而不能像虛擬機用 systemd 去啟動后台服務,容器內沒有后台服務的概念。例如:

# 錯誤代碼 # 目的:啟動 nginx 在后台以守護進程的形式在運行 CMD service nginx start # 實際上執行 # sh 為主進程,執行完成進程退出,導致容器也會退出 CMD ["sh" "-c" "service nginx start"] # 正確做法 # nginx :可執行文件 CMD ["nginx", "-g", "daemon off;"]
ENTRYPOINT
7, ENTRYPOINT:格式和 RUN 指令格式一樣,分為 exec 格式和 shell 格式,目的和 CMD 一樣,都是在指定容器啟動程序及參數;當指定了 ENTRYPOINT 后,CMD 的含義就發生了變化,不再是直接的運行其命令,而是將 CMD 的內容作為參數傳給 ENTRYPOINT 指令,換句話說實際執行時,將變為: <ENTRYPOINT>"<CMD>"
用處 1 : 讓鏡像變成向命令一樣使用:

# 如果我們需要一個得知自己當前的公網 IP 的鏡像 # Dockerfile 內容: FROM ubuntu:18.04 RUN apt-get update \ && apt-get install -y curl \ && rm -rf /var/lib/apt/lists/* CMD ["curl", "-s", "https://ip.cn"] # 構建鏡像 docker build -t myip . # 查詢 ip 操作 # 不能添加參數,如上面的 -s 參數 docker run myip # 希望顯示 HTTP 頭信息,需要加上 -i 參數 # 試圖添加參數,會報錯,因為在容器后加入參數,會被解析成 CMD 命令,但 -i 不是任何命令 docker run myip -i # 用 ENTRYPOINT 的方式 # Dockerfile 內容: FROM ubuntu:18.04 RUN apt-get update \ && apt-get install -y curl \ && rm -rf /var/lib/apt/lists/* ENTRYPOINT ["curl", "-s", "https://ip.cn"] # 查詢 ip 操作,可加參數 # 此時 CMD 的內容 -i 傳遞給了主進程 curl docker run myip -i
用處 2 : 應用運行前的准備工作:比如數據庫配置,初始化工作,此時可以傳 ENTRYPOINT 一個腳本,然后通過 CMD 指定參數,在腳本最后執行

1 # allow the container to be started with `--user` 2 # Dockerfile 3 FROM alpine:3.4 4 RUN addgroup -S redis && adduser -S -G redis redis 5 ... 6 ENTRYPOINT ["docker-entrypoint.sh"] 7 EXPOSE 6379 8 CMD ["redis-server"] 9 10 # docker-entrypoint.sh 腳本文件 11 #!/bin/bash 12 if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then 13 chown -R redis . 14 exec su-exec redis "$0" "$@" 15 fi 16 exec "$@"
ENV
8, ENV : 用來設置環境變量,格式有兩種:
1,ENV <key> <value>
2,ENV <key1>=<value1> <key2>=<value2>...
在設置了環境變量之后,無論是后面的其它指令,如 RUN ,還是運行時的應用,都可以直接使用這里定義的環境變量

# 定義環境變量 ENV VERSION=1.0 DEBUG=ON \ NAME="Happy Feet" # 官方 node 鏡像 Dockerfile 中: ENV NODE_VERSION 7.2.0 RUN 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
ARG
9, ARG : 構建參數,格式:
1,ARG <參數名>[=<默認值>]
構建參數和 ENV 的效果一樣,都是設置環境變量,所不同的是,ARG 所設置的是構建環境的環境變量,在將來容器運行時是不會存在這些環境變量的。
VOLUME
10,VOLUME:定義匿名卷,格式為:
1,VOLUME ["<路徑1>”,”<路徑2>"...]
2,VOLUME <路徑>
之前說過,容器運行時應該盡量保持容器存儲層不發生寫操作,對於數據庫類需要保存動態數據的應用,其數據庫文件應該保存在卷中,為了防止運行時用戶忘記將動態文件所保存目錄掛載為卷,在 Dockerfile 中,我們可以事先指定某些目錄掛載為匿名卷,這樣在運行時如果用戶不指定掛載,其應用也可以正常運行,不會向容器存儲層寫入大量數據。

# /data 目錄會在運行時自動掛載為匿名卷 VOLUME /data # 運行時也可以覆蓋這個掛載設置 # 用 mydata 這個命名卷掛載到了 /data 這個位置,代替 Dockerfile 中的匿名卷的掛載配置 docker run -d -v mydata:/data xxxx
EXPOSE
11,EXPOSE:聲明端口,格式為:
EXPOSE <端口1> [<端口2>...]
該條指令是聲明運行時容器提供的服務端口,這只是一個聲明,在運行時並不會因為這個聲明應用就會開啟這個端口的服務。這樣聲明帶來兩個好處:
1,幫助鏡像使用者理解這個鏡像服務的守護端口,以方便配置映射
2,在運行時使用隨機端口映射,也就是 docker run -P 時,會自動隨機映射 EXPOSE 的端口
note: 要將 EXPOSE 和在運行時使用 -p <宿主端口>:<容器端口> 區分開來。-p 是映射宿主端口和容器端口,就是將容器的對應端口服務公開給外界訪問,而 EXPOSE 僅僅是聲明容器打算使用什么端口而已,並不會在宿主進行端口映射。
WORKDIR
12, WORKDIR : 指定工作目錄,格式為:
WORKDIR <工作目錄路徑>
該條指令可以來指定工作目錄(或者稱為當前目錄),以后各層的當前目錄就被改為指定的目錄,如果該目錄不存在,則會自動建立。

# 常見錯誤 $ RUN cd /app $ RUN echo "hello" > word.txt /× 如果將這個 Dockerfile 進行構建鏡像運行后,會發現根本找不到 /app/word.txt 文件,這是 因為在 Dockerfile 中,這兩行 RUN 命令的執行環境根本不同,是兩個完全不同的容器。 沒一個 RUN 都是啟動一個容器、執行命令、然后提交存儲層文件變更;第一層的執行僅僅是 當前進程的工作目錄變更,一個內存上的變化而已,其結果不會造成任何文件改變;而到了第 二層的時候,啟動的是一個全新的容器,跟第一層的容器更完全沒有關系,自然不可能繼承前一層構建過程中的內存變化。 ×/ /× 因此,如果需要改變以后各層的工作目錄的位置,那么應該使用 WORKDIR 指令 ×/
USER
13,USER:指定當前用戶,格式為:
USER <用戶名>[:<用戶組>]
該條指令和 WORKDIR 相似,都是改變環境狀態並影響以后的層,WORKDIR 是改變工作目錄, USER 是改變之后層的執行 RUN ,CMD 以及 ENTRYPOINT 這類命令的身份。如果以 root 執行的腳本,在執行期間希望改變身份,比如希望以某個已經建立好的用戶來運行某個服務進程,不要使用 su 或者 sudo ,這些都需要比較麻煩的配置,而且在 TTY 缺失的情況下經常出錯,建議使用 gosu 。

# 建立 redis 用戶,並使用 gosu 換另一個用戶執行命令 RUN groupadd -r redis && useradd -r -g redis redis # 下載 gosu RUN wget -O /user/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"]
HEALTHCHECK
14,HEALTHCHECK:健康檢查,格式為:
HEALTHCHECK [選項] CMD <命令> :設置檢查容器健康狀況的命令
HEALTHCHECK NONE : 如果基礎鏡像有健康檢查指令,使用這行可以屏蔽掉其健康檢查指令
options:
--interval=<間隔> :兩次健康檢查的間隔,默認為 30s;
--timeout=<時長>: 健康檢查命令運行超時時間,如果超過這個時間,本次健康檢查就被視為失敗,默認 30s;
--retries=<次數> : 當連續失敗指定次數后,則將容器狀態視為 unhealthy ,默認 3 次;
return value:
0 : 成功
1:失敗
2:保留(不要使用這個值)
ONBUILD
15,ONBUILD:后構建指令,格式為:
ONBUILD <其它指令>
ONBUILD 是一個特殊的指令,它后面跟的是其它指令,比如 RUN,COPY 等,而這些指令,在當前鏡像構建時並不會被執行。只有當以以前鏡像為基礎鏡像,去構建下一級鏡像的時候才會被執行。