本系列文章將介紹Docker的有關知識:
(2)Docker 鏡像
(3)Docker 容器的隔離性 - 使用 Linux namespace 隔離容器的運行環境
(4)Docker 容器的隔離性 - 使用 cgroups 限制容器使用的資源
(5)Docker 網絡
對於每個軟件,除了它自身的代碼以外,它的運行還需要有一個運行環境和依賴。不管這個軟件是象往常一樣運行在物理機或者虛機之中,還是運行在現在的容器之中,這些都是不變的。在傳統環境中,軟件在運行之前也需要經過 代碼開發->運行環境准備 -> 安裝軟件 -> 運行軟件 等環節,在容器環境中,中間的兩個環節被鏡像制作過程替代了。也就是說,鏡像的制作也包括運行環境准備和安裝軟件等兩個主要環節,以及一些其他環節。因此,Docker 容器鏡像其實並沒有什么新的理論,只是這過程有了新的方式而已。
鏡像(image)是動態的容器的靜態表示(specification),包括容器所要運行的應用代碼以及運行時的配置。Docker 鏡像包括一個或者多個只讀層( read-only layers ),因此,鏡像一旦被創建就再也不能被修改了。一個運行着的Docker 容器是一個鏡像的實例( instantiation )。從同一個鏡像中運行的容器包含有相同的應用代碼和運行時依賴。但是不像鏡像是靜態的,每個運行着的容器都有一個可寫層( writable layer ,也成為容器層 container layer),它位於底下的若干只讀層之上。運行時的所有變化,包括對數據和文件的寫和更新,都會保存在這個層中。因此,從同一個鏡像運行的多個容器包含了不同的容器層。
Docker 有兩種方式來創建一個容器鏡像:
- 創建一個容器,運行若干命令,再使用 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。
一些說明:
- 關於 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
# 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
# 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中。問題解決。
參考鏈接
- http://developers.redhat.com/blog/2016/03/09/more-about-docker-images-size/
- http://developers.redhat.com/blog/2016/02/24/10-things-to-avoid-in-docker-containers/
- http://techknowblogs.blogspot.in/2017/12/docker-host-os-guest-os-base-image-etc.html
- http://www.floydhilton.com/docker/2017/03/31/Docker-ContainerHost-vs-ContainerOS-Linux-Windows.html
- http://crunchtools.com/comparison-linux-container-images/