理解Docker(2):Docker 鏡像

本系列文章將介紹Docker的有關知識:html

(1)Docker 安裝及基本用法node

(2)Docker 鏡像linux

(3)Docker 容器的隔離性 - 使用 Linux namespace 隔離容器的運行環境nginx

(4)Docker 容器的隔離性 - 使用 cgroups 限制容器使用的資源docker

(5)Docker 網絡shell

 

  對於每一個軟件,除了它自身的代碼之外,它的運行還須要有一個運行環境和依賴。無論這個軟件是象往常同樣運行在物理機或者虛機之中,仍是運行在如今的容器之中,這些都是不變的。在傳統環境中,軟件在運行以前也須要通過 代碼開發->運行環境準備 -> 安裝軟件 -> 運行軟件 等環節,在容器環境中,中間的兩個環節被鏡像製做過程替代了。也就是說,鏡像的製做也包括運行環境準備和安裝軟件等兩個主要環節,以及一些其餘環節。所以,Docker 容器鏡像其實並無什麼新的理論,只是這過程有了新的方式而已。json

  鏡像(image)是動態的容器的靜態表示(specification),包括容器所要運行的應用代碼以及運行時的配置。Docker 鏡像包括一個或者多個只讀層( read-only layers ),所以,鏡像一旦被建立就不再能被修改了。一個運行着的Docker 容器是一個鏡像的實例( instantiation )。從同一個鏡像中運行的容器包含有相同的應用代碼和運行時依賴。可是不像鏡像是靜態的,每一個運行着的容器都有一個可寫層( writable layer ,也成爲容器層 container layer),它位於底下的若干只讀層之上。運行時的全部變化,包括對數據和文件的寫和更新,都會保存在這個層中。所以,從同一個鏡像運行的多個容器包含了不一樣的容器層。ubuntu

 Docker 有兩種方式來建立一個容器鏡像:vim

  • 建立一個容器,運行若干命令,再使用 docker commit 來生成一個新的鏡像。不建議使用這種方案。
  • 建立一個 Dockerfile 而後再使用 docker build 來建立一個鏡像。大多人會使用 Dockerfile 來建立鏡像。

1. 鏡像有關的幾個基礎概念

1.1 Host OS VS Guest OS VS Base image

好比,一臺主機安裝的是 Centos 操做系統,如今在上面跑一個 Ubuntu 容器。此時,Host OS 是 Centos,Guest OS 是 Ubuntu。Guest OS 也被成爲容器的 Base Image。c#

 

一些說明:

  • 關於 linux 內核和版本:全部 Linux 發行版都採用相同的 Linux 內核(kernel),而後全部發行版對內核都有輕微改動。這些改動都會上傳回 linux 社區,並被合併。
  • 關於Linux 容器環境:由於全部Linux發行版都包含同一個linux 內核(有輕微修改),以及不一樣的本身的軟件,所以,會很容易地將某個 userland 軟件安裝在linux 內核上,來模擬不一樣的發行版環境。好比說,在 Ubuntu 上運行 Centos 容器,這意味着從 Centos 獲取 userland 軟件,運行在 Ubuntu 內核上。所以,這就像在同一個操做系統(linux 內核)上運行不一樣的 userland 軟件(發行版的)。這就是爲何Docker 不支持在 Linux 主機上運行 FreeBSD 或者windows 容器。

可見,容器的 base image 並不真的是 base OS。Base image 會遠遠比 base OS 更輕量。它只安裝發行版特殊的部分(userland 軟件)。

那爲何還須要 base image 呢?這是由於,docker 容器文件系統與 host OS 是隔離的。容器鏡像中的應用軟件沒法看到主機文件系統,除非將主機文件系統掛載爲容器的卷。所以,能夠想像一下,你容器中的應用依賴於各類操做系統庫,所以咱們不得不將這些庫打包到鏡像之中。另外,base image 會讓咱們使用到各個發行版的包管理系統,好比 yum 和 apt-get。並且,各個linux 發行版的 base image 也不是普通的發行版,而是一個簡化了的版本。並且,base image 並不帶有 linux 內核,由於容器會使用主機的內核。

所以,須要注重理解 image 和 OS 這兩個概念。之因此成爲 base image,而不是 base OS,是由於 base image 中並不包括完整的 OS。而這一點,是容器與虛擬機以前的本質區別之一。那就是,容器並無虛擬化,而是共享主機上的linux 內核。

1.2 關於Container Base image

從上面內容能夠看出,容器把 linux 鏡像從內核空間和用戶空間進行了分開管理。對 host OS 來講,它更側重於內核,再加上少許的用戶空間內容;對 Guest OS 來講,它側重於(只有)用戶空間,只包括庫文件、編譯器、配置文件,以及用戶代碼。

常見的容器基礎鏡像:

所以,用戶須要仔細選擇容器的 base image,不只從上表中的幾個方面,還包括性能、安全性等一些因素。

Ubuntu 更是推出了只有 29M 的 Minimal Ubuntu 容器鏡像,具體在這裏 https://blog.ubuntu.com/2018/07/09/minimal-ubuntu-released。 

2. docker build 生成鏡像

2.1 生成過程實例

 在使用 Dockerfile 建立容器以前,須要先準備一個 Dockerfile 文件,而後運行 docker build 命令來建立鏡像。咱們經過下面的例子來看看Docker 建立容器的過程。

FROM ubuntu:14.04

MAINTAINER sammy "sammy@sammy.com"

RUN apt-get update

RUN apt-get -y install ntp

EXPOSE 5555

CMD ["/usr/sbin/ntpd"]

這是一個很是簡單的Dockerfile,它的目的是基於 Ubuntu 14.04 基礎鏡像安裝 ntp 從而生成一個新的鏡像。看看其過程:

root@devstack:/home/sammy/ntponubuntu# docker build -t sammy_ntp2 .
Sending build context to Docker daemon 2.048 kB
Step 1 : FROM ubuntu:14.04
 ---> 4a725d3b3b1c
Step 2 : MAINTAINER sammy "sammy@sammy.com"
 ---> Using cache
 ---> c4299e3f774c
Step 3 : RUN apt-get update
 ---> Using cache
 ---> 694a19d54103
Step 4 : RUN apt-get -y install ntp
 ---> Running in 9bd153c65a76
Reading package lists...
...
Fetched 561 kB in 10s (51.1 kB/s)
Selecting previously unselected package libedit2:amd64.
(Reading database ... 11558 files and directories currently installed.)
...
Processing triggers for libc-bin (2.19-0ubuntu6.9) ...
Processing triggers for ureadahead (0.100.0-16) ...
 ---> 9cc05cf6f48d
Removing intermediate container 9bd153c65a76
Step 5 : EXPOSE 5555
 ---> Running in eb4633151d98
 ---> f5c96137bec9
Removing intermediate container eb4633151d98
Step 6 : CMD /usr/sbin/ntpd
 ---> Running in e81b1eae3678
 ---> af678df648bc
Removing intermediate container e81b1eae3678
Successfully built af678df648bc

Dockerfile 中的每一個步驟都會對應每個 docker build 輸出中的 step。

Step 1:FROM ubuntu:14.04

獲取基礎鏡像 ubuntu:14.04. Docker 首先會在本地查找,若是找到了,則直接利用;不然從 Docker registry 中下載。在第一次使用這個基礎鏡像的時候,Docker 會從 Docker Hub 中下載這個鏡像,並保存在本地:

Step 1 : FROM ubuntu:14.04
14.04: Pulling from library/ubuntu
862a3e9af0ae: Pull complete
6498e51874bf: Pull complete
159ebdd1959b: Pull complete
0fdbedd3771a: Pull complete
7a1f7116d1e3: Pull complete
Digest: sha256:5b5d48912298181c3c80086e7d3982029b288678fccabf2265899199c24d7f89
Status: Downloaded newer image for ubuntu:14.04
 ---> 4a725d3b3b1c

之後再使用的時候就直接使用這個鏡像而再也不須要下載了。

Step 2:MAINTAINER sammy "sammy@sammy.com"

本例中依然是從 Cache 中環境新的鏡像。在第一次的時候,Docker 會建立一個臨時的容器 1be8f33c1846,而後運行 MAINTAINER 命令,再使用 docker commit 生成新的鏡像

Step 2 : MAINTAINER sammy "sammy@sammy.com"
 ---> Running in 1be8f33c1846
 ---> c4299e3f774c

經過這個臨時容器的過程(create -> commit -> destroy),生成了新的鏡像 c4299e3f774c:

2016-09-16T21:58:09.010886393+08:00 container create 1be8f33c18469f089d1eee8c444dad1ff0c7309be82767092082311379245358 (image=sha256:4a725d3b3b1cc18c8cbd05358ffbbfedfe1eb947f58061e5858f08e2899731ee, name=focused_poitras)
2016-09-16T21:58:09.060071206+08:00 container commit 1be8f33c18469f089d1eee8c444dad1ff0c7309be82767092082311379245358 (comment=, image=sha256:4a725d3b3b1cc18c8cbd05358ffbbfedfe1eb947f58061e5858f08e2899731ee, name=focused_poitras)
2016-09-16T21:58:09.071988068+08:00 container destroy 1be8f33c18469f089d1eee8c444dad1ff0c7309be82767092082311379245358 (image=sha256:4a725d3b3b1cc18c8cbd05358ffbbfedfe1eb947f58061e5858f08e2899731ee, name=focused_poitras)

這個鏡像是基於 ubuntu 14.04 基礎鏡像生成的,layers 沒有變化,只是元數據 CMD 發生了改變:

"Cmd": [
                "/bin/sh",
                "-c",
                "#(nop) ",
                "MAINTAINER sammy \"sammy@sammy.com\""
            ]

所以能夠認爲只是鏡像的元數據發生了改變。生成的新的鏡像做爲中間鏡像會被保存在 cache 中。

 Step 3: RUN apt-get update

本例中Docker 仍然從緩存中獲取了鏡像。在第一次的時候,Docker 仍然是經過建立臨時容器在執行 docker commit 的方式來建立新的鏡像:

Step 3 : RUN apt-get update
 ---> Running in 8b3b97af3bd7
Ign http://archive.ubuntu.com trusty InRelease
Get:1 http://archive.ubuntu.com trusty-updates InRelease [65.9 kB]
...
Get:22 http://archive.ubuntu.com trusty/universe amd64 Packages [7589 kB]
Fetched 22.2 MB in 16min 21s (22.6 kB/s)
Reading package lists...
 ---> 694a19d54103
Removing intermediate container 8b3b97af3bd7

經過以上步驟,生成了新的中間鏡像 694a19d54103,它也會被保存在緩存中。你可使用 docker inspect 694a19d54103 命令查看該中間鏡像,可是沒法在docker images 列表中找到它,這是由於 docker images 默認隱藏了中間狀態的鏡像,所以你須要使用 docker images -a 來獲取它:

root@devstack:/home/sammy# docker images -a | grep 694a19d54103
<none>                  <none>              694a19d54103        11 hours ago        210.1 MB

該鏡像和原始鏡像相比,多了一個 layer,它保存的是 apt-get update 命令所帶來的變化:

"RootFS": {
            "Type": "layers",
            "Layers": [
                "sha256:102fca64f92471ff7fca48e55807ae2471502822ba620292b0a06ebcab907cf4",
                "sha256:24fe29584c046f2a88f7f566dd0bf7b08a8c0d393dfad8370633b0748bba8cbc",
                "sha256:530d731d21e1b1bbe356d70d3bca4d72d76fed89e90faab271d29bd58c8ccea4",
                "sha256:344f56a35ff9fc747ada7d2b88bd21c49b2ec404872662cbaf0a65201873c0c6",
                "sha256:ffb6ddc7582aa7e2e73f102df3ffcd272e59b7cf3f7abefe08d11a7c85dea53a",
                "sha256:a1afe95c99b39c30b5c1d3e8fda451bd3f066be304616197f1046e64cf6cda93" #這一層是新加的
            ]
        }

Step 4: RUN apt-get -y install ntp

和上面 Step 3 過程同樣,這個步驟也會經過建立臨時容器,執行該命令,再使用 docker commit 命令生成一箇中間鏡像 9cc05cf6f48d 。和上面步驟生成的鏡像相比,它又多了一層:

root@devstack:/home/sammy# docker images -a | grep 9cc05cf6f48d
<none>                  <none>              9cc05cf6f48d        10 hours ago        212.8 MB
root@devstack:/home/sammy# docker inspect --format={{'.RootFS.Layers'}} 9cc05cf6f48d
[sha256:102fca64f92471ff7fca48e55807ae2471502822ba620292b0a06ebcab907cf4 
sha256:24fe29584c046f2a88f7f566dd0bf7b08a8c0d393dfad8370633b0748bba8cbc
sha256:530d731d21e1b1bbe356d70d3bca4d72d76fed89e90faab271d29bd58c8ccea4
sha256:344f56a35ff9fc747ada7d2b88bd21c49b2ec404872662cbaf0a65201873c0c6
sha256:ffb6ddc7582aa7e2e73f102df3ffcd272e59b7cf3f7abefe08d11a7c85dea53a
sha256:a1afe95c99b39c30b5c1d3e8fda451bd3f066be304616197f1046e64cf6cda93
sha256:a93086f33a2b7ee18eec2454b468141f95a403f5081284b6f177f83cdb3d54ba]

Step 5: EXPOSE 5555

 這一步和上面的 Step 2 同樣,Docker 生成了一個臨時容器,執行 EXPOSE 55 命令,再經過 docker commit 建立了中間鏡像 f5c96137bec9。該鏡像的 layers 沒有變化,可是元數據發生了一些變化,包括:

"ExposedPorts": {
                "5555/tcp": {}
            }
"Cmd": [
                "/bin/sh",
                "-c",
                "#(nop) ",
                "EXPOSE 5555/tcp"
            ]

Step 6: CMD ["/usr/sbin/ntpd"]

這一步和上面的步驟相同,最終它建立了鏡像 af678df648bc,該鏡像只是修改了 CMD 元數據:

 "Cmd": [
                "/bin/sh",
                "-c",
                "#(nop) ",
                "CMD [\"/usr/sbin/ntpd\"]"
            ]

該鏡像也是Docker 根據本 Dockerfile 生成的最終鏡像。它也出如今了 docker images 結果中:

root@devstack:/home/sammy# docker images | grep af678df648bc
sammy_ntp2              latest              af678df648bc        11 hours ago        212.8 MB

咱們可使用 docker history 命令查看該鏡像中每一層的信息:

root@devstack:/home/sammy/ntponubuntu# docker history af678df648bc
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
af678df648bc        16 hours ago        /bin/sh -c #(nop)  CMD ["/usr/sbin/ntpd"]       0 B
f5c96137bec9        16 hours ago        /bin/sh -c #(nop)  EXPOSE 5555/tcp              0 B
9cc05cf6f48d        16 hours ago        /bin/sh -c apt-get -y install ntp               2.679 MB
694a19d54103        16 hours ago        /bin/sh -c apt-get update                       22.17 MB
c4299e3f774c        17 hours ago        /bin/sh -c #(nop)  MAINTAINER sammy "sammy@sa   0 B
4a725d3b3b1c        3 weeks ago         /bin/sh -c #(nop) CMD ["/bin/bash"]             0 B
<missing>           3 weeks ago         /bin/sh -c mkdir -p /run/systemd && echo 'doc   7 B
<missing>           3 weeks ago         /bin/sh -c sed -i 's/^#\s*\(deb.*universe\)$/   1.895 kB
<missing>           3 weeks ago         /bin/sh -c rm -rf /var/lib/apt/lists/*          0 B
<missing>           3 weeks ago         /bin/sh -c set -xe   && echo '#!/bin/sh' > /u   194.6 kB
<missing>           3 weeks ago         /bin/sh -c #(nop) ADD file:ada91758a31d8de3c7   187.8 MB

以上過程說明:

  • 容器鏡像包括元數據和文件系統,其中文件系統是指對基礎鏡像的文件系統的修改,元數據不影響文件系統,只是會影響容器的配置
  • 每一個步驟都會生成一個新的鏡像,新的鏡像與上一次的鏡像相比,要麼元數據有了變化,要麼文件系統有了變化而多加了一層
  • Docker 在須要執行指令時經過建立臨時鏡像,運行指定的命令,再經過 docker commit 來生成新的鏡像
  • Docker 會將中間鏡像都保存在緩存中,這樣未來若是能直接使用的話就不須要再從頭建立了。關於鏡像緩存,請搜索相關文檔。

2.2 Docker 鏡像分層,COW 和 鏡像大小(size)

2.2.1 鏡像分層和容器層

  從上面例子能夠看出,一個 Docker 鏡像是基於基礎鏡像的多層疊加,最終構成和容器的 rootfs (根文件系統)。當 Docker 建立一個容器時,它會在基礎鏡像的容器層之上添加一層新的薄薄的可寫容器層。接下來,全部對容器的變化,好比寫新的文件,修改已有文件和刪除文件,都只會做用在這個容器層之中。所以,經過不拷貝完整的 rootfs,Docker 減小了容器所佔用的空間,以及減小了容器啓動所需時間。

2.2.2 COW 和鏡像大小

  COW,copy-on-write 技術,一方面帶來了容器啓動的快捷,另外一方也形成了容器鏡像大小的增長。每一次 RUN 命令都會在鏡像上增長一層,每一層都會佔用磁盤空間。舉個例子,在 Ubuntu 14.04 基礎鏡像中運行 RUN apt-get upgrade 會在保留基礎層的同時再建立一個新層來放全部新的文件,而不是修改老的文件,所以,新的鏡像大小會超過直接在老的文件系統上作更新時的文件大小。所以,爲了減小鏡像大小起見,全部文件相關的操做,好比刪除,釋放和移動等,都須要儘量地放在一個 RUN 指令中進行。

  好比說,經過將上面的示例 Dockerfile 修改成:

FROM ubuntu:14.04
MAINTAINER sammy "sammy@sammy.com"
RUN apt-get update && apt-get -y install ntp
EXPOSE 5555
CMD ["/usr/sbin/ntpd"]

結果產生的鏡像,不只層數少了一層(7 -> 6),並且大小減小了 0.001M :),由於這個例子比較特殊,文件都是添加,而沒有更新,所以size 的降低很是小。

2.2.3 使用容器須要避免的一些作法

這篇文章 10 things to avoid in docker containers 列舉了一些在使用容器時須要避免的作法,包括:

  • 不要在容器中保存數據(Don’t store data in containers
  • 將應用打包到鏡像再部署而不是更新到已有容器(Don’t ship your application in two pieces
  • 不要產生過大的鏡像 (Don’t create large images
  • 不要使用單層鏡像 (Don’t use a single layer image
  • 不要從運行着的容器上產生鏡像 (Don’t create images from running containers )
  • 不要只是使用 「latest」標籤 (Don’t use only the 「latest」 tag
  • 不要在容器內運行超過一個的進程 (Don’t run more than one process in a single container )
  • 不要在容器內保存 credentials,而是要從外面經過環境變量傳入 ( Don’t store credentials in the image. Use environment variables
  • 不要使用 root 用戶跑容器進程(Don’t run processes as a root user )
  • 不要依賴於IP地址,而是要從外面經過環境變量傳入 (Don’t rely on IP addresses )

2.3 鏡像的內容

容器鏡像的內容,實際上是一個 json 文件加上 tar 包。以很是小的鏡像 kubernetes/pause 爲例,咱們來作個實驗:

(1)將鏡像導出爲 tar 文件

root@kub-node-1:/home/ubuntu/kub/image# docker save -o pause.tar kubernetes/pause:latest
root@kub-node-1:/home/ubuntu/kub/image# ls
pause.tar

(2)解壓 pause.tar 文件

root@kub-node-1:/home/ubuntu/kub/image# tar -xf pause.tar

root@kub-node-1:/home/ubuntu/kub/image/pause# ls -l
total 280
drwxr-xr-x 2 root root 4096 Jan 23 09:02 afa9f35badc97e21193ee701222d9edfc5b0f0e5c518d357eb8b016d8287cda7
drwxr-xr-x 2 root root 4096 Jul 19 2014 e0b1695ad29a961b7e28713942942786692107d7f9087d72ccf9bbc0a3ab133e
drwxr-xr-x 2 root root 4096 Jan 23 09:20 e3caa892ed5297d0c98916b251c5be1d26c3a50b581fe145e3a6516c00531464
-rw-r--r-- 1 root root 1691 Jul 19 2014 f9d5de0795395db6c50cb1ac82ebed1bd8eb3eefcebb1aa724e01239594e937b.json
-rw-r--r-- 1 root root 366 Jan 1 1970 manifest.json
-rw------- 1 root root 258560 Jan 23 09:02 pause.tar
-rw-r--r-- 1 root root 99 Jan 1 1970 repositories

其中的 repositories 文件的內容,就是鏡像名稱、版本、最上層的layer的名稱:

root@kub-node-1:/home/ubuntu/kub/image# cat repositories
{"kubernetes/pause":{"latest":"afa9f35badc97e21193ee701222d9edfc5b0f0e5c518d357eb8b016d8287cda7"}}

而 manifest.json 文件則保持的是鏡像的元數據,包括真正元數據 json 文件的名稱及每一層的名稱,tag 等:

root@kub-node-1:/home/ubuntu/kub/image# cat manifest.json
[{"Config":"f9d5de0795395db6c50cb1ac82ebed1bd8eb3eefcebb1aa724e01239594e937b.json","RepoTags":["kubernetes/pause:latest"],"Layers":["e0b1695ad29a961b7e28713942942786692107d7f9087d72ccf9bbc0a3ab133e/layer.tar","e3caa892ed5297d0c98916b251c5be1d26c3a50b581fe145e3a6516c00531464/layer.tar","afa9f35badc97e21193ee701222d9edfc5b0f0e5c518d357eb8b016d8287cda7/layer.tar"]}]

f9d5de0795395db6c50cb1ac82ebed1bd8eb3eefcebb1aa724e01239594e937b.json 文件則真正包含鏡像的全部元數據。

而剩下的3個文件夾則與該鏡像的3個layers 一一對應:

"RootFS": {
            "Type": "layers",
            "Layers": [
                "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef",
                "sha256:e16a89738269fec22db26ec6362823a9ec42d0163685d88ba03c4fb5d5e723f6",
                "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
            ]
        }

每一個文件夾中的內容爲:

root@kub-node-1:/home/ubuntu/kub/image# ls e0b1695ad29a961b7e28713942942786692107d7f9087d72ccf9bbc0a3ab133e -l
total 12
-rw-r--r-- 1 root root  393 Jul 19  2014 json
-rw-r--r-- 1 root root 1024 Jul 19  2014 layer.tar
-rw-r--r-- 1 root root    3 Jul 19  2014 VERSION

由於 pause 鏡像比較特殊,解壓 layer.tar 後沒有文件。若是看 nginx 鏡像的某層的 layer.tar 文件,則能看到該layer中包含的文件:

root@kub-node-1:/home/ubuntu/kub/image/nginx/2c9d2d9d91f48573ea451f8d529e88dee79d64782892def6063fdda3f127d33c# ls -l
total 39268
drwxr-xr-x  2 root root     4096 Jan  8 21:49 bin
drwxr-xr-x 13 root root     4096 Jan  8 21:54 etc
-rw-r--r--  1 root root      469 Jan  8 23:32 json
drwxr-xr-x  3 root root     4096 Dec 10 08:00 lib
drwx------  2 root root     4096 Jan  8 21:56 root
drwxr-xr-x  2 root root     4096 Jan  8 21:28 run
drwxr-xr-x  2 root root     4096 Jan  8 21:49 sbin
drwxr-xr-x  7 root root     4096 Dec 10 08:00 usr
drwxr-xr-x  5 root root     4096 Dec 10 08:00 var
-rw-r--r--  1 root root        3 Jan  8 23:32 VERSION

從以上分析可見,

  • docker 鏡像中主要就是 tar 文件包和元數據 json 文件
  • docker 鏡像的打包過程,其實就是將每一層對應的文件打包過程,最後組成一個單一的 tar 文件
  • docker 鏡像的使用過程,其實就是將一層層的 tar 文件接包到文件系統的過程。

3. Dockerfile 語法

上面的步驟說明了 Docker 能夠經過讀取 Dockerfile 的內容來生成容器鏡像。Dockerfile 的每一行都是 INSTRUCTION arguments 格式,即 「指令 參數」。關於 Dockerfile 的預防,請參考 https://docs.docker.com/engine/reference/builder/。下面只是就一些主要的指令作一些說明。

3.1 幾個主要指令

3.1.1 ADD 和 COPY

Add:將 host 上的文件拷貝到或者將網絡上的文件下載到容器中的指定目錄
# Usage: ADD [source directory or URL] [destination directory]
ADD /my_app_folder /my_app_folder

例子:

FROM ubuntu:14.04
MAINTAINER Sammy Liu <sammy.liu@unknow.com>
ADD temp dockfile
ENTRYPOINT top

ADD 指令會將本地 temp 目錄中的文件拷貝到容器的 dockfile 目錄下面,從而在鏡像中增長一個 layer。在未指定絕對路徑的時候,會放到 WORKDIR 目錄下面。

root@cc2a5605f905:/# ls dockfile/
dockerfile-add  dockerfile-cmd  dockerfile-env  dockerfile-ports  dockerfile-user  dockerfile-user-h
root@cc2a5605f905:/# pwd
/

那二者有什麼區別呢?

  • ADD 多了2個功能, 下載URL和對支持的壓縮格式的包進行解壓.  其餘都同樣。好比 ADD http://foo.com/bar.go /tmp/main.go 會將文件從因特網上方下載下來,ADD /foo.tar.gz /tmp/ 會將壓縮文件解壓再COPY過去
  • 若是你不但願壓縮文件拷貝到container後會被解壓的話, 那麼使用COPY。
  • 若是須要自動下載URL並拷貝到container的話, 請使用ADD

3.1.2 CMD

CMD:在容器被建立後執行的命令,和 RUN 不一樣,它是在構造容器時候所執行的命令
# Usage 1: CMD application "argument", "argument", ..
CMD "echo" "Hello docker!"

CMD 有三種格式:

  • CMD ["executable","param1","param2"] (like an exec, preferred form)
  • CMD ["param1","param2"] (做爲 ENTRYPOINT 的參數)
  • CMD command param1 param2 (做爲 shell 運行)

一個Dockerfile裏只能有一個CMD,若是有多個,只有最後一個生效。

3.1.3 ENTRYPOINT

ENTRYPOINT :設置默認應用,會保證每次容器被建立後該應用都會被執行。CMD 和 ENTRYPOINT 的關係會在下面詳細解釋。

3.1.4 ENV:設置環境變量,可使用屢次

# Usage: ENV key value
ENV SERVER_WORKS 4

設置了後,後續的RUN命令均可以使用,而且會做爲容器的環境變量。舉個例子,下面是 dockfile:

FROM ubuntu:14.04
ENV abc=1
ENV def=2
ENTRYPOINT top

生成鏡像:docker build -t envimg4 -f dockerfile-env . 其元數據包括了這兩個環境變量:

"Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                "abc=1",
                "def=2"
            ],

啓動容器:docker run -it --name envc41 envimg4。也能看到:

"Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                "abc=1",
                "def=2"
            ]

進入容器:能看到定義的 abc 和 def 變量

root@devstack:/home/sammy/ntponubuntu# docker exec -it envc41 bash
root@ba460e0e9dc4:/# echo $abc
1
root@ba460e0e9dc4:/# echo $def
2

3.1.5 EXPOSE :向容器外暴露一個端口

# Usage: EXPOSE [port]
EXPOSE 8080

3.1.6 FROM:指定進行的基礎鏡像,必須是第一條指令

# Usage: FROM [image name]
FROM ubuntu

3.1.7 MAINTAINER:能夠在任意地方使用,設置鏡像的做者

# Usage: MAINTAINER [name]
MAINTAINER authors_name

3.1.8 RUN:運行命令,結果會生成鏡像中的一個新層

# Usage: RUN [command]
RUN aptitude install -y ntp

3.1.9 USER:設置該鏡像的容器的主進程所使用的用戶,以及後續 RUN, CMD 和 ENTRYPOINT 指令運行所使用的用戶

語法:

# Usage: USER [UID]
USER 751 

Dockerfile 中的默認用戶是基礎鏡像中所使用的用戶。好比,你的鏡像是從一個使用非 root 用戶 sammy 的鏡像繼承而來的,那麼你的 Dockerfile 中 RUN 指定運行的命令的用戶就會使用 sammy 用戶。

舉例:

(1)建立 dockerfile 文件

root@devstack:/home/sammy/dockerfile# cat dockerfile-user
FROM ubuntu:14.04
USER 1000
ENTRYPOINT top

(2)建立鏡像:docker build -t dockerfile-user-1000 -f dockerfile-user .

(3)啓動容器:docker run -it --name c-user-1000-3 dockerfile-user-1000 top

能看出來當前用戶ID 爲 1000: 

PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND
    1 1000      20   0    4440    648    548 S  0.0  0.0   0:00.00 sh
    5 1000      20   0   19840   1296    984 R  0.0  0.1   0:00.00 top 

(4)基於該鏡像再創造一個鏡像,而後再啓動一個容器,能夠發現容器中進程所使用的用戶ID 一樣爲 1000. 

3.1.10 VOLUME:容許容器訪問host上某個目錄

# Usage: VOLUME ["/dir_1", "/dir_2" ..]
VOLUME ["/my_files"]

3.1.11 WORKDIR:設置 CMD 所指定命令的執行目錄

# Usage: WORKDIR /path
WORKDIR ~/

3.1.12 HEALTHCHECK: 容器健康檢查

這是 Docker 1.12 版本中新引入的指令,其語法爲 HEALTHCHECK [OPTIONS] CMD command。 來看一個例子:

FROM ubuntu:14.04
MAINTAINER Sammy Liu <sammy.liu@unknow.com>
RUN apt-get update
RUN apt-get -y install curl
EXPOSE 8888
CMD while true; do echo 'hello world' | nc -l -p 8888; done
HEALTHCHECK --interval=10s --timeout=2s CMD curl -f http://localhost:8888/ || exit 1

在啓動容器後,其health 狀態首先是 starting,而後在過了10秒作了第一次健康檢查成功後,變爲 healthy 狀態。

root@devstack:/home/sammy/dockerfile# docker ps | grep c-health2
4c459eef1894        img-health2         "/bin/sh -c 'while tr"   7 seconds ago       Up 6 seconds (health: starting)   8888/tcp                  c-health2
root@devstack:/home/sammy/dockerfile# docker ps | grep c-health2
4c459eef1894        img-health2         "/bin/sh -c 'while tr"   9 seconds ago       Up 8 seconds (health: starting)   8888/tcp                  c-health2
root@devstack:/home/sammy/dockerfile# docker ps | grep c-health2
4c459eef1894        img-health2         "/bin/sh -c 'while tr"   11 seconds ago      Up 11 seconds (healthy)     8888/tcp                  c-health2

須要注意的是 CMD 是在容器以內運行的,所以,你須要確保其命令或者腳本存在於容器以內而且能夠被運行。

3.2 幾個比較繞的地方

3.2.1 EXPOSE 和 docker run -p -P 之間的關係

容器的端口必須被髮出(publish)出來後才能被外界使用。Dockerfile 中的 EXPOSE 只是「標記」某個端口會被暴露出來,只有在使用了 docker run -p 或者 -P 後,端口才會被「發出」出來,此時端口才能被使用。

舉例:

(1)Dockerfile

FROM ubuntu:14.04
MAINTAINER Sammy Liu <sammy.liu@unknow.com>
CMD while true; do echo 'hello world' | nc -l -p 8888; done

(2)建立鏡像:docker build -t no-exposed-ports -f dockerfile-ports .

(3)啓動容器1:docker run -d --name no-exposed-ports1 no-exposed-ports。此容器沒有 exposed 和 published 任何端口。

(4)啓動容器2:docker run -d --name no-exposed-ports2 -p 8888:8888 no-exposed-ports

此時容器的 8888 端口被髮布爲主機上的 8888 端口:

"Ports": {
                "8888/tcp": [
                    {
                        "HostIp": "0.0.0.0",
                        "HostPort": "8888"
                    }
                ]
            }

該端口會正確返回:

root@devstack:/home/sammy/dockerfile# telnet 0.0.0.0 8888
Trying 0.0.0.0...
Connected to 0.0.0.0.
Escape character is '^]'.
hello world
Connection closed by foreign host.

(5)使用 -P 參數:docker run -d --name no-exposed-ports3 -P no-exposed-ports

此時沒有任何端口被 published,說明 Docker 在使用了 「-P」 情形下只是自動將 exposed 的端口 published。

(6)使用 -p 加上一個不存在的端口:docker run -d --name no-exposed-ports4 -p 8889:8889 no-exposed-ports

此時,8889 端口會被暴露,可是無法使用。說明 -p 會將沒有 exposed 的端口自動 exposed 出來。

(7)修改 dockerfile 爲:

FROM ubuntu:14.04
MAINTAINER Sammy Liu <sammy.liu@unknow.com>
EXPOSE 8888
CMD while true; do echo 'hello world' | nc -l -p 8888; done

建立鏡像exposed-ports, 再運行 docker run -d --name exposed-ports1 -P exposed-ports 建立一個容器,此時 8888 端口自動被 published 爲主機上的 32776 端口:

"Ports": {
                "8888/tcp": [
                    {
                        "HostIp": "0.0.0.0",
                        "HostPort": "32776"
                    }
                ]
            }

可見:

  • EXPOSE或者--expose只是爲其餘命令提供所需信息的元數據,或者只是告訴容器操做人員有哪些已知選擇。它只是做爲記錄機制,也就是告訴用戶哪些端口會提供服務。它保存在容器的元數據中。
  • 使用 -p 發佈特定端口。若是該端口已經被 exposed,則發佈它;若是它尚未被 exposed,則它會被 exposed 和 published。Docker 不會檢查容器端口的正確性。
  • 使用 -P 時 Docker 會自動將全部已經被 exposed 的端口發出出來。

3.2.2 CMD 和 ENTRYPOINT

這兩個指令都指定了運行容器時所運行的命令。如下是它們共存的一些規則:

  • Dockerfile 至少須要指定一個 CMD 或者 ENTRYPOINT 指令
  • CMD 能夠用來指定 ENTRYPOINT 指令的參數
  沒有 ENTRYPOINT ENTRYPOINT exec_entry p1_entry ENTRYPOINT [「exec_entry」, 「p1_entry」]
沒有 CMD 錯誤,不容許 /bin/sh -c exec_entry p1_entry  exec_entry p1_entry
CMD [「exec_cmd」, 「p1_cmd」]  exec_cmd p1_cmd  /bin/sh -c exec_entry p1_entry exec_cmd p1_cmd  exec_entry p1_entry exec_cmd p1_cmd
CMD [「p1_cmd」, 「p2_cmd」]  p1_cmd p2_cmd  /bin/sh -c exec_entry p1_entry p1_cmd p2_cmd  exec_entry p1_entry p1_cmd p2_cmd
CMD exec_cmd p1_cmd  /bin/sh -c exec_cmd p1_cmd  /bin/sh -c exec_entry p1_entry /bin/sh -c exec_cmd p1_cmd  exec_entry p1_entry /bin/sh -c exec_cmd p1_cmd
備註 只有 CMD 時,執行 CMD 定義的指令  CMD 和 ENTRYPOINT 都存在時,CMD 的指令做爲 ENTRYPOINT 的參數  

 舉例:

(1)同時有 CMD 和 ENTRYPOINT

FROM ubuntu:14.04
MAINTAINER Sammy Liu <sammy.liu@unknow.com>
CMD top
ENTRYPOINT ps

此時會運行的指令爲 /bin/sh -c ps /bin/sh -c top

可是實際上只是運行了 ps:

root@devstack:/home/sammy/dockerfile# /bin/sh -c ps /bin/sh -c top
  PID TTY          TIME CMD
10789 pts/3    00:00:00 su
10790 pts/3    00:00:00 bash
18479 pts/3    00:00:00 sh
18480 pts/3    00:00:00 ps
root@devstack:/home/sammy/dockerfile# /bin/sh -c ps
  PID TTY          TIME CMD
10789 pts/3    00:00:00 su
10790 pts/3    00:00:00 bash
18481 pts/3    00:00:00 sh
18482 pts/3    00:00:00 ps

(2)CMD 做爲 ENTRYPOINT 的參數

FROM ubuntu:14.04
MAINTAINER Sammy Liu <sammy.liu@unknow.com>
CMD ["-n", "10"]
ENTRYPOINT top

啓動容器後運行的命令爲 /bin/sh -c top -n 10.

4. 在 Docker hub 上建立本身的鏡像

當咱們從docker鏡像倉庫中下載的鏡像不能知足咱們的需求時,咱們能夠經過如下兩種方式對鏡像進行更改。

  • 從已經建立的容器中更新鏡像,而且提交這個鏡像
  • 使用 Dockerfile 指令來建立一個新的鏡像

經過如下步驟,採用第一種方法,在 docker hub 上建立本身的鏡像:

(1)建立 docker hub 賬號。https://hub.docker.com/

(2)基於一個鏡像完成某些操做。好比基於 nginx 鏡像,安裝 ping ifconfig 等網絡工具。首先運行 docker run -it nginx /bin/bash 基於 nginx:latest 建立一個容器,而後在容器中執行 apt-get 命令安裝軟件,而後運行 exit 退出容器。

(3)將容器中的內容保存爲一個鏡像

docker commit -m="install net tools" -a="sammyliu8" 3f8a4339aadd sammyliu8/nginx:v1

這裏的 3f8a4339aadd 爲剛纔容器的ID。此時,能在本地看到該鏡像:

(4)運行 docker login 登陸 docker hub

(5)運行 docker push sammyliu8/nginx 將鏡像上傳到 docker hub。此時在 Docker hub 界面上能看到該鏡像了。

 

(6)在其餘節點上,能夠運行 docker pull sammyliu8/nginx 拉該鏡像了。

(7)不過,這樣作出來的新nginx有個問題,那就是nginx 服務不會自動起來。這是由於,官方的 nginx 的CMD 爲 nginx -g "daemon off;",可是新的鏡像的CMD 爲 /bin/bash。可是,運行前面命令啓動的容器又沒法安裝軟件。所以,只能先按照上面的步驟啓動一個容器,製做鏡像,而後基於該鏡像再不帶命令地再啓一個容器,在另外一個窗口中,使用docker commit 將其保存爲新的鏡像,並上傳到docker hub中。問題解決。

 

 

參考連接

 
相關文章
相關標籤/搜索