首先來看一張圖,簡單明了
COPY
格式:
COPY <源路徑>... <目標路徑>
COPY ["<源路徑1>",... "<目標路徑>"]
和 RUN
指令一樣,也有兩種格式,一種類似于命令行,一種類似于函數調用。
COPY
指令將從構建上下文目錄中 <源路徑>
的文件/目錄復制到新的一層的鏡像內的 <目標路徑>
位置。比如:
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
指令將會自動解壓縮這個壓縮文件到<目標路徑>
去。
在某些情況下,這個自動解壓縮的功能非常有用,比如官方鏡像 ubuntu 中:
FROM scratch
ADD ubuntu-xenial-core-cloudimg-amd64-root.tar.gz /
但在某些情況下,如果我們真的是希望復制個壓縮文件進去,而不解壓縮,這時就不可以使用 ADD 命令了。
在 Docker 官方的 Dockerfile 最佳實踐文檔 中要求,盡可能的使用 COPY,因為 COPY 的語義很明確,就是復制文件而已,而 ADD 則包含了更復雜的功能,其行為也不一定很清晰。最適合使用 ADD 的場合,就是所提及的需要自動解壓縮的場合。
另外需要注意的是,ADD 指令會令鏡像構建緩存失效,從而可能會令鏡像構建變得比較緩慢。
因此在 COPY 和 ADD 指令中選擇的時候,可以遵循這樣的原則,所有的文件復制均使用 COPY 指令,僅在需要自動解壓縮的場合使用
CMD
CMD 指令的格式和 RUN 相似,也是兩種格式:
shell 格式:CMD <命令>
exec 格式:CMD ["可執行文件", "參數1", "參數2"...]
VOLUME
格式為:
VOLUME ["<路徑1>", "<路徑2>"...]
-
VOLUME <路徑>
之前我們說過,容器運行時應該盡量保持容器存儲層不發生寫操作,對于數據庫類需要保存動態數據的應用,其數據庫文件應該保存于卷(volume)中,后面的章節我們會進一步介紹 Docker 卷的概念。為了防止運行時用戶忘記將動態文件所保存目錄掛載為卷,在 Dockerfile 中,我們可以事先指定某些目錄掛載為匿名卷,這樣在運行時如果用戶不指定掛載,其應用也可以正常運行,不會向容器存儲層寫入大量數據。
VOLUME /data
這里的 /data 目錄就會在運行時自動掛載為匿名卷,任何向 /data 中寫入的信息都不會記錄進容器存儲層,從而保證了容器存儲層的無狀態化。當然,運行時可以覆蓋這個掛載設置。比如:
docker run -d -v mydata:/data xxxx
在這行命令中,就使用了 mydata 這個命名卷掛載到了 /data 這個位置,替代了 Dockerfile 中定義的匿名卷的掛載配置。
EXPOSE
格式為 EXPOSE <端口1> [<端口2>...]
。
EXPOSE 指令是聲明運行時容器提供服務端口,這只是一個聲明,在運行時并不會因為這個聲明應用就會開啟這個端口的服務。在 Dockerfile 中寫入這樣的聲明有兩個好處,一個是幫助鏡像使用者理解這個鏡像服務的守護端口,以方便配置映射;另一個用處則是在運行時使用隨機端口映射時,也就是 docker run -P
時,會自動隨機映射 EXPOSE 的端口。
要將 EXPOSE 和在運行時使用 -p <宿主端口>:<容器端口> 區分開來。-p,是映射宿主端口和容器端口,換句話說,就是將容器的對應端口服務公開給外界訪問,而 EXPOSE 僅僅是聲明容器打算使用什么端口而已,并不會自動在宿主進行端口映射。
WORKDIR
格式為 WORKDIR <工作目錄路徑>
。
使用 WORKDIR
指令可以來指定工作目錄(或者稱為當前目錄),以后各層的當前目錄就被改為指定的目錄,如該目錄不存在,WORKDIR
會幫你建立目錄。
之前提到一些初學者常犯的錯誤是把 Dockerfile
等同于 Shell 腳本來書寫,這種錯誤的理解還可能會導致出現下面這樣的錯誤:
RUN cd /app
RUN echo "hello" > world.txt
如果將這個 Dockerfile
進行構建鏡像運行后,會發現找不到 /app/world.txt
文件,或者其內容不是 hello
。原因其實很簡單,在 Shell 中,連續兩行是同一個進程執行環境,因此前一個命令修改的內存狀態,會直接影響后一個命令;而在 Dockerfile
中,這兩行 RUN
命令的執行環境根本不同,是兩個完全不同的容器。這就是對 Dockerfile
構建分層存儲的概念不了解所導致的錯誤。
之前說過每一個 RUN
都是啟動一個容器、執行命令、然后提交存儲層文件變更。第一層 RUN cd /app
的執行僅僅是當前進程的工作目錄變更,一個內存上的變化而已,其結果不會造成任何文件變更。而到第二層的時候,啟動的是一個全新的容器,跟第一層的容器更完全沒關系,自然不可能繼承前一層構建過程中的內存變化。
因此如果需要改變以后各層的工作目錄的位置,那么應該使用 WORKDIR
指令。