docker commit 命令除了學習以外,還有一些特殊的應用場合,好比被***後保存現場等。可是,不要使用 docker commit 定製鏡像,定製鏡像應該使用 Dockerfile 來完成。node
使用 docker commit 意味着全部對鏡像的操做都是黑箱操做,生成的鏡像也被稱爲黑箱鏡像,換句話說,就是除了製做鏡像的人知道執行過什麼命令、怎麼生成的鏡像,別人根本無從得知。並且,即便是這個製做鏡像的人,過一段時間後也沒法記清具體在操做的。雖然 docker diff 或許能夠告訴獲得一些線索,可是遠遠不到能夠確保生成一致鏡像的地步。這種黑箱鏡像的維護工做是很是痛苦的。mysql
Dockerfile 是一個文本文件,其內包含了一條條的指令(Instruction),每一條指令構建一層,所以每一條指令的內容,就是描述該層應當如何構建。linux
格式: * COPY <源路徑>... <目標路徑> * COPY ["<源路徑1>",... "<目標路徑>"] 好比: COPY package.json /usr/src/app/
說明: <源路徑> 能夠是多個,甚至能夠是通配符,其通配符規則要知足 Go 的 filepath.Match 規則,如: COPY hom* /mydir/ COPY hom?.txt /mydir/ <目標路徑> 能夠是容器內的絕對路徑,也能夠是相對於工做目錄的相對路徑(工做目錄能夠用 WORKDIR 指令來指定)。目標路徑不須要事先建立,若是目錄不存在會在複製文件前先行建立缺失目錄。
注意: 使用 COPY 指令,源文件的各類元數據都會保留。好比讀、寫、執行權限、文件變動時間等。這個特性對於鏡像定製頗有用。特別是構建相關文件都在使用 Git 進行管理的時候。
ADD 指令和 COPY 的格式和性質基本一致。可是在 COPY 基礎上增長了一些功能。
若是 <源路徑> 爲一個 tar 壓縮文件的話,壓縮格式爲 gzip, bzip2 以及 xz 的狀況下,ADD 指令將會自動解壓縮這個壓縮文件到 <目標路徑> 去。
最適合使用 ADD 的場合,就是當咱們須要自動解壓縮的場合。如官方鏡像 ubuntu 中:nginx
FROM scratch ##空白鏡像 ADD ubuntu-xenial-core-cloudimg-amd64-root.tar.gz / ...
因爲ADD 則包含了更復雜的功能,其行爲也不必定很清晰。它不像COPY 的語義很明確,就是複製文件而已。因此,咱們仍是儘量使用COPY吧。git
CMD 指令就是用於指定默認的容器主進程的啓動命令的。github
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 數組,所以必定要使用雙引號 ",而不要使用單引號。redis
shell格式: CMD echo $HOME exec格式: CMD [ "sh", "-c", "echo $HOME" ] 這就是爲何咱們可使用環境變量的緣由,由於這些環境變量會被 shell 進行解析處理。
Docker 不是虛擬機,容器中的應用都應該之前臺執行,而不是像虛擬機、物理機裏面那樣,用 upstart/systemd 去啓動後臺服務,容器內沒有後臺服務的概念。sql
好比,關於nginx的啓動,咱們錯誤的寫成:docker
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 可執行文件,而且之前臺形式運行,如:shell
CMD ["nginx", "-g", "daemon off;"]
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' ]; then chown -R redis . exec su-exec redis "$0" "$@" fi exec "$@"
該腳本的內容就是根據 CMD 的內容來判斷,若是是 redis-server 的話,則切換到 redis 用戶身份啓動服務器,不然依舊使用 root 身份執行。好比:
docker run -it redis id uid=0(root) gid=0(root) groups=0(root)
這個指令很簡單,就是設置環境變量.
格式有兩種:
* 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.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
在這裏先定義了環境變量 NODE_VERSION,其後的 RUN 這層裏,屢次使用 $NODE_VERSION 來進行操做定製。能夠看到,未來升級鏡像構建版本的時候,只須要更新 7.2.0 便可,Dockerfile 構建維護變得更輕鬆了。
下列指令能夠支持環境變量展開:
ADD、COPY、ENV、EXPOSE、LABEL、USER、WORKDIR、VOLUME、STOPSIGNAL、ONBUILD
能夠從這個指令列表裏感受到,環境變量可使用的地方不少,很強大。經過環境變量,咱們可讓一份 Dockerfile 製做更多的鏡像,只需使用不一樣的環境變量便可。
格式:
構建參數和 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)中,關於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 <port> [<port>/<protocol>...]
該EXPOSE指令通知Docker容器在運行時偵聽指定的網絡端口。能夠指定端口是偵聽TCP仍是UDP,若是未指定協議,則默認爲TCP。
EXPOSE 指令是聲明運行時容器提供服務端口,這只是一個聲明,在運行時並不會由於這個聲明應用就會開啓這個端口的服務。在 Dockerfile 中寫入這樣的聲明有兩個好處,一個是幫助鏡像使用者理解這個鏡像服務的守護端口,以方便配置映射;另外一個用處則是在運行時使用隨機端口映射時,也就是 docker run -P 時,會自動隨機映射 EXPOSE 的端口。
好比,我這裏編寫一個Dockerfile文件:
FROM busybox VOLUME /data EXPOSE 80 docker build -t busytest:v2 . docker run -itd --name busytest -P busytest:v2 docker 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:v2 docker 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 會幫你創建目錄。
在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>[:<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" ]