Docker 鏡像加速器
概述
國(guó)內(nèi)從 Docker Hub 拉取鏡像有時(shí)會(huì)遇到困難仙粱,此時(shí)可以配置鏡像加速器苟跪。Docker 官方和國(guó)內(nèi)很多云服務(wù)商都提供了國(guó)內(nèi)加速器服務(wù)岛宦,例如:
- [Docker 官方提供的中國(guó) registry mirror]
- [阿里云加速器]
- [DaoCloud 加速器]
我們以 Docker 官方加速器為例進(jìn)行介紹失尖。
Ubuntu 14.04较剃、Debian 7 Wheezy
對(duì)于使用 upstart 的系統(tǒng)而言勒葱,編輯/etc/default/docker
文件篮洁,在其中的DOCKER_OPTS
中配置加速器地址:
DOCKER_OPTS="--registry-mirror=https://registry.docker-cn.com"
重新啟動(dòng)服務(wù)蜗侈。
$ sudo service docker restart
Ubuntu 16.04+、Debian 8+、CentOS 7
對(duì)于使用 systemd 的系統(tǒng)题造,請(qǐng)?jiān)?code>/etc/docker/daemon.json中寫(xiě)入如下內(nèi)容(如果文件不存在請(qǐng)新建該文件)
{
"registry-mirrors": [
"https://registry.docker-cn.com"
]
}
注意,一定要保證該文件符合 json 規(guī)范牵触,否則 Docker 將不能啟動(dòng)淮悼。
之后重新啟動(dòng)服務(wù)。
$ sudo systemctl daemon-reload
$ sudo systemctl restart docker
Windows 10
對(duì)于使用 Windows 10 的系統(tǒng)揽思,在系統(tǒng)右下角托盤(pán) Docker 圖標(biāo)內(nèi)右鍵菜單選擇Settings
袜腥,打開(kāi)配置窗口后左側(cè)導(dǎo)航菜單選擇Daemon
锡宋。在Registry mirrors
一欄中填寫(xiě)加速器地址https://registry.docker-cn.com
特恬,之后點(diǎn)擊Apply
保存后 Docker 就會(huì)重啟并應(yīng)用配置的鏡像地址了执俩。
macOS
對(duì)于使用 macOS 的用戶(hù)矮固,在任務(wù)欄點(diǎn)擊 Docker for mac 應(yīng)用圖標(biāo) -> Perferences... -> Daemon -> Registry mirrors浦妄。在列表中填寫(xiě)加速器地址?https://registry.docker-cn.com
儿咱。修改完成之后搔体,點(diǎn)擊Apply & Restart
按鈕缩多,Docker 就會(huì)重啟并應(yīng)用配置的鏡像地址了呆奕。
檢查加速器是否生效
配置加速器之后,如果拉取鏡像仍然十分緩慢衬吆,請(qǐng)手動(dòng)檢查加速器配置是否生效梁钾,在命令行執(zhí)行docker info
,如果從結(jié)果中看到了如下內(nèi)容逊抡,說(shuō)明配置成功姆泻。
Registry Mirrors:
https://registry.docker-cn.com/
Docker 鏡像
使用 Docker 鏡像
在之前的介紹中,我們知道鏡像是 Docker 的三大組件之一冒嫡。
Docker 運(yùn)行容器前需要本地存在對(duì)應(yīng)的鏡像拇勃,如果本地不存在該鏡像,Docker 會(huì)從鏡像倉(cāng)庫(kù)下載該鏡像灯谣。
列出鏡像
概述
要想列出已經(jīng)下載下來(lái)的鏡像潜秋,可以使用docker images
命令蛔琅。
root@suoron:~# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
hello-world latest fce289e99eb9 7 weeks ago 1.84kB
<none> <none> 00285df0df87 5 days ago 342 MB
列表包含了倉(cāng)庫(kù)名
胎许、標(biāo)簽
、鏡像 ID
罗售、創(chuàng)建時(shí)間
以及所占用的空間
辜窑。
其中倉(cāng)庫(kù)名、標(biāo)簽在之前的基礎(chǔ)概念章節(jié)已經(jīng)介紹過(guò)了寨躁。鏡像 ID則是鏡像的唯一標(biāo)識(shí)穆碎,一個(gè)鏡像可以對(duì)應(yīng)多個(gè)標(biāo)簽。因此职恳,在上面的例子中所禀,我們可以看到ubuntu:16.04
和?ubuntu:latest
?擁有相同的 ID,因?yàn)樗鼈儗?duì)應(yīng)的是同一個(gè)鏡像放钦。
鏡像體積
如果仔細(xì)觀察色徘,會(huì)注意到,這里標(biāo)識(shí)的所占用空間和在 Docker Hub 上看到的鏡像大小不同操禀。比如褂策,ubuntu:16.04
鏡像大小,在這里是127 MB
,但是在 Docker Hub 顯示的卻是50 MB
斤寂。這是因?yàn)?Docker Hub 中顯示的體積是壓縮后的體積耿焊。在鏡像下載和上傳過(guò)程中鏡像是保持著壓縮狀態(tài)的,因此 Docker Hub 所顯示的大小是網(wǎng)絡(luò)傳輸中更關(guān)心的流量大小。而docker image ls
顯示的是鏡像下載到本地后甜熔,展開(kāi)的大小业稼,準(zhǔn)確說(shuō),是展開(kāi)后的各層所占空間的總和歇父,因?yàn)殓R像到本地后,查看空間的時(shí)候再愈,更關(guān)心的是本地磁盤(pán)空間占用的大小榜苫。
另外一個(gè)需要注意的問(wèn)題是,docker image ls
列表中的鏡像體積總和并非是所有鏡像實(shí)際硬盤(pán)消耗翎冲。由于 Docker 鏡像是多層存儲(chǔ)結(jié)構(gòu)垂睬,并且可以繼承、復(fù)用抗悍,因此不同鏡像可能會(huì)因?yàn)槭褂孟嗤幕A(chǔ)鏡像驹饺,從而擁有共同的層。由于 Docker 使用 Union FS缴渊,相同的層只需要保存一份即可赏壹,因此實(shí)際鏡像硬盤(pán)占用空間很可能要比這個(gè)列表鏡像大小的總和要小的多。
你可以通過(guò)以下命令來(lái)便捷的查看鏡像衔沼、容器蝌借、數(shù)據(jù)卷所占用的空間。
$ docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 24 0 1.992GB 1.992GB (100%)
Containers 1 0 62.82MB 62.82MB (100%)
Local Volumes 9 0 652.2MB 652.2MB (100%)
Build Cache 0B 0B
虛懸鏡像
上面的鏡像列表中指蚁,還可以看到一個(gè)特殊的鏡像菩佑,這個(gè)鏡像既沒(méi)有倉(cāng)庫(kù)名,也沒(méi)有標(biāo)簽凝化,均為<none>
稍坯。:
<none> <none> 00285df0df87 5 days ago 342 MB
這個(gè)鏡像原本是有鏡像名和標(biāo)簽的,原來(lái)為mongo:3.2
搓劫,隨著官方鏡像維護(hù)瞧哟,發(fā)布了新版本后,重新docker pull mongo:3.2
時(shí)枪向,mongo:3.2
這個(gè)鏡像名被轉(zhuǎn)移到了新下載的鏡像身上勤揩,而舊的鏡像上的這個(gè)名稱(chēng)則被取消,從而成為了<none>
遣疯。除了docker pull
可能導(dǎo)致這種情況雄可,docker build
也同樣可以導(dǎo)致這種現(xiàn)象凿傅。由于新舊鏡像同名,舊鏡像名稱(chēng)被取消数苫,從而出現(xiàn)倉(cāng)庫(kù)名聪舒、標(biāo)簽均為<none>
的鏡像。這類(lèi)無(wú)標(biāo)簽鏡像也被稱(chēng)為虛懸鏡像(dangling image)虐急,可以用下面的命令專(zhuān)門(mén)顯示這類(lèi)鏡像:
$ docker image ls -f dangling=true
REPOSITORY TAG IMAGE ID CREATED SIZE
<none> <none> 00285df0df87 5 days ago 342 MB
一般來(lái)說(shuō)箱残,虛懸鏡像已經(jīng)失去了存在的價(jià)值,是可以隨意刪除的止吁,可以用下面的命令刪除被辑。
$ docker image prune
中間層鏡像
為了加速鏡像構(gòu)建、重復(fù)利用資源敬惦,Docker 會(huì)利用中間層鏡像盼理。所以在使用一段時(shí)間后,可能會(huì)看到一些依賴(lài)的中間層鏡像俄删。默認(rèn)的docker image ls
列表中只會(huì)顯示頂層鏡像宏怔,如果希望顯示包括中間層鏡像在內(nèi)的所有鏡像的話(huà),需要加-a
參數(shù)畴椰。
$ docker image ls -a
這樣會(huì)看到很多無(wú)標(biāo)簽的鏡像臊诊,與之前的虛懸鏡像不同,這些無(wú)標(biāo)簽的鏡像很多都是中間層鏡像斜脂,是其它鏡像所依賴(lài)的鏡像抓艳。這些無(wú)標(biāo)簽鏡像不應(yīng)該刪除,否則會(huì)導(dǎo)致上層鏡像因?yàn)橐蕾?lài)丟失而出錯(cuò)帚戳。實(shí)際上玷或,這些鏡像也沒(méi)必要?jiǎng)h除,因?yàn)橹罢f(shuō)過(guò)销斟,相同的層只會(huì)存一遍庐椒,而這些鏡像是別的鏡像的依賴(lài),因此并不會(huì)因?yàn)樗鼈儽涣谐鰜?lái)而多存了一份蚂踊,無(wú)論如何你也會(huì)需要它們。只要?jiǎng)h除那些依賴(lài)它們的鏡像后笔宿,這些依賴(lài)的中間層鏡像也會(huì)被連帶刪除犁钟。
列出部分鏡像
不加任何參數(shù)的情況下,docker image ls
會(huì)列出所有頂級(jí)鏡像泼橘,但是有時(shí)候我們只希望列出部分鏡像涝动。docker image ls
有好幾個(gè)參數(shù)可以幫助做到這個(gè)事情。
根據(jù)倉(cāng)庫(kù)名列出鏡像
$ docker image ls ubuntu
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu 16.04 f753707788c5 4 weeks ago 127 MB
ubuntu latest f753707788c5 4 weeks ago 127 MB
ubuntu 14.04 1e0c3dd64ccd 4 weeks ago 188 MB
列出特定的某個(gè)鏡像炬灭,也就是說(shuō)指定倉(cāng)庫(kù)名和標(biāo)簽
$ docker image ls ubuntu:16.04
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu 16.04 f753707788c5 4 weeks ago 127 MB
除此以外醋粟,docker image ls
還支持強(qiáng)大的過(guò)濾器參數(shù)--filter
,或者簡(jiǎn)寫(xiě)-f
。之前我們已經(jīng)看到了使用過(guò)濾器來(lái)列出虛懸鏡像的用法米愿,它還有更多的用法厦凤。比如,我們希望看到在mongo:3.2
之后建立的鏡像育苟,可以用下面的命令:
$ docker image ls -f since=mongo:3.2
REPOSITORY TAG IMAGE ID CREATED SIZE
redis latest 5f515359c7f8 5 days ago 183 MB
nginx latest 05a60462f8ba 5 days ago 181 MB
想查看某個(gè)位置之前的鏡像也可以较鼓,只需要把since
換成before
即可。
此外违柏,如果鏡像構(gòu)建時(shí)博烂,定義了LABEL
,還可以通過(guò)LABEL
來(lái)過(guò)濾漱竖。
$ docker image ls -f label=com.example.version=0.1
...
以特定格式顯示
默認(rèn)情況下禽篱,docker image ls
會(huì)輸出一個(gè)完整的表格,但是我們并非所有時(shí)候都會(huì)需要這些內(nèi)容馍惹。比如谆级,剛才刪除虛懸鏡像的時(shí)候,我們需要利用docker image ls
把所有的虛懸鏡像的 ID 列出來(lái)讼积,然后才可以交給docker image rm
命令作為參數(shù)來(lái)刪除指定的這些鏡像肥照,這個(gè)時(shí)候就用到了-q
參數(shù)。
$ docker image ls -q
5f515359c7f8
05a60462f8ba
fe9198c04d62
00285df0df87
f753707788c5
f753707788c5
1e0c3dd64ccd
--filter
配合-q
產(chǎn)生出指定范圍的 ID 列表勤众,然后送給另一個(gè)docker
命令作為參數(shù)舆绎,從而針對(duì)這組實(shí)體成批的進(jìn)行某種操作的做法在 Docker 命令行使用過(guò)程中非常常見(jiàn),不僅僅是鏡像们颜,將來(lái)我們會(huì)在各個(gè)命令中看到這類(lèi)搭配以完成很強(qiáng)大的功能吕朵。因此每次在文檔看到過(guò)濾器后,可以多注意一下它們的用法窥突。
另外一些時(shí)候努溃,我們可能只是對(duì)表格的結(jié)構(gòu)不滿(mǎn)意,希望自己組織列阻问;或者不希望有標(biāo)題梧税,這樣方便其它程序解析結(jié)果等,這就用到了 Go 的模板語(yǔ)法称近。
比如第队,下面的命令會(huì)直接列出鏡像結(jié)果,并且只包含鏡像ID和倉(cāng)庫(kù)名:
$ docker image ls --format "{{.ID}}: {{.Repository}}"
5f515359c7f8: redis
05a60462f8ba: nginx
fe9198c04d62: mongo
00285df0df87: <none>
f753707788c5: ubuntu
f753707788c5: ubuntu
1e0c3dd64ccd: ubuntu
或者打算以表格等距顯示刨秆,并且有標(biāo)題行凳谦,和默認(rèn)一樣,不過(guò)自己定義列:
$ docker image ls --format "table {{.ID}}\t{{.Repository}}\t{{.Tag}}"
IMAGE ID REPOSITORY TAG
5f515359c7f8 redis latest
05a60462f8ba nginx latest
fe9198c04d62 mongo 3.2
00285df0df87 <none> <none>
f753707788c5 ubuntu 16.04
f753707788c5 ubuntu latest
1e0c3dd64ccd ubuntu 14.04
刪除本地鏡像
概述
如果要?jiǎng)h除本地的鏡像衡未,可以使用docker image rm
命令尸执,其格式為:
$ docker image rm [選項(xiàng)] <鏡像1> [<鏡像2> ...]
用 ID家凯、鏡像名、摘要?jiǎng)h除鏡像
其中如失,<鏡像>
可以是鏡像短 ID
绊诲、鏡像長(zhǎng) ID
、鏡像名
或者鏡像摘要
岖常。
比如我們有這么一些鏡像:
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
centos latest 0584b3d2cf6d 3 weeks ago 196.5 MB
redis alpine 501ad78535f0 3 weeks ago 21.03 MB
docker latest cf693ec9b5c7 3 weeks ago 105.1 MB
nginx latest e43d811ce2f4 5 weeks ago 181.5 MB
我們可以用鏡像的完整 ID驯镊,也稱(chēng)為長(zhǎng) ID
,來(lái)刪除鏡像竭鞍。使用腳本的時(shí)候可能會(huì)用長(zhǎng) ID板惑,但是人工輸入就太累了,所以更多的時(shí)候是用短 ID
來(lái)刪除鏡像偎快。docker image ls
默認(rèn)列出的就已經(jīng)是短 ID 了冯乘,一般取前3個(gè)字符以上,只要足夠區(qū)分于別的鏡像就可以了晒夹。
比如這里裆馒,如果我們要?jiǎng)h除redis:alpine
鏡像,可以執(zhí)行:
$ docker image rm 501
Untagged: redis:alpine
Untagged: redis@sha256:f1ed3708f538b537eb9c2a7dd50dc90a706f7debd7e1196c9264edeea521a86d
Deleted: sha256:501ad78535f015d88872e13fa87a828425117e3d28075d0c117932b05bf189b7
Deleted: sha256:96167737e29ca8e9d74982ef2a0dda76ed7b430da55e321c071f0dbff8c2899b
Deleted: sha256:32770d1dcf835f192cafd6b9263b7b597a1778a403a109e2cc2ee866f74adf23
Deleted: sha256:127227698ad74a5846ff5153475e03439d96d4b1c7f2a449c7a826ef74a2d2fa
Deleted: sha256:1333ecc582459bac54e1437335c0816bc17634e131ea0cc48daa27d32c75eab3
Deleted: sha256:4fc455b921edf9c4aea207c51ab39b10b06540c8b4825ba57b3feed1668fa7c7
我們也可以用鏡像名
丐怯,也就是<倉(cāng)庫(kù)名>:<標(biāo)簽>
喷好,來(lái)刪除鏡像。
$ docker image rm centos
Untagged: centos:latest
Untagged: centos@sha256:b2f9d1c0ff5f87a4743104d099a3d561002ac500db1b9bfa02a783a46e0d366c
Deleted: sha256:0584b3d2cf6d235ee310cf14b54667d889887b838d3f3d3033acd70fc3c48b8a
Deleted: sha256:97ca462ad9eeae25941546209454496e1d66749d53dfa2ee32bf1faabd239d38
當(dāng)然读跷,更精確的是使用鏡像摘要
刪除鏡像梗搅。
$ docker image ls --digests
REPOSITORY TAG DIGEST IMAGE ID CREATED SIZE
node slim sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228 6e0c4c8e3913 3 weeks ago 214 MB
$ docker image rm node@sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228
Untagged: node@sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228
Untagged 和 Deleted
如果觀察上面這幾個(gè)命令的運(yùn)行輸出信息的話(huà),你會(huì)注意到刪除行為分為兩類(lèi)效览,一類(lèi)是Untagged
无切,另一類(lèi)是Deleted
。我們之前介紹過(guò)丐枉,鏡像的唯一標(biāo)識(shí)是其 ID 和摘要哆键,而一個(gè)鏡像可以有多個(gè)標(biāo)簽。
因此當(dāng)我們使用上面命令刪除鏡像的時(shí)候瘦锹,實(shí)際上是在要求刪除某個(gè)標(biāo)簽的鏡像籍嘹。所以首先需要做的是將滿(mǎn)足我們要求的所有鏡像標(biāo)簽都取消,這就是我們看到的Untagged
的信息沼本。因?yàn)橐粋€(gè)鏡像可以對(duì)應(yīng)多個(gè)標(biāo)簽噩峦,因此當(dāng)我們刪除了所指定的標(biāo)簽后,可能還有別的標(biāo)簽指向了這個(gè)鏡像抽兆,如果是這種情況,那么Delete
行為就不會(huì)發(fā)生族淮。所以并非所有的docker image rm
都會(huì)產(chǎn)生刪除鏡像的行為辫红,有可能僅僅是取消了某個(gè)標(biāo)簽而已凭涂。
當(dāng)該鏡像所有的標(biāo)簽都被取消了,該鏡像很可能會(huì)失去了存在的意義贴妻,因此會(huì)觸發(fā)刪除行為切油。鏡像是多層存儲(chǔ)結(jié)構(gòu),因此在刪除的時(shí)候也是從上層向基礎(chǔ)層方向依次進(jìn)行判斷刪除名惩。鏡像的多層結(jié)構(gòu)讓鏡像復(fù)用變動(dòng)非常容易澎胡,因此很有可能某個(gè)其它鏡像正依賴(lài)于當(dāng)前鏡像的某一層。這種情況娩鹉,依舊不會(huì)觸發(fā)刪除該層的行為攻谁。直到?jīng)]有任何層依賴(lài)當(dāng)前層時(shí),才會(huì)真實(shí)的刪除當(dāng)前層弯予。這就是為什么戚宦,有時(shí)候會(huì)奇怪,為什么明明沒(méi)有別的標(biāo)簽指向這個(gè)鏡像锈嫩,但是它還是存在的原因受楼,也是為什么有時(shí)候會(huì)發(fā)現(xiàn)所刪除的層數(shù)和自己docker pull
看到的層數(shù)不一樣的源。
除了鏡像依賴(lài)以外呼寸,還需要注意的是容器對(duì)鏡像的依賴(lài)艳汽。如果有用這個(gè)鏡像啟動(dòng)的容器存在(即使容器沒(méi)有運(yùn)行),那么同樣不可以刪除這個(gè)鏡像对雪。之前講過(guò)河狐,容器是以鏡像為基礎(chǔ),再加一層容器存儲(chǔ)層慌植,組成這樣的多層存儲(chǔ)結(jié)構(gòu)去運(yùn)行的甚牲。因此該鏡像如果被這個(gè)容器所依賴(lài)的,那么刪除必然會(huì)導(dǎo)致故障蝶柿。如果這些容器是不需要的丈钙,應(yīng)該先將它們刪除,然后再來(lái)刪除鏡像交汤。
用 docker image ls 命令來(lái)配合
像其它可以承接多個(gè)實(shí)體的命令一樣雏赦,可以使用docker image ls -q
來(lái)配合使用docker image rm
,這樣可以成批的刪除希望刪除的鏡像芙扎。我們?cè)凇扮R像列表”章節(jié)介紹過(guò)很多過(guò)濾鏡像列表的方式都可以拿過(guò)來(lái)使用星岗。
比如,我們需要?jiǎng)h除所有倉(cāng)庫(kù)名為redis
的鏡像:
$ docker image rm $(docker image ls -q redis)
或者刪除所有在mongo:3.2
之前的鏡像:
$ docker image rm $(docker image ls -q -f before=mongo:3.2)
充分利用你的想象力和 Linux 命令行的強(qiáng)大戒洼,你可以完成很多非常贊的功能俏橘。
CentOS/RHEL 的用戶(hù)需要注意的事項(xiàng)
在 Ubuntu/Debian 上有UnionFS
可以使用,如aufs
或者overlay2
圈浇,而 CentOS 和 RHEL 的內(nèi)核中沒(méi)有相關(guān)驅(qū)動(dòng)寥掐。因此對(duì)于這類(lèi)系統(tǒng)靴寂,一般使用devicemapper
驅(qū)動(dòng)利用 LVM 的一些機(jī)制來(lái)模擬分層存儲(chǔ)。這樣的做法除了性能比較差外召耘,穩(wěn)定性一般也不好百炬,而且配置相對(duì)復(fù)雜。Docker 安裝在 CentOS/RHEL 上后污它,會(huì)默認(rèn)選擇devicemapper
剖踊,但是為了簡(jiǎn)化配置,其devicemapper
是跑在一個(gè)稀疏文件模擬的塊設(shè)備上衫贬,也被稱(chēng)為loop-lvm
德澈。這樣的選擇是因?yàn)椴恍枰~外配置就可以運(yùn)行 Docker,這是自動(dòng)配置唯一能做到的事情祥山。但是loop-lvm
的做法非常不好圃验,其穩(wěn)定性、性能更差缝呕,無(wú)論是日志還是docker info
中都會(huì)看到警告信息澳窑。官方文檔有明確的文章講解了如何配置塊設(shè)備給devicemapper
驅(qū)動(dòng)做存儲(chǔ)層的做法,這類(lèi)做法也被稱(chēng)為配置direct-lvm
供常。
除了前面說(shuō)到的問(wèn)題外摊聋,devicemapper
+loop-lvm
還有一個(gè)缺陷,因?yàn)樗窍∈栉募幌荆运鼤?huì)不斷增長(zhǎng)麻裁。用戶(hù)在使用過(guò)程中會(huì)注意到/var/lib/docker/devicemapper/devicemapper/data
不斷增長(zhǎng),而且無(wú)法控制源祈。很多人會(huì)希望刪除鏡像或者可以解決這個(gè)問(wèn)題煎源,結(jié)果發(fā)現(xiàn)效果并不明顯。原因就是這個(gè)稀疏文件的空間釋放后基本不進(jìn)行垃圾回收的問(wèn)題香缺。因此往往會(huì)出現(xiàn)即使刪除了文件內(nèi)容手销,空間卻無(wú)法回收,隨著使用這個(gè)稀疏文件一直在不斷增長(zhǎng)图张。
所以對(duì)于 CentOS/RHEL 的用戶(hù)來(lái)說(shuō)锋拖,在沒(méi)有辦法使用UnionFS
的情況下,一定要配置direct-lvm
給devicemapper
祸轮,無(wú)論是為了性能兽埃、穩(wěn)定性還是空間利用率。
或許有人注意到了 CentOS 7 中存在被 backports 回來(lái)的overlay
驅(qū)動(dòng)适袜,不過(guò) CentOS 里的這個(gè)驅(qū)動(dòng)達(dá)不到生產(chǎn)環(huán)境使用的穩(wěn)定程度柄错,所以不推薦使用。
使用 Dockerfile 定制鏡像
Dockerfile 定制鏡像
從剛才的docker commit
的學(xué)習(xí)中,我們可以了解到鄙陡,鏡像的定制實(shí)際上就是定制每一層所添加的配置冕房、文件躏啰。如果我們可以把每一層修改趁矾、安裝、構(gòu)建给僵、操作的命令都寫(xiě)入一個(gè)腳本毫捣,用這個(gè)腳本來(lái)構(gòu)建、定制鏡像帝际,那么之前提及的無(wú)法重復(fù)的問(wèn)題蔓同、鏡像構(gòu)建透明性的問(wèn)題、體積的問(wèn)題就都會(huì)解決。這個(gè)腳本就是 Dockerfile。
Dockerfile 是一個(gè)文本文件它掂,其內(nèi)包含了一條條的指令(Instruction)湿痢,每一條指令構(gòu)建一層,因此每一條指令的內(nèi)容筑舅,就是描述該層應(yīng)當(dāng)如何構(gòu)建。
還以之前定制nginx
鏡像為例,這次我們使用 Dockerfile 來(lái)定制尚揣。
在一個(gè)空白目錄中,建立一個(gè)文本文件掖举,并命名為Dockerfile
:
$ mkdir mynginx
$ cd mynginx
$ touch Dockerfile
其內(nèi)容為:
FROM nginx
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
這個(gè) Dockerfile 很簡(jiǎn)單快骗,一共就兩行。涉及到了兩條指令塔次,FROM
和RUN
方篮。
FROM 指定基礎(chǔ)鏡像
所謂定制鏡像,那一定是以一個(gè)鏡像為基礎(chǔ)励负,在其上進(jìn)行定制藕溅。就像我們之前運(yùn)行了一個(gè)nginx
鏡像的容器,再進(jìn)行修改一樣熄守,基礎(chǔ)鏡像是必須指定的蜈垮。而FROM
就是指定基礎(chǔ)鏡像,因此一個(gè)Dockerfile
中FROM
是必備的指令裕照,并且必須是第一條指令攒发。
在 Docker Store 上有非常多的高質(zhì)量的官方鏡像,有可以直接拿來(lái)使用的服務(wù)類(lèi)的鏡像晋南,如 nginx
惠猿、redis
、mongo
负间、mysql
偶妖、httpd
姜凄、php
、tomcat
等趾访;也有一些方便開(kāi)發(fā)态秧、構(gòu)建、運(yùn)行各種語(yǔ)言應(yīng)用的鏡像扼鞋,如 node
申鱼、openjdk
、python
云头、ruby
捐友、golang
等±;保可以在其中尋找一個(gè)最符合我們最終目標(biāo)的鏡像為基礎(chǔ)鏡像進(jìn)行定制匣砖。
如果沒(méi)有找到對(duì)應(yīng)服務(wù)的鏡像,官方鏡像中還提供了一些更為基礎(chǔ)的操作系統(tǒng)鏡像昏滴,如 ubuntu
猴鲫、debian
、centos
影涉、fedora
变隔、alpine
等,這些操作系統(tǒng)的軟件庫(kù)為我們提供了更廣闊的擴(kuò)展空間蟹倾。
除了選擇現(xiàn)有鏡像為基礎(chǔ)鏡像外匣缘,Docker 還存在一個(gè)特殊的鏡像,名為scratch
鲜棠。這個(gè)鏡像是虛擬的概念肌厨,并不實(shí)際存在,它表示一個(gè)空白的鏡像豁陆。
FROM scratch
...
如果你以scratch
為基礎(chǔ)鏡像的話(huà)柑爸,意味著你不以任何鏡像為基礎(chǔ),接下來(lái)所寫(xiě)的指令將作為鏡像第一層開(kāi)始存在盒音。
不以任何系統(tǒng)為基礎(chǔ)表鳍,直接將可執(zhí)行文件復(fù)制進(jìn)鏡像的做法并不罕見(jiàn),比如 swarm
祥诽、coreos/etcd
譬圣。對(duì)于 Linux 下靜態(tài)編譯的程序來(lái)說(shuō),并不需要有操作系統(tǒng)提供運(yùn)行時(shí)支持雄坪,所需的一切庫(kù)都已經(jīng)在可執(zhí)行文件里了厘熟,因此直接FROM scratch
會(huì)讓鏡像體積更加小巧。使用 Go 語(yǔ)言 開(kāi)發(fā)的應(yīng)用很多會(huì)使用這種方式來(lái)制作鏡像,這也是為什么有人認(rèn)為 Go 是特別適合容器微服務(wù)架構(gòu)的語(yǔ)言的原因之一绳姨。
RUN 執(zhí)行命令
RUN
指令是用來(lái)執(zhí)行命令行命令的登澜。由于命令行的強(qiáng)大能力,RUN
指令在定制鏡像時(shí)是最常用的指令之一飘庄。其格式有兩種:
-
shell格式:
RUN <命令>
脑蠕,就像直接在命令行中輸入的命令一樣。剛才寫(xiě)的 Dockerfile 中的RUN
指令就是這種格式竭宰。
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
-
exec 格式:
RUN ["可執(zhí)行文件", "參數(shù)1", "參數(shù)2"]
空郊,這更像是函數(shù)調(diào)用中的格式。
既然RUN
就像 Shell 腳本一樣可以執(zhí)行命令切揭,那么我們是否就可以像 Shell 腳本一樣把每個(gè)命令對(duì)應(yīng)一個(gè) RUN 呢?比如這樣:
FROM debian:jessie
RUN apt-get update
RUN apt-get install -y gcc libc6-dev make
RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-3.2.5.tar.gz"
RUN mkdir -p /usr/src/redis
RUN tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1
RUN make -C /usr/src/redis
RUN make -C /usr/src/redis install
之前說(shuō)過(guò)锁摔,Dockerfile 中每一個(gè)指令都會(huì)建立一層廓旬,RUN
也不例外。每一個(gè)RUN
的行為谐腰,就和剛才我們手工建立鏡像的過(guò)程一樣:新建立一層孕豹,在其上執(zhí)行這些命令,執(zhí)行結(jié)束后十气,commit
這一層的修改励背,構(gòu)成新的鏡像。
而上面的這種寫(xiě)法砸西,創(chuàng)建了 7 層鏡像叶眉。這是完全沒(méi)有意義的,而且很多運(yùn)行時(shí)不需要的東西芹枷,都被裝進(jìn)了鏡像里衅疙,比如編譯環(huán)境、更新的軟件包等等鸳慈。結(jié)果就是產(chǎn)生非常臃腫饱溢、非常多層的鏡像,不僅僅增加了構(gòu)建部署的時(shí)間走芋,也很容易出錯(cuò)绩郎。 這是很多初學(xué) Docker 的人常犯的一個(gè)錯(cuò)誤。
Union FS 是有最大層數(shù)限制的翁逞,比如 AUFS肋杖,曾經(jīng)是最大不得超過(guò) 42 層,現(xiàn)在是不得超過(guò) 127 層熄攘。
上面的Dockerfile
正確的寫(xiě)法應(yīng)該是這樣:
FROM debian:jessie
RUN buildDeps='gcc libc6-dev make' \
&& apt-get update \
&& apt-get install -y $buildDeps \
&& wget -O redis.tar.gz "http://download.redis.io/releases/redis-3.2.5.tar.gz" \
&& mkdir -p /usr/src/redis \
&& tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \
&& make -C /usr/src/redis \
&& make -C /usr/src/redis install \
&& rm -rf /var/lib/apt/lists/* \
&& rm redis.tar.gz \
&& rm -r /usr/src/redis \
&& apt-get purge -y --auto-remove $buildDeps
首先兽愤,之前所有的命令只有一個(gè)目的,就是編譯、安裝 redis 可執(zhí)行文件浅萧。因此沒(méi)有必要建立很多層逐沙,這只是一層的事情。因此洼畅,這里沒(méi)有使用很多個(gè)RUN
對(duì)一一對(duì)應(yīng)不同的命令吩案,而是僅僅使用一個(gè)RUN
指令,并使用&&
將各個(gè)所需命令串聯(lián)起來(lái)帝簇。將之前的 7 層徘郭,簡(jiǎn)化為了 1 層。在撰寫(xiě) Dockerfile 的時(shí)候丧肴,要經(jīng)常提醒自己残揉,這并不是在寫(xiě) Shell 腳本,而是在定義每一層該如何構(gòu)建芋浮。
并且抱环,這里為了格式化還進(jìn)行了換行。Dockerfile 支持 Shell 類(lèi)的行尾添加\
的命令換行方式纸巷,以及行首#
進(jìn)行注釋的格式镇草。良好的格式,比如換行瘤旨、縮進(jìn)梯啤、注釋等,會(huì)讓維護(hù)存哲、排障更為容易因宇,這是一個(gè)比較好的習(xí)慣。
此外宏胯,還可以看到這一組命令的最后添加了清理工作的命令羽嫡,刪除了為了編譯構(gòu)建所需要的軟件,清理了所有下載肩袍、展開(kāi)的文件杭棵,并且還清理了apt
緩存文件。這是很重要的一步氛赐,我們之前說(shuō)過(guò)魂爪,鏡像是多層存儲(chǔ),每一層的東西并不會(huì)在下一層被刪除艰管,會(huì)一直跟隨著鏡像滓侍。因此鏡像構(gòu)建時(shí),一定要確保每一層只添加真正需要添加的東西牲芋,任何無(wú)關(guān)的東西都應(yīng)該清理掉撩笆。
很多人初學(xué) Docker 制作出了很臃腫的鏡像的原因之一捺球,就是忘記了每一層構(gòu)建的最后一定要清理掉無(wú)關(guān)文件。
構(gòu)建鏡像
好了夕冲,讓我們?cè)倩氐街岸ㄖ频?nginx 鏡像的 Dockerfile 來(lái)〉現(xiàn)在我們明白了這個(gè) Dockerfile 的內(nèi)容,那么讓我們來(lái)構(gòu)建這個(gè)鏡像吧歹鱼。
在Dockerfile
文件所在目錄執(zhí)行:
$ docker build -t nginx:v3 .
Sending build context to Docker daemon 2.048 kB
Step 1 : FROM nginx
---> e43d811ce2f4
Step 2 : RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
---> Running in 9cdc27646c7b
---> 44aa4490ce2c
Removing intermediate container 9cdc27646c7b
Successfully built 44aa4490ce2c
從命令的輸出結(jié)果中泣栈,我們可以清晰的看到鏡像的構(gòu)建過(guò)程。在Step 2
中弥姻,如同我們之前所說(shuō)的那樣南片,RUN
指令啟動(dòng)了一個(gè)容器9cdc27646c7b
,執(zhí)行了所要求的命令庭敦,并最后提交了這一層44aa4490ce2c
疼进,隨后刪除了所用到的這個(gè)容器9cdc27646c7b
。
這里我們使用了docker build
命令進(jìn)行鏡像構(gòu)建螺捐。其格式為:
docker build [選項(xiàng)] <上下文路徑/URL/->
在這里我們指定了最終鏡像的名稱(chēng)-t nginx:v3
颠悬,構(gòu)建成功后,我們可以像之前運(yùn)行nginx:v2
那樣來(lái)運(yùn)行這個(gè)鏡像定血,其結(jié)果會(huì)和nginx:v2
一樣。
鏡像構(gòu)建上下文(Context)
如果注意诞外,會(huì)看到docker build
命令最后有一個(gè).
澜沟。.
表示當(dāng)前目錄,而Dockerfile
就在當(dāng)前目錄峡谊,因此不少初學(xué)者以為這個(gè)路徑是在指定Dockerfile
所在路徑茫虽,這么理解其實(shí)是不準(zhǔn)確的。如果對(duì)應(yīng)上面的命令格式既们,你可能會(huì)發(fā)現(xiàn)濒析,這是在指定上下文路徑。那么什么是上下文呢啥纸?
首先我們要理解docker build
的工作原理号杏。Docker 在運(yùn)行時(shí)分為 Docker 引擎(也就是服務(wù)端守護(hù)進(jìn)程)和客戶(hù)端工具。Docker 的引擎提供了一組 REST API斯棒,被稱(chēng)為 Docker Remote API盾致,而如docker
命令這樣的客戶(hù)端工具,則是通過(guò)這組 API 與 Docker 引擎交互荣暮,從而完成各種功能庭惜。因此,雖然表面上我們好像是在本機(jī)執(zhí)行各種docker
功能穗酥,但實(shí)際上护赊,一切都是使用的遠(yuǎn)程調(diào)用形式在服務(wù)端(Docker 引擎)完成惠遏。也因?yàn)檫@種 C/S 設(shè)計(jì),讓我們操作遠(yuǎn)程服務(wù)器的 Docker 引擎變得輕而易舉骏啰。
當(dāng)我們進(jìn)行鏡像構(gòu)建的時(shí)候节吮,并非所有定制都會(huì)通過(guò)RUN
指令完成,經(jīng)常會(huì)需要將一些本地文件復(fù)制進(jìn)鏡像器一,比如通過(guò)COPY
指令课锌、ADD
指令等。而docker build
命令構(gòu)建鏡像祈秕,其實(shí)并非在本地構(gòu)建渺贤,而是在服務(wù)端,也就是 Docker 引擎中構(gòu)建的请毛。那么在這種客戶(hù)端/服務(wù)端的架構(gòu)中志鞍,如何才能讓服務(wù)端獲得本地文件呢?
這就引入了上下文的概念方仿。當(dāng)構(gòu)建的時(shí)候固棚,用戶(hù)會(huì)指定構(gòu)建鏡像上下文的路徑,docker build
命令得知這個(gè)路徑后仙蚜,會(huì)將路徑下的所有內(nèi)容打包此洲,然后上傳給 Docker 引擎。這樣 Docker 引擎收到這個(gè)上下文包后委粉,展開(kāi)就會(huì)獲得構(gòu)建鏡像所需的一切文件呜师。
如果在Dockerfile
中這么寫(xiě):
COPY ./package.json /app/
這并不是要復(fù)制執(zhí)行docker build
命令所在的目錄下的package.json
,也不是復(fù)制Dockerfile
所在目錄下的package.json
贾节,而是復(fù)制上下文(context)目錄下的package.json
汁汗。
因此,COPY
這類(lèi)指令中的源文件的路徑都是相對(duì)路徑栗涂。這也是初學(xué)者經(jīng)常會(huì)問(wèn)的為什么COPY ../package.json /app
或者COPY /opt/xxxx /app
無(wú)法工作的原因知牌,因?yàn)檫@些路徑已經(jīng)超出了上下文的范圍,Docker 引擎無(wú)法獲得這些位置的文件斤程。如果真的需要那些文件角寸,應(yīng)該將它們復(fù)制到上下文目錄中去。
現(xiàn)在就可以理解剛才的命令docker build -t nginx:v3 .
中的這個(gè).
暖释,實(shí)際上是在指定上下文的目錄袭厂,docker build
命令會(huì)將該目錄下的內(nèi)容打包交給 Docker 引擎以幫助構(gòu)建鏡像。
如果觀察docker build
輸出球匕,我們其實(shí)已經(jīng)看到了這個(gè)發(fā)送上下文的過(guò)程:
$ docker build -t nginx:v3 .
Sending build context to Docker daemon 2.048 kB
...
理解構(gòu)建上下文對(duì)于鏡像構(gòu)建是很重要的纹磺,避免犯一些不應(yīng)該的錯(cuò)誤。比如有些初學(xué)者在發(fā)現(xiàn)COPY /opt/xxxx /app
不工作后亮曹,于是干脆將Dockerfile
放到了硬盤(pán)根目錄去構(gòu)建橄杨,結(jié)果發(fā)現(xiàn)docker build
執(zhí)行后秘症,在發(fā)送一個(gè)幾十 GB 的東西,極為緩慢而且很容易構(gòu)建失敗式矫。那是因?yàn)檫@種做法是在讓docker build
打包整個(gè)硬盤(pán)乡摹,這顯然是使用錯(cuò)誤。
一般來(lái)說(shuō)采转,應(yīng)該會(huì)將Dockerfile
置于一個(gè)空目錄下聪廉,或者項(xiàng)目根目錄下。如果該目錄下沒(méi)有所需文件故慈,那么應(yīng)該把所需文件復(fù)制一份過(guò)來(lái)板熊。如果目錄下有些東西確實(shí)不希望構(gòu)建時(shí)傳給 Docker 引擎,那么可以用.gitignore
一樣的語(yǔ)法寫(xiě)一個(gè).dockerignore
察绷,該文件是用于剔除不需要作為上下文傳遞給 Docker 引擎的干签。
那么為什么會(huì)有人誤以為.
是指定Dockerfile
所在目錄呢?這是因?yàn)樵谀J(rèn)情況下拆撼,如果不額外指定Dockerfile
的話(huà)容劳,會(huì)將上下文目錄下的名為Dockerfile
的文件作為 Dockerfile。
這只是默認(rèn)行為闸度,實(shí)際上Dockerfile
的文件名并不要求必須為Dockerfile
竭贩,而且并不要求必須位于上下文目錄中,比如可以用-f ../Dockerfile.php
參數(shù)指定某個(gè)文件作為Dockerfile
莺禁。
當(dāng)然娶视,一般大家習(xí)慣性的會(huì)使用默認(rèn)的文件名Dockerfile
,以及會(huì)將其置于鏡像構(gòu)建上下文目錄中睁宰。
其它docker build
的用法
直接用 Git repo 進(jìn)行構(gòu)建
或許你已經(jīng)注意到了,docker build
還支持從 URL 構(gòu)建寝凌,比如可以直接從 Git repo 中構(gòu)建:
$ docker build https://github.com/twang2218/gitlab-ce-zh.git#:8.14
docker build https://github.com/twang2218/gitlab-ce-zh.git\#:8.14
Sending build context to Docker daemon 2.048 kB
Step 1 : FROM gitlab/gitlab-ce:8.14.0-ce.0
8.14.0-ce.0: Pulling from gitlab/gitlab-ce
aed15891ba52: Already exists
773ae8583d14: Already exists
...
這行命令指定了構(gòu)建所需的 Git repo柒傻,并且指定默認(rèn)的master
分支,構(gòu)建目錄為/8.14/
较木,然后 Docker 就會(huì)自己去git clone
這個(gè)項(xiàng)目红符、切換到指定分支、并進(jìn)入到指定目錄后開(kāi)始構(gòu)建伐债。
用給定的 tar 壓縮包構(gòu)建
$ docker build http://server/context.tar.gz
如果所給出的 URL 不是個(gè) Git repo预侯,而是個(gè)tar
壓縮包,那么 Docker 引擎會(huì)下載這個(gè)包峰锁,并自動(dòng)解壓縮萎馅,以其作為上下文,開(kāi)始構(gòu)建虹蒋。
從標(biāo)準(zhǔn)輸入中讀取 Dockerfile 進(jìn)行構(gòu)建
docker build - < Dockerfile
或
cat Dockerfile | docker build -
如果標(biāo)準(zhǔn)輸入傳入的是文本文件糜芳,則將其視為Dockerfile
飒货,并開(kāi)始構(gòu)建。這種形式由于直接從標(biāo)準(zhǔn)輸入中讀取 Dockerfile 的內(nèi)容更扁,它沒(méi)有上下文竟秫,因此不可以像其他方法那樣可以將本地文件COPY
進(jìn)鏡像之類(lèi)的事情民泵。
從標(biāo)準(zhǔn)輸入中讀取上下文壓縮包進(jìn)行構(gòu)建
$ docker build - < context.tar.gz
如果發(fā)現(xiàn)標(biāo)準(zhǔn)輸入的文件格式是?gzip
、bzip2
?以及?xz
?的話(huà)扣墩,將會(huì)使其為上下文壓縮包,直接將其展開(kāi)扛吞,將里面視為上下文呻惕,并開(kāi)始構(gòu)建。
Dockerfile 指令
我們已經(jīng)介紹了FROM
喻粹,RUN
蟆融,還提及了COPY
,ADD
,其實(shí) Dockerfile 功能很強(qiáng)大守呜,它提供了十多個(gè)指令型酥。下面我們繼續(xù)講解其他的指令。
COPY
格式:
COPY <源路徑>... <目標(biāo)路徑>
COPY ["<源路徑1>",... "<目標(biāo)路徑>"]
和RUN
指令一樣查乒,也有兩種格式弥喉,一種類(lèi)似于命令行,一種類(lèi)似于函數(shù)調(diào)用玛迄。
COPY
指令將從構(gòu)建上下文目錄中<源路徑>
的文件/目錄復(fù)制到新的一層的鏡像內(nèi)的<目標(biāo)路徑>
位置由境。比如:
COPY package.json /usr/src/app/
<源路徑>
可以是多個(gè),甚至可以是通配符蓖议,其通配符規(guī)則要滿(mǎn)足 Go 的 filepath.Match
規(guī)則虏杰,如:
COPY hom* /mydir/
COPY hom.txt /mydir/
<目標(biāo)路徑>
可以是容器內(nèi)的絕對(duì)路徑,也可以是相對(duì)于工作目錄的相對(duì)路徑(工作目錄可以用WORKDIR
指令來(lái)指定)勒虾。目標(biāo)路徑不需要事先創(chuàng)建纺阔,如果目錄不存在會(huì)在復(fù)制文件前先行創(chuàng)建缺失目錄。
此外修然,還需要注意一點(diǎn)笛钝,使用COPY
指令,源文件的各種元數(shù)據(jù)都會(huì)保留愕宋。比如讀玻靡、寫(xiě)、執(zhí)行權(quán)限中贝、文件變更時(shí)間等囤捻。這個(gè)特性對(duì)于鏡像定制很有用。特別是構(gòu)建相關(guān)文件都在使用 Git 進(jìn)行管理的時(shí)候雄妥。
ADD
ADD
指令和COPY
的格式和性質(zhì)基本一致最蕾。但是在COPY
基礎(chǔ)上增加了一些功能依溯。
比如<源路徑>
可以是一個(gè)URL
,這種情況下瘟则,Docker 引擎會(huì)試圖去下載這個(gè)鏈接的文件放到<目標(biāo)路徑>
去黎炉。下載后的文件權(quán)限自動(dòng)設(shè)置為600
,如果這并不是想要的權(quán)限醋拧,那么還需要增加額外的一層RUN
進(jìn)行權(quán)限調(diào)整慷嗜,另外,如果下載的是個(gè)壓縮包丹壕,需要解壓縮庆械,也一樣還需要額外的一層RUN
指令進(jìn)行解壓縮。所以不如直接使用RUN
指令菌赖,然后使用wget
或者curl
工具下載缭乘,處理權(quán)限、解壓縮琉用、然后清理無(wú)用文件更合理堕绩。因此,這個(gè)功能其實(shí)并不實(shí)用邑时,而且不推薦使用奴紧。
如果<源路徑>
為一個(gè)tar
壓縮文件的話(huà),壓縮格式為gzip
,bzip2
以及xz
的情況下晶丘,ADD
指令將會(huì)自動(dòng)解壓縮這個(gè)壓縮文件到<目標(biāo)路徑>
去黍氮。
在某些情況下,這個(gè)自動(dòng)解壓縮的功能非常有用浅浮,比如官方鏡像ubuntu
中:
FROM scratch
ADD ubuntu-xenial-core-cloudimg-amd64-root.tar.gz /
...
但在某些情況下沫浆,如果我們真的是希望復(fù)制個(gè)壓縮文件進(jìn)去,而不解壓縮滚秩,這時(shí)就不可以使用ADD
命令了件缸。
在 Docker 官方的Dockerfile 最佳實(shí)踐文檔
中要求,盡可能的使用COPY
叔遂,因?yàn)?code>COPY的語(yǔ)義很明確,就是復(fù)制文件而已争剿,而ADD
則包含了更復(fù)雜的功能已艰,其行為也不一定很清晰。最適合使用ADD
的場(chǎng)合蚕苇,就是所提及的需要自動(dòng)解壓縮的場(chǎng)合哩掺。
另外需要注意的是,ADD
指令會(huì)令鏡像構(gòu)建緩存失效涩笤,從而可能會(huì)令鏡像構(gòu)建變得比較緩慢嚼吞。
因此在COPY
和ADD
指令中選擇的時(shí)候盒件,可以遵循這樣的原則,所有的文件復(fù)制均使用COPY
指令舱禽,僅在需要自動(dòng)解壓縮的場(chǎng)合使用ADD
炒刁。
CMD
CMD
指令的格式和RUN
相似,也是兩種格式:
-
shell
格式:CMD <命令>
-
exec
格式:CMD ["可執(zhí)行文件", "參數(shù)1", "參數(shù)2"...]
- 參數(shù)列表格式:
CMD ["參數(shù)1", "參數(shù)2"...]
誊稚。在指定了ENTRYPOINT
指令后翔始,用CMD
指定具體的參數(shù)。
之前介紹容器的時(shí)候曾經(jīng)說(shuō)過(guò)里伯,Docker 不是虛擬機(jī)城瞎,容器就是進(jìn)程。既然是進(jìn)程疾瓮,那么在啟動(dòng)容器的時(shí)候脖镀,需要指定所運(yùn)行的程序及參數(shù)。CMD
指令就是用于指定默認(rèn)的容器主進(jìn)程的啟動(dòng)命令的狼电。
在運(yùn)行時(shí)可以指定新的命令來(lái)替代鏡像設(shè)置中的這個(gè)默認(rèn)命令蜒灰,比如,ubuntu
鏡像默認(rèn)的CMD
是/bin/bash
漫萄,如果我們直接docker run -it ubuntu
的話(huà)卷员,會(huì)直接進(jìn)入bash
。我們也可以在運(yùn)行時(shí)指定運(yùn)行別的命令腾务,如docker run -it ubuntu cat /etc/os-release
毕骡。這就是用cat /etc/os-release
命令替換了默認(rèn)的/bin/bash
命令了,輸出了系統(tǒng)版本信息岩瘦。
在指令格式上未巫,一般推薦使用exec
格式,這類(lèi)格式在解析時(shí)會(huì)被解析為 JSON 數(shù)組启昧,因此一定要使用雙引號(hào)"
叙凡,而不要使用單引號(hào)。
如果使用shell
格式的話(huà)密末,實(shí)際的命令會(huì)被包裝為sh -c
的參數(shù)的形式進(jìn)行執(zhí)行握爷。比如:
CMD echo $HOME
在實(shí)際執(zhí)行中,會(huì)將其變更為:
CMD [ "sh", "-c", "echo $HOME" ]
這就是為什么我們可以使用環(huán)境變量的原因严里,因?yàn)檫@些環(huán)境變量會(huì)被 shell 進(jìn)行解析處理新啼。
提到CMD
就不得不提容器中應(yīng)用在前臺(tái)執(zhí)行和后臺(tái)執(zhí)行的問(wèn)題。這是初學(xué)者常出現(xiàn)的一個(gè)混淆刹碾。
Docker 不是虛擬機(jī)燥撞,容器中的應(yīng)用都應(yīng)該以前臺(tái)執(zhí)行,而不是像虛擬機(jī)、物理機(jī)里面那樣物舒,用 upstart/systemd 去啟動(dòng)后臺(tái)服務(wù)色洞,容器內(nèi)沒(méi)有后臺(tái)服務(wù)
的概念。
一些初學(xué)者將CMD
寫(xiě)為:
CMD service nginx start
然后發(fā)現(xiàn)容器執(zhí)行后就立即退出了冠胯。甚至在容器內(nèi)去使用systemctl
命令結(jié)果卻發(fā)現(xiàn)根本執(zhí)行不了火诸。這就是因?yàn)闆](méi)有搞明白前臺(tái)、后臺(tái)的概念涵叮,沒(méi)有區(qū)分容器和虛擬機(jī)的差異惭蹂,依舊在以傳統(tǒng)虛擬機(jī)的角度去理解容器。
對(duì)于容器而言割粮,其啟動(dòng)程序就是容器應(yīng)用進(jìn)程盾碗,容器就是為了主進(jìn)程而存在的,主進(jìn)程退出舀瓢,容器就失去了存在的意義廷雅,從而退出,其它輔助進(jìn)程不是它需要關(guān)心的東西京髓。
而使用service nginx start
命令航缀,則是希望 upstart 來(lái)以后臺(tái)守護(hù)進(jìn)程形式啟動(dòng)nginx
服務(wù)。而剛才說(shuō)了CMD service nginx start
會(huì)被理解為CMD [ "sh", "-c", "service nginx start"]
堰怨,因此主進(jìn)程實(shí)際上是sh
芥玉。那么當(dāng)service nginx start
命令結(jié)束后,sh
也就結(jié)束了备图,sh
作為主進(jìn)程退出了灿巧,自然就會(huì)令容器退出。
正確的做法是直接執(zhí)行nginx
可執(zhí)行文件揽涮,并且要求以前臺(tái)形式運(yùn)行抠藕。比如:
CMD ["nginx", "-g", "daemon off;"]
ENTRYPOINT
ENTRYPOINT
的格式和RUN
指令格式一樣,分為exec
格式和shell
格式蒋困。
ENTRYPOINT
的目的和CMD
一樣盾似,都是在指定容器啟動(dòng)程序及參數(shù)。ENTRYPOINT
在運(yùn)行時(shí)也可以替代雪标,不過(guò)比CMD
要略顯繁瑣零院,需要通過(guò)docker run
的參數(shù)--entrypoint
來(lái)指定。
當(dāng)指定了ENTRYPOINT
后村刨,CMD
的含義就發(fā)生了改變门粪,不再是直接的運(yùn)行其命令,而是將CMD
的內(nèi)容作為參數(shù)傳給ENTRYPOINT
指令烹困,換句話(huà)說(shuō)實(shí)際執(zhí)行時(shí),將變?yōu)椋?/p>
<ENTRYPOINT> "<CMD>"
那么有了CMD
后乾吻,為什么還要有ENTRYPOINT
呢髓梅?這種<ENTRYPOINT> "<CMD>"
有什么好處么拟蜻?讓我們來(lái)看幾個(gè)場(chǎng)景。
場(chǎng)景一:讓鏡像變成像命令一樣使用
假設(shè)我們需要一個(gè)得知自己當(dāng)前公網(wǎng) IP 的鏡像枯饿,那么可以先用CMD
來(lái)實(shí)現(xiàn):
FROM ubuntu:16.04
RUN apt-get update \
&& apt-get install -y curl \
&& rm -rf /var/lib/apt/lists/*
CMD [ "curl", "-s", "https://ip.cn" ]
假如我們使用docker build -t myip .
來(lái)構(gòu)建鏡像的話(huà)酝锅,如果我們需要查詢(xún)當(dāng)前公網(wǎng) IP,只需要執(zhí)行:
$ docker run myip
當(dāng)前 IP:61.148.226.66 來(lái)自:北京市 聯(lián)通
嗯奢方,這么看起來(lái)好像可以直接把鏡像當(dāng)做命令使用了搔扁,不過(guò)命令總有參數(shù),如果我們希望加參數(shù)呢蟋字?比如從上面的CMD
中可以看到實(shí)質(zhì)的命令是curl
稿蹲,那么如果我們希望顯示 HTTP 頭信息,就需要加上-i
參數(shù)鹊奖。那么我們可以直接加-i
參數(shù)給docker run myip
么苛聘?
$ docker run myip -i
docker: Error response from daemon: invalid header field value "oci runtime error: container_linux.go:247: starting container process caused \"exec: \\\"-i\\\": executable file not found in $PATH\"\n".
我們可以看到可執(zhí)行文件找不到的報(bào)錯(cuò),executable file not found
忠聚。之前我們說(shuō)過(guò)设哗,跟在鏡像名后面的是command
,運(yùn)行時(shí)會(huì)替換CMD
的默認(rèn)值两蟀。因此這里的-i
替換了原來(lái)的CMD
网梢,而不是添加在原來(lái)的curl -s http://ip.cn
后面。而-i
根本不是命令赂毯,所以自然找不到战虏。
那么如果我們希望加入-i
這參數(shù),我們就必須重新完整的輸入這個(gè)命令:
$ docker run myip curl -s http://ip.cn -i
這顯然不是很好的解決方案欢瞪,而使用ENTRYPOINT
就可以解決這個(gè)問(wèn)題』罾樱現(xiàn)在我們重新用ENTRYPOINT
來(lái)實(shí)現(xiàn)這個(gè)鏡像:
FROM ubuntu:16.04
RUN apt-get update \
&& apt-get install -y curl \
&& rm -rf /var/lib/apt/lists/*
ENTRYPOINT [ "curl", "-s", "http://ip.cn" ]
這次我們?cè)賮?lái)嘗試直接使用docker run myip -i
:
$ docker run myip
當(dāng)前 IP:61.148.226.66 來(lái)自:北京市 聯(lián)通
$ docker run myip -i
HTTP/1.1 200 OK
Server: nginx/1.8.0
Date: Tue, 22 Nov 2016 05:12:40 GMT
Content-Type: text/html; charset=UTF-8
Vary: Accept-Encoding
X-Powered-By: PHP/5.6.24-1~dotdeb+7.1
X-Cache: MISS from cache-2
X-Cache-Lookup: MISS from cache-2:80
X-Cache: MISS from proxy-2_6
Transfer-Encoding: chunked
Via: 1.1 cache-2:80, 1.1 proxy-2_6:8006
Connection: keep-alive
當(dāng)前 IP:61.148.226.66 來(lái)自:北京市 聯(lián)通
可以看到,這次成功了遣鼓。這是因?yàn)楫?dāng)存在ENTRYPOINT
后啸盏,CMD
的內(nèi)容將會(huì)作為參數(shù)傳給ENTRYPOINT
,而這里-i
就是新的CMD
骑祟,因此會(huì)作為參數(shù)傳給curl
回懦,從而達(dá)到了我們預(yù)期的效果。
場(chǎng)景二:應(yīng)用運(yùn)行前的準(zhǔn)備工作
啟動(dòng)容器就是啟動(dòng)主進(jìn)程次企,但有些時(shí)候怯晕,啟動(dòng)主進(jìn)程前,需要一些準(zhǔn)備工作缸棵。
比如mysql
類(lèi)的數(shù)據(jù)庫(kù)舟茶,可能需要一些數(shù)據(jù)庫(kù)配置、初始化的工作,這些工作要在最終的 mysql 服務(wù)器運(yùn)行之前解決吧凉。
此外隧出,可能希望避免使用root
用戶(hù)去啟動(dòng)服務(wù),從而提高安全性阀捅,而在啟動(dòng)服務(wù)前還需要以root
身份執(zhí)行一些必要的準(zhǔn)備工作胀瞪,最后切換到服務(wù)用戶(hù)身份啟動(dòng)服務(wù)∷潜桑或者除了服務(wù)外凄诞,其它命令依舊可以使用root
身份執(zhí)行,方便調(diào)試等忍级。
這些準(zhǔn)備工作是和容器CMD
無(wú)關(guān)的帆谍,無(wú)論CMD
為什么,都需要事先進(jìn)行一個(gè)預(yù)處理的工作颤练。這種情況下既忆,可以寫(xiě)一個(gè)腳本,然后放入ENTRYPOINT
中去執(zhí)行嗦玖,而這個(gè)腳本會(huì)將接到的參數(shù)(也就是<CMD>
)作為命令患雇,在腳本最后執(zhí)行。比如官方鏡像redis
中就是這么做的:
FROM alpine:3.4
...
RUN addgroup -S redis && adduser -S -G redis redis
...
ENTRYPOINT ["docker-entrypoint.sh"]
EXPOSE 6379
CMD [ "redis-server" ]
可以看到其中為了 redis 服務(wù)創(chuàng)建了 redis 用戶(hù)宇挫,并在最后指定了ENTRYPOINT
為docker-entrypoint.sh
腳本苛吱。
#!/bin/sh
...
# allow the container to be started with `--user`
if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then
chown -R redis .
exec su-exec redis "$0" "$@"
fi
exec "$@"
該腳本的內(nèi)容就是根據(jù)CMD
的內(nèi)容來(lái)判斷,如果是redis-server
的話(huà)器瘪,則切換到redis
用戶(hù)身份啟動(dòng)服務(wù)器翠储,否則依舊使用root
身份執(zhí)行。比如:
$ docker run -it redis id
uid=0(root) gid=0(root) groups=0(root)
ENV
格式有兩種:
ENV <key> <value>
ENV <key1>=<value1> <key2>=<value2>...
這個(gè)指令很簡(jiǎn)單橡疼,就是設(shè)置環(huán)境變量而已援所,無(wú)論是后面的其它指令,如RUN
欣除,還是運(yùn)行時(shí)的應(yīng)用住拭,都可以直接使用這里定義的環(huán)境變量。
ENV VERSION=1.0 DEBUG=on \
NAME="Happy Feet"
這個(gè)例子中演示了如何換行历帚,以及對(duì)含有空格的值用雙引號(hào)括起來(lái)的辦法滔岳,這和 Shell 下的行為是一致的。
定義了環(huán)境變量挽牢,那么在后續(xù)的指令中谱煤,就可以使用這個(gè)環(huán)境變量。比如在官方node
鏡像Dockerfile
中禽拔,就有類(lèi)似這樣的代碼:
ENV NODE_VERSION 7.2.0
RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \
&& curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \
&& gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \
&& grep " node-v$NODE_VERSION-linux-x64.tar.xz\$" SHASUMS256.txt | sha256sum -c - \
&& tar -xJf "node-v$NODE_VERSION-linux-x64.tar.xz" -C /usr/local --strip-components=1 \
&& rm "node-v$NODE_VERSION-linux-x64.tar.xz" SHASUMS256.txt.asc SHASUMS256.txt \
&& ln -s /usr/local/bin/node /usr/local/bin/nodejs
在這里先定義了環(huán)境變量NODE_VERSION
刘离,其后的RUN
這層里室叉,多次使用$NODE_VERSION
來(lái)進(jìn)行操作定制×蛱瑁可以看到太惠,將來(lái)升級(jí)鏡像構(gòu)建版本的時(shí)候,只需要更新7.2.0
即可疲憋,Dockerfile
構(gòu)建維護(hù)變得更輕松了。
下列指令可以支持環(huán)境變量展開(kāi):ADD
梁只、COPY
缚柳、ENV
、EXPOSE
搪锣、LABEL
秋忙、USER
、WORKDIR
构舟、VOLUME
灰追、STOPSIGNAL
、ONBUILD
狗超。
可以從這個(gè)指令列表里感覺(jué)到弹澎,環(huán)境變量可以使用的地方很多,很強(qiáng)大努咐。通過(guò)環(huán)境變量苦蒿,我們可以讓一份Dockerfile
制作更多的鏡像,只需使用不同的環(huán)境變量即可渗稍。
VOLUME
格式為:
VOLUME ["<路徑1>", "<路徑2>"...]
VOLUME <路徑>
之前我們說(shuō)過(guò)佩迟,容器運(yùn)行時(shí)應(yīng)該盡量保持容器存儲(chǔ)層不發(fā)生寫(xiě)操作,對(duì)于數(shù)據(jù)庫(kù)類(lèi)需要保存動(dòng)態(tài)數(shù)據(jù)的應(yīng)用竿屹,其數(shù)據(jù)庫(kù)文件應(yīng)該保存于卷(volume)中报强,后面的章節(jié)我們會(huì)進(jìn)一步介紹 Docker 卷的概念。為了防止運(yùn)行時(shí)用戶(hù)忘記將動(dòng)態(tài)文件所保存目錄掛載為卷拱燃,在Dockerfile
中秉溉,我們可以事先指定某些目錄掛載為匿名卷,這樣在運(yùn)行時(shí)如果用戶(hù)不指定掛載扼雏,其應(yīng)用也可以正常運(yùn)行坚嗜,不會(huì)向容器存儲(chǔ)層寫(xiě)入大量數(shù)據(jù)。
VOLUME /data
這里的/data
目錄就會(huì)在運(yùn)行時(shí)自動(dòng)掛載為匿名卷诗充,任何向/data
中寫(xiě)入的信息都不會(huì)記錄進(jìn)容器存儲(chǔ)層苍蔬,從而保證了容器存儲(chǔ)層的無(wú)狀態(tài)化。當(dāng)然蝴蜓,運(yùn)行時(shí)可以覆蓋這個(gè)掛載設(shè)置碟绑。比如:
docker run -d -v mydata:/data xxxx
在這行命令中俺猿,就使用了mydata
這個(gè)命名卷掛載到了/data
這個(gè)位置,替代了Dockerfile
中定義的匿名卷的掛載配置格仲。
EXPOSE
格式為EXPOSE <端口1> [<端口2>...]
押袍。
EXPOSE
指令是聲明運(yùn)行時(shí)容器提供服務(wù)端口,這只是一個(gè)聲明凯肋,在運(yùn)行時(shí)并不會(huì)因?yàn)檫@個(gè)聲明應(yīng)用就會(huì)開(kāi)啟這個(gè)端口的服務(wù)谊惭。在 Dockerfile 中寫(xiě)入這樣的聲明有兩個(gè)好處,一個(gè)是幫助鏡像使用者理解這個(gè)鏡像服務(wù)的守護(hù)端口侮东,以方便配置映射圈盔;另一個(gè)用處則是在運(yùn)行時(shí)使用隨機(jī)端口映射時(shí),也就是docker run -P
時(shí)悄雅,會(huì)自動(dòng)隨機(jī)映射EXPOSE
的端口驱敲。
此外,在早期 Docker 版本中還有一個(gè)特殊的用處宽闲。以前所有容器都運(yùn)行于默認(rèn)橋接網(wǎng)絡(luò)中众眨,因此所有容器互相之間都可以直接訪(fǎng)問(wèn),這樣存在一定的安全性問(wèn)題容诬。于是有了一個(gè) Docker 引擎參數(shù)--icc=false
娩梨,當(dāng)指定該參數(shù)后,容器間將默認(rèn)無(wú)法互訪(fǎng)放案,除非互相間使用了--links
參數(shù)的容器才可以互通姚建,并且只有鏡像中EXPOSE
所聲明的端口才可以被訪(fǎng)問(wèn)限书。這個(gè)--icc=false
的用法僵刮,在引入了docker network
后已經(jīng)基本不用了,通過(guò)自定義網(wǎng)絡(luò)可以很輕松的實(shí)現(xiàn)容器間的互聯(lián)與隔離食呻。
要將EXPOSE
和在運(yùn)行時(shí)使用-p <宿主端口>:<容器端口>
區(qū)分開(kāi)來(lái)友雳。-p
稿湿,是映射宿主端口和容器端口,換句話(huà)說(shuō)押赊,就是將容器的對(duì)應(yīng)端口服務(wù)公開(kāi)給外界訪(fǎng)問(wèn)饺藤,而EXPOSE
僅僅是聲明容器打算使用什么端口而已,并不會(huì)自動(dòng)在宿主進(jìn)行端口映射流礁。
WORKDIR
格式為WORKDIR <工作目錄路徑>
涕俗。
使用WORKDIR
指令可以來(lái)指定工作目錄(或者稱(chēng)為當(dāng)前目錄),以后各層的當(dāng)前目錄就被改為指定的目錄神帅,如該目錄不存在再姑,WORKDIR
會(huì)幫你建立目錄。
之前提到一些初學(xué)者常犯的錯(cuò)誤是把Dockerfile
等同于 Shell 腳本來(lái)書(shū)寫(xiě)找御,這種錯(cuò)誤的理解還可能會(huì)導(dǎo)致出現(xiàn)下面這樣的錯(cuò)誤:
RUN cd /app
RUN echo "hello" > world.txt
如果將這個(gè)Dockerfile
進(jìn)行構(gòu)建鏡像運(yùn)行后元镀,會(huì)發(fā)現(xiàn)找不到/app/world.txt
文件绍填,或者其內(nèi)容不是hello
。原因其實(shí)很簡(jiǎn)單栖疑,在 Shell 中讨永,連續(xù)兩行是同一個(gè)進(jìn)程執(zhí)行環(huán)境,因此前一個(gè)命令修改的內(nèi)存狀態(tài)遇革,會(huì)直接影響后一個(gè)命令卿闹;而在Dockerfile
中,這兩行RUN
命令的執(zhí)行環(huán)境根本不同萝快,是兩個(gè)完全不同的容器比原。這就是對(duì)Dockerfile
構(gòu)建分層存儲(chǔ)的概念不了解所導(dǎo)致的錯(cuò)誤。
之前說(shuō)過(guò)每一個(gè)RUN
都是啟動(dòng)一個(gè)容器杠巡、執(zhí)行命令、然后提交存儲(chǔ)層文件變更雇寇。第一層RUN cd /app
的執(zhí)行僅僅是當(dāng)前進(jìn)程的工作目錄變更氢拥,一個(gè)內(nèi)存上的變化而已,其結(jié)果不會(huì)造成任何文件變更锨侯。而到第二層的時(shí)候嫩海,啟動(dòng)的是一個(gè)全新的容器,跟第一層的容器更完全沒(méi)關(guān)系囚痴,自然不可能繼承前一層構(gòu)建過(guò)程中的內(nèi)存變化叁怪。
因此如果需要改變以后各層的工作目錄的位置,那么應(yīng)該使用WORKDIR
指令深滚。