【Docker IV】Dockerfile的那點事兒

上篇博文中提到了在實際工作中構建鏡像更多使用的Dockerfile,今天我們再來詳細的看看這個有趣有內涵的小可愛。

一、Dockerfile構建鏡像的流程

還是簡簡單單的看下上篇博文中最後的Dockerfile。

#Version:0.0.1  # 版本信息
FROM centos:latest  # 表示從哪個基礎鏡像開始構建
MAINTAINER Yuan "[email protected]"  # 表示作者以及郵箱

# 以下就是Dockerfile的執行,每條命令都是以RUN來開始,表示開始執行命令。這裏很簡單的跳轉到根目錄下,創建readme目錄,在readme目錄下再創建readme.md文件,並向其中寫入內容
RUN cd /
RUN mkdir readme
RUN touch /readme/readme.md
RUN echo "This is readme file created by dockerfile" > /readme/readme.md

以上內容很簡單,

  1. 進入根目錄。
  2. 創建readme。
  3. 在/readme/下創建readme.md文件。
  4. 向readme.md文件中追加內容。

執行命令docker build -t="centos/dockerfile_test:0.0.1" .後會有如下輸出:

[root@localhost dockerfile_test]# docker build -t="centos/dockerfile_test:0.0.1" .
Sending build context to Docker daemon  2.048kB
Step 1/6 : FROM centos:latest
 ---> 470671670cac
Step 2/6 : MAINTAINER Yuan "[email protected]"
 ---> Running in 4452ab2a7bbf
Removing intermediate container 4452ab2a7bbf
 ---> 9c9b6692a1dc
Step 3/6 : RUN cd /
 ---> Running in 386c520eb51f
Removing intermediate container 386c520eb51f
 ---> 7b621d54b17b
Step 4/6 : RUN mkdir readme
 ---> Running in 97c3cc32af97
Removing intermediate container 97c3cc32af97
 ---> c781c868ece5
Step 5/6 : RUN touch /readme/readme.md
 ---> Running in e60270cfa7af
Removing intermediate container e60270cfa7af
 ---> 5b1b86a3daf4
Step 6/6 : RUN echo "This is readme file created by dockerfile" > /readme/readme.md
 ---> Running in 7ff787a8a110
Removing intermediate container 7ff787a8a110
 ---> d3a80ce9dda6
Successfully built d3a80ce9dda6
Successfully tagged centos/dockerfile_test:0.0.1

由上面的輸出可以大致瞭解到通過Dockerfile執行docker build命令構建鏡像的流程,如下:

  1. 從基礎鏡像運行一個容器,如Step 1/6。
  2. 執行一條Dockerfile裏RUN指令後配置的操作命令,對當前容器做出修改。
  3. 在Docker內部執行類似於docker commit的操作,將當前操作容器提交成一個緩存的新鏡像。
  4. Docker再基於剛構建的緩存的新鏡像再運行一個容器,在該容器中繼續執行Dockerfile裏配置的指令,直到所有指令執行完成。

以上是正確的Dockerfile構建的過程,如果在其中RUN指令後的某一步驟配置錯誤,那麼就不會完成正確的構建,但是會生成已經成功的最後一步的那個鏡像,我們可以根據該鏡像創建出容器,進入到容器內部調測我們錯誤的那一步配置。例如,如果我把RUN touch /readme/readme.md寫成了RUN tauch /readme/readme.md那麼執行構建的時候,就會有如下輸出:

[root@localhost dockerfile_test]# docker build -t="centos/dockerfile_test:0.0.2.error" .
Sending build context to Docker daemon  2.048kB
Step 1/6 : FROM centos:latest
 ---> 470671670cac
Step 2/6 : MAINTAINER Yuan "[email protected]"
 ---> Using cache
 ---> 9c9b6692a1dc
Step 3/6 : RUN cd /
 ---> Using cache
 ---> 7b621d54b17b
Step 4/6 : RUN mkdir readme
 ---> Using cache
 ---> c781c868ece5
Step 5/6 : RUN tauch /readme/readme.md
 ---> Running in dae2437bf215
/bin/sh: tauch: command not found
The command '/bin/sh -c tauch /readme/readme.md' returned a non-zero code: 127

輸出顯示執行到第5步時出現了錯誤,因此我們可以使用第4步構建出來的鏡像ID是c781c868ece5的鏡像運行容器,docker run -it c781c868ece5 /bin/bash,進入容器調測第5步執行的命令。當然這裏很簡單看起來不需要進入容器調測就可以知道是哪裏出了問題,但是對於一些更爲複雜的指令編排,進入容器查看就會更加方便了。

這裏要注意的是,雖然說每一步執行都提交了一個新的緩存鏡像,但是當執行出問題時,我們並不可以通過docker images查詢到出問題以前提交的緩存鏡像,這個緩存鏡像是在Docker內部有效而不對使用者可見的。此外,有趣的是,我們不能通過docker images查詢到緩存鏡像,但是我們爲了排查問題原因根據緩存鏡像創建的容器,在執行docker ps命令時,在容器列表中卻是可以看到緩存鏡像ID。

對於鏡像是如何構建的,我們可以通過docker history 鏡像ID來查看構建過程,如下是文章開篇正常構建dockerfile_test:0.0.1鏡像的歷史過程,可以看到Dockerfile中配置命令的執行步驟:

[root@localhost dockerfile_test]# docker history d3a80ce9dda6
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
d3a80ce9dda6        10 days ago         /bin/sh -c echo "This is readme file created…   42B                 
5b1b86a3daf4        10 days ago         /bin/sh -c touch /readme/readme.md              0B                  
c781c868ece5        10 days ago         /bin/sh -c mkdir readme                         0B                  
7b621d54b17b        10 days ago         /bin/sh -c cd /                                 0B                  
9c9b6692a1dc        10 days ago         /bin/sh -c #(nop)  MAINTAINER Yuan "earlyuan…   0B                  
470671670cac        4 months ago        /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B                  
<missing>           4 months ago        /bin/sh -c #(nop)  LABEL org.label-schema.sc…   0B                  
<missing>           4 months ago        /bin/sh -c #(nop) ADD file:aa54047c80ba30064…   237MB

二、Dockerfile指令

  1. CMD

CMD指令用於指定容器啓動時要運行的命令。格式爲CMD [命令,命令執行參數1,命令執行參數2...]。CMD指令是在數據結構中存放所有要執行的命令,需要注意的是這裏的命令只可以有一條。說點有趣的話題吧,我在剛開始接觸這個指令的時候,看到CMD指令存放在數組中,我天真地認爲這裏可以放多條命令,於是乎在我放了多條命令後,雖然也構建了鏡像,但是根據該鏡像啓動容器時卻報錯了,提示error: garbage option,在我以爲容器創建失敗時,通過docker ps -a命令查詢容器時又意外的可以查詢到這個容器。這裏說這個題外話是想提醒大家,CMD雖然是數組結構存放,但是隻能在數組第一個元素存放命令,後面的元素均爲第一條命令的參數。下面看一個CMD指令的小例子:

[root@localhost dockerfile_command_test]# vi command_test_first 
#Version:0.0.1
FROM centos:latest
MAINTAINER Yuan "[email protected]"
CMD ["/bin/ps","-aux"]

上面的Dockerfile中我設置了一條CMD指令,在容器啓動時執行ps -aux命令。使用命令docker build -t="centos/command_test_first:0.0.1" -f /opt/earl_docker_test/dockerfile_command_test/command_test_first .構建鏡像後,執行命令docker run -it centos/command_test_first:0.0.1可以看到如下輸出:

[root@localhost dockerfile_command_test]# docker run -it centos/command_test_first:0.0.1 
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.0  46340  1700 pts/0    Rs+  05:53   0:00 /bin/ps -aux

在容器啓動時,查詢了系統進程的信息。這個時候可以通過docker ps -a命令查詢容器列表,可以看到有一個創建了但是剛剛退出的容器,這就是我們通過上述鏡像創建的容器,因爲沒有執行/bin/bash的原因,所以容器執行完ps -aux命令後就退出運行了。

針對上面的現象,如果我們不希望容器啓動就停止運行了,我們可以在執行docker run命令的時候再次指定啓動容器時要運行的命令,因爲docker run命令是可以覆蓋CMD指令的,例如docker run -it centos/command_test_first:0.0.1 /bin/bash,這樣就在啓動容器後,創建了shell窗口,可以對容器內部進行操作。

  1. ENTRYPOINT

ENTRYPOINT指令與CMD指令非常類似,但是它們有一個非常大的區別就是CMD指令會被docker run命令中的參數所覆蓋,而ENTRYPOINT不會被覆蓋,實際上,docker run命令中的任何參數都可以被當做參數傳遞給ENTRYPOINT指令中指定的命令。比如我們看下面這個例子:

[root@localhost dockerfile_entrypoint_test]# vi command_entrypoint 
#Version:0.0.1
FROM centos:latest
MAINTAINER Yuan "[email protected]"
ENTRYPOINT ["/bin/ls"]

執行命令docker build -t="centos/command_test_second:0.0.1" -f /opt/earl_docker_test/dockerfile_entrypoint_test/command_entrypoint .構建鏡像後,我們通過鏡像啓動容器,輸入如下命令docker run -it centos/command_test_second:0.0.1 "-l",可以看到docker run命令後有一個參數"-l",於是乎我們就得到了如下的輸出:

[root@localhost dockerfile_entrypoint_test]# docker run -it centos/command_test_second:0.0.1 "-l"
total 0
lrwxrwxrwx.   1 root root   7 May 11  2019 bin -> usr/bin
drwxr-xr-x.   5 root root 360 Jun  9 05:28 dev
drwxr-xr-x.   1 root root  66 Jun  9 05:28 etc
drwxr-xr-x.   2 root root   6 May 11  2019 home
lrwxrwxrwx.   1 root root   7 May 11  2019 lib -> usr/lib
lrwxrwxrwx.   1 root root   9 May 11  2019 lib64 -> usr/lib64
drwx------.   2 root root   6 Jan 13 21:48 lost+found
drwxr-xr-x.   2 root root   6 May 11  2019 media
drwxr-xr-x.   2 root root   6 May 11  2019 mnt
drwxr-xr-x.   2 root root   6 May 11  2019 opt
dr-xr-xr-x. 113 root root   0 Jun  9 05:28 proc
dr-xr-x---.   2 root root 162 Jan 13 21:49 root
drwxr-xr-x.  11 root root 163 Jan 13 21:49 run
lrwxrwxrwx.   1 root root   8 May 11  2019 sbin -> usr/sbin
drwxr-xr-x.   2 root root   6 May 11  2019 srv
dr-xr-xr-x.  13 root root   0 Jun  9 03:04 sys
drwxrwxrwt.   7 root root 145 Jan 13 21:49 tmp
drwxr-xr-x.  12 root root 144 Jan 13 21:49 usr
drwxr-xr-x.  20 root root 262 Jan 13 21:49 var

容器創建成功,並且將"-l"參數傳遞給了Dockerfile中我們定義的ENTRYPOINT指令後的命令/bin/ls,輸出了當前目錄下的詳細信息。

我們可以使用ENTRYPOINT和CMD組合起來玩點有趣的東西,就像下面的例子:

[root@localhost dockerfile_entrypoint_test]# vi command_entrypoint 
#Version:0.0.1
FROM centos:latest
MAINTAINER Yuan "[email protected]"
ENTRYPOINT ["/bin/ls"]
CMD ["-alt"]

執行命令docker build -t="centos/command_test_second:0.0.2" -f /opt/earl_docker_test/dockerfile_entrypoint_test/command_entrypoint .構建鏡像後,我們通過鏡像啓動容器,輸入如下命令docker run -it centos/command_test_second:0.0.2,這次我們不給啓動命令傳遞參數,於是乎我們就得到了如下的輸出:

[root@localhost dockerfile_entrypoint_test]# docker run -it centos/command_test_second:0.0.2
total 0
drwxr-xr-x.   5 root root 360 Jun  9 14:46 dev
dr-xr-xr-x. 114 root root   0 Jun  9 14:46 proc
drwxr-xr-x.   1 root root   6 Jun  9 14:46 .
drwxr-xr-x.   1 root root   6 Jun  9 14:46 ..
drwxr-xr-x.   1 root root  66 Jun  9 14:46 etc
-rwxr-xr-x.   1 root root   0 Jun  9 14:46 .dockerenv
dr-xr-xr-x.  13 root root   0 Jun  9 03:04 sys
dr-xr-x---.   2 root root 162 Jan 13 21:49 root
drwxr-xr-x.  11 root root 163 Jan 13 21:49 run
drwxrwxrwt.   7 root root 145 Jan 13 21:49 tmp
drwxr-xr-x.  20 root root 262 Jan 13 21:49 var
drwxr-xr-x.  12 root root 144 Jan 13 21:49 usr
drwx------.   2 root root   6 Jan 13 21:48 lost+found
lrwxrwxrwx.   1 root root   7 May 11  2019 bin -> usr/bin
drwxr-xr-x.   2 root root   6 May 11  2019 home
lrwxrwxrwx.   1 root root   7 May 11  2019 lib -> usr/lib
lrwxrwxrwx.   1 root root   9 May 11  2019 lib64 -> usr/lib64
drwxr-xr-x.   2 root root   6 May 11  2019 media
drwxr-xr-x.   2 root root   6 May 11  2019 mnt
drwxr-xr-x.   2 root root   6 May 11  2019 opt
lrwxrwxrwx.   1 root root   8 May 11  2019 sbin -> usr/sbin
drwxr-xr-x.   2 root root   6 May 11  2019 srv

可以看到,即使我們沒有指定啓動參數,在根據Dockerfile創建的鏡像啓動時,依舊會將CMD指令的配置作爲參數繼續執行。如果我們指定了啓動參數,那麼根據docker run會覆蓋CMD的原則,那麼就不會執行Dockerfile中CMD的配置了。

  1. WORKDIR

在容器內部設置工作目錄。我們可以使用WORKDIR指令在Dockerfile中指定接下來的操作的工作目錄,類似於linux命令行cd到某個目錄下進行操作。我們來看個例子:

[root@localhost dockerfile_workdir_test]# vi command_workdir 
#Version:0.0.1
FROM centos:latest
MAINTAINER Yuan "[email protected]"

RUN yum install -y wget
RUN mkdir /opt/download
WORKDIR /opt/download
RUN wget https://mirrors.aliyun.com/centos/7.8.2003/sclo/x86_64/rh/Packages/r/rh-nginx116-nginx-1.16.1-4.el7.x86_64.rpm

上面的例子中,我們先通過yum安裝了wget命令,由於設置的工作目錄必須存在,纔可以使用WORKDIR指令,所以我們必須先創建一個目錄download,然後設置工作目錄,最後使用wget命令進行資源的下載。

執行命令docker build -t="centos/command_test_third:0.0.1" -f /opt/earl_docker_test/dockerfile_workdir_test/command_workdir .構建鏡像後,我們通過鏡像啓動容器,可以在/opt/download目錄下看到我們剛剛下載的rpm包。

如果我們偶爾有這樣的需求,那就是在Dockerfile中通過WORKDIR指令指定了工作目錄,但是在創建容器時需要使用另外的路徑作爲工作目錄,那麼怎麼辦呢?Docker爲我們提供了這樣一個參數,“docker run -w newWorkDir”,我們來看下面這個例子。我們在創建容器時輸入以下的命令docker run -it -w /home centos:latest pwd,這行命令會使用centos基礎鏡像創建一個容器,並將工作目錄設定到/home目錄下,再輸出當前目錄路徑。

[root@localhost dockerfile_workdir_test]# docker run -it -w /home centos:latest pwd
/home
  1. ENV

ENV指令用於設置環境變量。我們先來看下面這個例子:

[root@localhost dockerfile_env_test]# vi command_env 
#Version:0.0.1
FROM centos:latest
MAINTAINER Yuan "[email protected]"

ENV MY_TEST_DIR /opt/test
RUN mkdir $MY_TEST_DIR
RUN echo "i love docker" > $MY_TEST_DIR/test.txt

我們設置了環境變量MY_TEST_DIR,並通過“$變量名”的方式引用它的值,在其中創建文件,寫入內容。通過命令docker build -t="centos/command_test_fourth:0.0.1" -f /opt/earl_docker_test/dockerfile_env_test/command_env .構建鏡像後,我們根據鏡像啓動容器,可以看到在/opt/test目錄下創建了test.txt文件,並在其中有i love docker的內容。我們可以在容器內輸入命令env,可以看到有如下輸出:

[root@5c371ca0128e test]# env
LANG=en_US.UTF-8
HOSTNAME=5c371ca0128e
OLDPWD=/opt
PWD=/opt/test
HOME=/root
MY_TEST_DIR=/opt/test
TERM=xterm
SHLVL=1
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
LESSOPEN=||/usr/bin/lesspipe.sh %s
_=/usr/bin/env

可以看到MY_TEST_DIR已經設置到環境變量中了。環境變量一旦設置成功,根據其鏡像創建的容器中也將永久有效。

  1. COPY

COPY指令可以將構建上下文目錄中的文件複製到容器中,來看看下面的例子:

[root@localhost dockerfile_copy_test]# vi command_copy 
#Version:0.0.1
FROM centos:latest
MAINTAINER Yuan "[email protected]"

COPY copyTest.txt /opt/docker/test/

在構建上下文中我們創建一個copyTest.txt的文件,通過命令docker build -t="centos/command_test_sixth:0.0.1" -f /opt/earl_docker_test/dockerfile_copy_test/command_copy .構建鏡像後,我們根據鏡像啓動容器,可以看到Docker幫我們把copyTest.txt就複製到了/opt/docker/test/目錄下。雖然我們容器中一開始並沒有/opt/docker/test/這個目錄,但是當目的地址不存在時,Docker可以幫我們創建所需要的目錄結構,就像是Docker幫我們敲了一個mkdir -p命令一樣。

這裏需要注意的是,需要複製的文件或者目錄必須位於當前的構建上下文中,否則是無法實現從本地複製到鏡像容器中的。因爲在構建時,會將構建上下文傳遞到Docker守護進程中,而複製的操作是在Docker守護進程中完成的。

  1. ADD

ADD指令與COPY指令非常類似,都是將構建上下文目錄中的文件或目錄複製到鏡像中,格式爲ADD 構建上下文中的源文件路徑 鏡像中的目標路徑。我們來看看下面這個例子:

[root@localhost dockerfile_add_test]# vi command_add 
#Version:0.0.1
FROM centos:latest
MAINTAINER Yuan "[email protected]"

ADD testZip.zip /opt/test/testZip.zip

在構建上下文中,我們創建了testZip.zip的壓縮包,通過命令docker build -t="centos/command_test_seventh:0.0.1" -f /opt/earl_docker_test/dockerfile_add_test/command_add .構建鏡像後,我們根據鏡像啓動容器,可以看到Docker幫我們把testZip.zip就複製到了/opt/test/目錄下。這樣的操作與COPY指令沒有什麼區別。

但是,不同與COPY指令只做複製操作,ADD指令在複製的基礎上,還可以做一丟丟的解壓操作。針對於gzip,bzip2,xz類型的歸檔文件,Docker在執行ADD指令時會自動將他們解壓到指定的位置。我們看下面這個例子:

[root@localhost dockerfile_add_test]# vi command_add 
#Version:0.0.1
FROM centos:latest
MAINTAINER Yuan "[email protected]"

ADD test.tar.gz /opt/test/

這裏我們將構建上下文中的test.tar.gz包添加到鏡像中的/opt/test目錄中去,通過命令docker build -t="centos/command_test_seventh:0.0.2" -f /opt/earl_docker_test/dockerfile_add_test/command_add .,構建鏡像後,我們根據鏡像啓動容器,可以看到Docker幫我們把test.tar.gz包直接解壓縮到了/opt/test目錄中,這一點是COPY指令不能做到的。

  1. LABEL

LABEL指令用於爲鏡像添加元數據,按照鍵值對的形式表示,格式爲LABEL key="value"。例如下面的例子:

[root@localhost dockerfile_add_test]# vi command_add 
#Version:0.0.1
FROM centos:latest
MAINTAINER Yuan "[email protected]"

LABEL version="0.0.1" type="docker test"
ADD test.tar.gz /opt/test/

通過命令docker build -t="centos/command_test_seventh:0.0.3" -f /opt/earl_docker_test/dockerfile_add_test/command_add .,構建鏡像後,我們根據鏡像啓動容器,輸入命令docker inspect centos/command_test_seventh:0.0.3,可以看到鏡像的詳細信息,如下:

"Config": {
    "Labels": {
        "org.label-schema.build-date": "20200114",
        "org.label-schema.license": "GPLv2",
        "org.label-schema.name": "CentOS Base Image",
        "org.label-schema.schema-version": "1.0",
        "org.label-schema.vendor": "CentOS",
        "org.opencontainers.image.created": "2020-01-14 00:00:00-08:00",
        "org.opencontainers.image.licenses": "GPL-2.0-only",
        "org.opencontainers.image.title": "CentOS Base Image",
        "org.opencontainers.image.vendor": "CentOS",
        "type": "docker test",
        "version": "0.0.1"
    }
}

可以看到我們添加的元數據就已經添加到鏡像中了。這裏推薦在Dockerfile中將所有元數據按照上面例子一樣,寫到一條LABEL指令中,這樣就可以避免過多的元數據添加創建過多的鏡像層。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章