最近學習docker過程中,發現Dockerfile是一個非常重要的文檔,本文系統學習一下。
文檔是基于Docker v17.09 版本。
翻譯作品,原文請見官網英文文檔。
00 前言
Docker可以讀取Dockerfile
中的指令自動構建鏡像,Dockerfile
是一個文本文件,它包含很多命令,用戶可以在命令行上調用這些命令組裝鏡像。用戶可以使用docker build
來自動構建鏡像,它可以連續執行若干命令行指令。
本文將介紹在Dockerfile
中你可以使用命令,你讀完這篇文章之后,Dockerfile
Best Practices 是另一篇很好的指導。
01 用法
docker build
命令根據Dockerfile
和上下文來構建鏡像,構建過程的上下文是通過PATH
或者URL
指定的一系列文件。PATH
是一個本地文件系統的目錄,URL
是一個Git倉庫的位置。
上下文是一個遞歸的處理過程。因此,PATH可以包含任何的子目錄,
URL`包括倉庫和它的子模塊。下面是一個構建鏡像的命令的示例,使用當前目錄作為上下文:
$ docker build .
Sending build context to Docker daemon 6.51 MB
...
Build是通過Docker daemon(docker 守護進程),而不是 CLI(命令行界面)執行的。Build過程要做的第一件事是發送整個上下文(遞歸)到Docker的守護進程。最佳實踐是,開始創建一個空的文件夾作為上下文,然后將你的Dockerfile文件放在那個文件夾下,僅添加一些你在編譯Dockerfile過程中需要的文件。
注意:千萬不要使用根路徑
/
作為PATH
,這將導致Build會發送你的硬盤上的所有內容到Docker的守護進程。
在Build的上下文中為了使用Dockerfile中指定的一個文件,這個文件是某個指令(例如COPY
指令)用到的。為了提高Build的性能,通過添加.dockerignore
文件,可以排除上下文目錄中的某些文件和目錄,關于如何創建.dockerignore
文件更多信息見本文的下面章節。
一般認為,Dockerfile
文件都應該位于上下文的根目錄下,你可以在docker build
后使用-f
標識來指定你的文件系統中任意位置的Dockerfile文件。
$ docker build -f /path/to/a/Dockerfile .
你還可以指定用來存儲成功編譯的鏡像文件的倉庫和標簽:
$ docker build -t shykes/myapp .
Build的時候也可以為鏡像添加多個倉庫標簽,在你執行Build命令的時候添加多個-t
參數即可:
$ docker build -t shykes/myapp:1.0.2 -t shykes/myapp:latest .
Docker守護進程在執行Dockerfile
中的指令之前,會首先對Dockerfile
做一個初步校驗,如果有語法錯誤,它會返回一個錯誤:
$ docker build -t test/myapp .
Sending build context to Docker daemon 2.048 kB
Error response from daemon: Unknown instruction: RUNCMD
Docker守護進程是逐步執行Dockerfile
中的指令的,如果需要的話,會提交每個指令的結果到新的鏡像中,最后輸出新鏡像的的ID。Docker的守護進程也會自動清除你發送的上下文。
注意,每一條指令都是獨立執行的,因此在創建一個鏡像的時候,RUN cd /tmp
這條指令不會對下一條指令有任何影響。
無論任何可能的時候,Docker都將會重用中間狀態(緩存)的鏡像,這樣能夠明顯地加速docker build
的過程,這是通過控制臺輸出的信息Using cache
來標識的。(更多信息參見,在Dockerfile
的最佳實踐指導中的Build cache section):
$ docker build -t svendowideit/ambassador .
Sending build context to Docker daemon 15.36 kB
Step 1/4 : FROM alpine:3.2
---> 31f630c65071
Step 2/4 : MAINTAINER SvenDowideit@home.org.au
---> Using cache
---> 2a1c91448f5f
Step 3/4 : RUN apk update && apk add socat && rm -r /var/cache/
---> Using cache
---> 21ed6e7fbb73
Step 4/4 : CMD env | grep _TCP= | (sed 's/.*_PORT_\([0-9]*\)_TCP=tcp:\/\/\(.*\):\(.*\)/socat -t 100000000 TCP4-LISTEN:\1,fork,reuseaddr TCP4:\2:\3 \&/' && echo wait) | sh
---> Using cache
---> 7ea8aef582cc
Successfully built 7ea8aef582cc
僅在編譯那些有具有本地主鏈的鏡像時使用緩存,意思是這些鏡像的創建依賴前面的Build,或者整個鏡像鏈都已經通過docker load
加載進來了。如果你希望對一個指定鏡像使用build cache,你可以使用--cache-from
來指定,通過--cache-from
指定的鏡像不需要有一個主鏈,也可能是從其他的中心拉取的。
當你編譯完成的時候,你該學習 Pushing a repository to its registry。
02 格式
下面是Dockerfile
文件的格式:
# Comment
INSTRUCTION arguments
指令對字母大小寫是不敏感的,但是,習慣上將它們大寫,以便容易和參數區分開。
Docker是按照順序來執行Dockerfile
中的指令的。一個Dockerfile
文件必須以FROM
指令開始,FROM
指令指定了你正在編譯鏡像的基礎鏡像。在Dockerfile
文件中,FROM
指令的前面僅可以是一個或者多個ARG
指令,這些聲明的參數被用于FROM
指令。
Docker認為以#
開頭的行是注釋,除非這一行是一個有效的轉義的指令。#
標識出現在一行的任何其它地方,都會被認為是一個參數。就像下面這段:
# Comment
RUN echo 'we are running some # of cool things'
注釋中不支持繼續字符。
03 轉義指令
轉義指令是可選的,它會影響在Dockerfile
中后續行的處理方式。轉義指令并不會添加任何層到構建的鏡像中,也不會作為構建一個步驟展示,轉義指令是被寫作一個特殊類型的注釋,形式為# directive=value
,一個指令可能只會被使用一次。
一旦有一行注釋、空行或者編譯指令被執行,Docker就不會再檢查轉義指令了,而是將任何格式的轉義指令認為是注釋,不會嘗試去驗證它是否是轉義指令。因此所有的轉義指令必須放在Dockerfile
文件的第一行。
轉義指令不是大小寫敏感的,但是通常使用小寫的形式,習慣上任何的轉義指令后面都跟一個空行。轉義指令不支持續行符。
根據上面這些規則,下面是一些無效的轉義指令的例子:
由于續行符,導致無效:
# direc \
tive=value
由于出現兩次,導致無效:
# directive=value1
# directive=value2
FROM ImageName
由于出現在了編譯指令之后,被當作了注釋:
FROM ImageName
# directive=value
由于出現在了注釋之后,被當作了注釋,而不是轉義指令:
# About my dockerfile
# directive=value
FROM ImageName
未知的指令由于無法識別被當作了注釋,另外一個已知的指令由于出現在了注釋的后面,被當作了注釋而不是轉義指令。
# unknowndirective=value
# knowndirective=value
轉義指令中允許出現非斷行的空格,所以下面幾行都是相同的:
#directive=value
# directive =value
# directive= value
# directive = value
# dIrEcTiVe=value
下面的轉義指令是支持的:
escape
04 轉義符指令
# escape=\ (backslash)
或者
# escape=` (backtick)
escape
指令是用來設置Dockerfile中轉義字符的字符,如果不指定的話,默認的轉義字符是\
。
轉義字符不僅用在一行中的轉義字符上,也用在開啟一個新行。Dockerfile
中指令允許是多行的。注意,無論在Dockerfile
中是否包含escape
轉義指令,在RUN
命令中是不會執行轉義的,除非是在一行的末尾。
在Windows環境下,設置轉義字符為 `
,是非常有用的,由于\
是目錄路徑的分隔符,`
和windows下的轉義字符是一致的。
考慮下面的一個例子,在windows環境下是失敗的,在第二行的第二個\
被解釋成了換行的轉義符,而不是被第一個\
轉義了的目標,同樣的,在第三行末尾的\
也是,它們被認作是一個指令,\
被認為是續行符。這個Dockerfile的結果就是第二行和第三行被認為是一行指令:
FROM microsoft/nanoserver
COPY testfile.txt c:\\
RUN dir c:\
結果是:
PS C:\John> docker build -t cmd .
Sending build context to Docker daemon 3.072 kB
Step 1/2 : FROM microsoft/nanoserver
---> 22738ff49c6d
Step 2/2 : COPY testfile.txt c:\RUN dir c:
GetFileAttributesEx c:RUN: The system cannot find the file specified.
PS C:\John>
一個解決辦法是,上面都使用/
作為COPY
指令和dir
的目標。然而,最好的情況下,這只是看著windows下的路徑不自然,最壞的情況下,并不是所有的windows命令都支持/
作為路徑分隔符。
另一種解決辦法,添加一個escape
轉義指令,下面的Dockerfile
成功的執行,如預期的一樣windows
平臺很自然路徑表示語義:
# escape=`
FROM microsoft/nanoserver
COPY testfile.txt c:\
RUN dir c:\
結果是:
PS C:\John> docker build -t succeeds --no-cache=true .
Sending build context to Docker daemon 3.072 kB
Step 1/3 : FROM microsoft/nanoserver
---> 22738ff49c6d
Step 2/3 : COPY testfile.txt c:\
---> 96655de338de
Removing intermediate container 4db9acbb1682
Step 3/3 : RUN dir c:\
---> Running in a2c157f842f5
Volume in drive C has no label.
Volume Serial Number is 7E6D-E0F7
Directory of c:\
10/05/2016 05:04 PM 1,894 License.txt
10/05/2016 02:22 PM <DIR> Program Files
10/05/2016 02:14 PM <DIR> Program Files (x86)
10/28/2016 11:18 AM 62 testfile.txt
10/28/2016 11:20 AM <DIR> Users
10/28/2016 11:20 AM <DIR> Windows
2 File(s) 1,956 bytes
4 Dir(s) 21,259,096,064 bytes free
---> 01c7f3bef04f
Removing intermediate container a2c157f842f5
Successfully built 01c7f3bef04f
PS C:\John>
05 環境變量占位符
環境變量(ENV聲明)可以被用在某些指令中作為變量(可以被Dockerfile解釋)。轉義指令也可以用于處理語句中包含類似變量的語法。
環境變量在Dockerfile
中表示為$variable_name
或者 ${variable_name}
,他們是等效的,大括號的語法通常用來強調沒有空格的變量名,例如${foo}_bar
。${variable_name}
語法也支持一些標準的bash
修飾符,例如下面:
-
${variable:-word}
意思是,如果variable
被設置了,結果將是那個值,如果variable
沒被設置,那個word
就是結果。 -
${variable:+word}
意思是,如果variable
被設置了,word
就是結果,否則結果就是空。
以上所有情形,word
可以是任何字符串,包括其它的環境變量。
轉義可以在變量之前添加\
:例如,\$foo
或者\${foo}
將被轉義為$foo
和${foo}
兩個常量。
舉個例子(轉義之后的結果展示在#
的后面):
FROM busybox
ENV foo /bar
WORKDIR ${foo} # WORKDIR /bar
ADD . $foo # ADD . /bar
COPY \$foo /quux # COPY $foo /quux
環境變量在下面這些Dockerfile
指令中都是支持的:
ADD
COPY
ENV
EXPOSE
FROM
LABEL
STOPSIGNAL
USER
VOLUME
WORKDIR
此外還有:
-
ONBUILD
(當與上面任何一個指令結合時)
注意:在1.4版本之前,
ONBUILD
是不支持環境變量的,即使與上面列出的指令結合時。
在整個指令中環境變量的替換值都是用同一個值,換句話說,就是下面的例子:
ENV abc=hello
ENV abc=bye def=$abc
ENV ghi=$abc
結果是,def
的值是hello
,而不是bye
,ghi
的值是bye
,因為它不是設置abc
為bye
的指令的一部分。
06 .dockerignore文件
在docker命令行界面中發送上下文到docker的守護進程之前,它會檢查上下文目錄根路徑下名為.dockerignore
的文件,如果這個文件存在,命令行界面會修改上下文,排除那些被.dockerignore
中的模式匹配到的文件和目錄。這有助于避免一些不必要的(大的或者敏感的文件和目錄)發送到守護進程,還能避免一些潛在的使用ADD
或者 COPY
添加文件和目錄到鏡像中。
命令行解釋.dockerignore
文件為一個換行符分割的模式列表,類似于Unix shell的glob文件。由于這個匹配的目的,上下文的根被認為是工作目錄和根目錄。例如,模式 /foo/bar
和foo/bar
都是在排除目錄foo
下面一個叫bar
的文件或者目錄,目錄foo
是PATH
的子目錄或者URL
指定的git倉庫下的子目錄。不排除任何其它的。
如果在.dockerignore
文件中有一行以#
開頭,那么這一行被認為是注釋,命令行解釋之前為忽略它。
下面是一個.dockerignore
文件的例子:
# comment
*/temp*
*/*/temp*
temp?
這個文件將引發下面的構建行為:
規則 | 行為 |
---|---|
# comment |
忽略。 |
*/temp* |
排除根目錄下的子目錄中任何以temp 開頭的文件和目錄,例如,/somedir/temporary.txt 這個文本文件會被排除,/somedir/temp 這個目錄也會被排除。 |
*/*/temp* |
排除來自子目錄的任何以temp 開頭的文件和目錄,這個子目錄是根目錄下兩層,例如,/somedir/subdir/temporary.txt 被排除的。 |
temp? |
排除那些根目錄下名字以temp 開始拓展一個字符的文件和目錄,例如,/tempa 和 /tempb 是被排除的。 |
完成這個匹配使用的是Go語言的文件路徑匹配規則,在預處理步驟中會去除掉開頭和結尾的空格,并清除.
和..
元素,在這個過程中使用的是Go語言的文件路徑清理方法,預處理過程中會忽略掉空白行。
在Go語言的文件路徑匹配規則之外,Docker還支持一個特殊的通配符**
,用于匹配任意數量的目錄(包括零),例如,**/*.go
將排除所有以.go
結尾的文件,它會在編譯上下文的根目錄的所有目錄中找。
以感嘆號!
開始的行被用于標出排除中的異常文件,下面的這個.dockerignore
文件的例子就使用了這種機制:
*.md
!README.md
在上下文中除了README.md
之外,所有markdown文件都會被排除。
異常規則!
的位置影響行為:.dockerignore
文件的最后一行匹配一個特定文件,它是包含還是排除呢?看下面的例子:
*.md
!README*.md
README-secret.md
除了README 文件之外,沒有任何markdown文件被包含進上下文,并沒有README-secret.md
。
現在看這個例子:
*.md
README-secret.md
!README*.md
所有的README文件都會被包含進去,中間一行是沒有任何影響的,因為!README*.md
能夠匹配 README-secret.md
,并且在后面。
你甚至可以用.dockerignore
來排除Dockerfile
文件和.dockerignore
,這些文件仍然是會被送到守護進程的,因為需要它們做這些工作,但是ADD
和COPY
指令是不會copy它們到鏡像中去的。
最后,你可能想要指定文件包含進上下文,而不是排除它們,為了實現這個目的,可以使用*
作為第一個模式,下面使用一個或者多個!
異常模式。
注意:由于歷史原因,模式
.
是被忽略的。
到此為止介紹Dockerfile文件中工作原理和一些語法,以及相關的一些東西,其中03和04節不太常用,翻譯不是太好,請高手指正。Dockerfile中常用的指令下一篇文章再介紹。