最近做了一個(gè)好玩的工具,叫xbin.io[1]。其中有一項(xiàng)工作是為不同的工具來構(gòu)建 Docker 鏡像,讓他們都運(yùn)行在 Docker 中(實(shí)際上,是兼容 Docker image 的其他 sandbox 系統(tǒng),沒有直接用 Docker)。支持的工具越來越多,為了節(jié)省資源,Build 的 Docker image 就越小越好,文件越少,其實(shí)啟動(dòng)速度也會(huì)略微快一些,也會(huì)更安全一些。
這篇文章來介紹一下做 Docker Image 的一些技巧。
在之前的博客Docker (容器) 的原理[2]中介紹過 Docker image 是如何工作的。簡單來說,就是使用 Linux 的overlayfs[3], overlay file system 可以做到,將兩個(gè) file system merge 在一起,下層的文件系統(tǒng)只讀,上層的文件系統(tǒng)可寫。如果你讀,找到上層就讀上層的,否則的話就找到下層的給你讀。然后寫的話會(huì)寫入到上層。這樣,其實(shí)對(duì)于最終用戶來說,可以認(rèn)為只有一個(gè) merge 之后的文件系統(tǒng),用起來和普通文件系統(tǒng)沒有什么區(qū)別。
有了這個(gè)功能,Docker 運(yùn)行的時(shí)候,從最下層的文件系統(tǒng)開始,merge 兩層,得到新的 fs 然后再 merge 上一層,然后再 merge 最上一層,最后得到最終的 directory,然后用chroot[4]改變進(jìn)程的 root 目錄,啟動(dòng) container。
了解了原理之后,你會(huì)發(fā)現(xiàn),這種設(shè)計(jì)對(duì)于 Docker 來說非常合適:
- 如果 2 個(gè) image 都是基于 Ubuntu,那么兩個(gè) Image 可以共用 Ubuntu 的 base image,只需要存儲(chǔ)一份;
- 如果 pull 新的 image,某一層如果已經(jīng)存在,那么這一層之前的內(nèi)容其實(shí)就不需要 pull 了;
后面 build image 的技巧其實(shí)都是基于這兩點(diǎn)。
另外稍微提一下,Docker image其實(shí)就是一個(gè) tar 包[5]。一般來說我們通過Dockerfile
用docker built
命令來構(gòu)建,但是其實(shí)也可以用其他工具構(gòu)建,只要構(gòu)建出來的image 符合 Docker 的規(guī)范[6],就可以運(yùn)行。比如,之前的博文Build 一個(gè)最小的 Redis Docker Image[7]就是用 Nix 構(gòu)建出來的。
技巧1:刪除緩存
一般的包管理器,比如apt
,pip
等,下載包的時(shí)候,都會(huì)下載緩存,下次安裝同一個(gè)包的時(shí)候不必從網(wǎng)絡(luò)上下載,直接使用緩存即可。
但是在 Docker Image 中,我們是不需要這些緩存的。所以我們?cè)?code>Dockerfile中下載東西一般會(huì)使用這種命令:
RUNdnfinstall-y--setopt=tsflags=nodocs
httpdvim&&
systemctlenablehttpd&&
dnfcleanall
在包安裝好之后,去刪除緩存。
一個(gè)常見的錯(cuò)誤是,有人會(huì)這么寫:
FROMfedora
RUNdnfinstall-ymariadb
RUNdnfinstall-ywordpress
RUNdnfcleanall
Dockerfile 里面的每一個(gè)RUN
都會(huì)創(chuàng)建一層新的 layer,如上所說,這樣其實(shí)是創(chuàng)建了 3 層 layer,前 2 層帶來了緩存,第三層刪除了緩存。如同 git 一樣,你在一個(gè)新的 commit 里面刪除了之前的文件,其實(shí)文件還是在 git 歷史中的,最終的 docker image 其實(shí)沒有減少。
但是 Docker 有了一個(gè)新的功能,docker build --squash
。squash 功能會(huì)在 Docker 完成構(gòu)建之后,將所有的 layers 壓縮成一個(gè) layer,也就是說,最終構(gòu)建出來的 Docker image 只有一層。所以,如上在多個(gè)RUN
中寫 clean 命令,其實(shí)也可以。我不太喜歡這種方式,因?yàn)榍拔奶岬降?,多個(gè) image 共享 base image 以及加速 pull 的 feature 其實(shí)就用不到了。
一些常見的包管理器刪除緩存的方法:
yum | yum clean all |
---|---|
dnf | dnf clean all |
rvm | rvm cleanup all |
gem | gem cleanup |
cpan | rm -rf ~/.cpan/{build,sources}/* |
pip | rm -rf ~/.cache/pip/* |
apt-get | apt-get clean |
另外,上面這個(gè)命令其實(shí)還有一個(gè)缺點(diǎn)。因?yàn)槲覀冊(cè)谕粋€(gè)RUN
中寫多行,不容易看出這個(gè)dnf
到底安裝了什么。而且,第一行和最后一行不一樣,如果修改,diff 看到的會(huì)是兩行內(nèi)容,很不友好,容易出錯(cuò)。
可以寫成這種形式,比較清晰。
RUNtrue
&&dnfinstall-y--setopt=tsflags=nodocs
httpdvim
&&systemctlenablehttpd
&&dnfcleanall
&&true
技巧2:改動(dòng)不頻繁的內(nèi)容往前放
通過前文介紹過的原理,可以知道,對(duì)于一個(gè) Docker image 有 ABCD 四層,B 修改了,那么 BCD 會(huì)改變。
根據(jù)這個(gè)原理,我們?cè)跇?gòu)建的時(shí)候可以將系統(tǒng)依賴往前寫,因?yàn)橄?code>apt,dnf
這些安裝的東西,是很少修改的。然后寫應(yīng)用的庫依賴,比如pip install
,最后 copy 應(yīng)用。
比如下面這個(gè) Dockerfile,就會(huì)在每次代碼改變的時(shí)候都重新 Build 大部分 layers,即使只改了一個(gè)網(wǎng)頁的標(biāo)題。
FROMpython:3.7-buster
#copysource
RUNmkdir-p/opt/app
COPYmyapp/opt/app/myapp/
WORKDIR/opt/app
#installdependenciesnginx
RUNapt-getupdate&&apt-getinstallnginx
RUNpipinstall-rrequirements.txt
RUNchown-Rwww-data:www-data/opt/app
#startserver
EXPOSE8020
STOPSIGNALSIGTERM
CMD["/opt/app/start-server.sh"]
我們可以改成,先安裝 Nginx,再單獨(dú) copyrequirements.txt
,然后安裝pip
依賴,最后 copy 應(yīng)用代碼。
FROMpython:3.7-buster
#installdependenciesnginx
RUNapt-getupdate&&apt-getinstallnginx
COPYmyapp/requirements.txt/opt/app/myapp/requirements.txt
RUNpipinstall-rrequirements.txt
#copysource
RUNmkdir-p/opt/app
COPYmyapp/opt/app/myapp/
WORKDIR/opt/app
RUNchown-Rwww-data:www-data/opt/app
#startserver
EXPOSE8020
STOPSIGNALSIGTERM
CMD["/opt/app/start-server.sh"]
技巧3:構(gòu)建和運(yùn)行 Image 分離
我們?cè)诰幾g應(yīng)用的時(shí)候需要很多構(gòu)建工具,比如 gcc, golang 等。但是在運(yùn)行的時(shí)候不需要。在構(gòu)建完成之后,去刪除那些構(gòu)建工具是很麻煩的。
我們可以這樣:使用一個(gè) Docker 作為 builder,安裝所有的構(gòu)建依賴,進(jìn)行構(gòu)建,構(gòu)建完成后,重新選擇一個(gè) Base image,然后將構(gòu)建的產(chǎn)物復(fù)制到新的 base image,這樣,最終的 image 只含有運(yùn)行需要的東西。
比如,這是安裝一個(gè) golang 應(yīng)用pup
的代碼:
FROMgolangasbuild
ENVCGO_ENABLED0
RUNgoinstallgithub.com/ericchiang/pup@latest
FROMalpine:3.15.4asrun
COPY--from=build/go/bin/pup/usr/local/bin/pup
我們使用golang
這個(gè) 1G 多大的 image 來安裝,安裝完成之后將 binary 復(fù)制到 alpine, 最終的產(chǎn)物只有 10M 左右。這種方法特別適合一些靜態(tài)編譯的編程語言,比如 golang 和 rust.
技巧4:檢查構(gòu)建產(chǎn)物
這是最有用的一個(gè)技巧了。
dive 是一個(gè) TUI,命令行的交互式 App,它可以讓你看到 docker 每一層里面都有什么。
dive ubuntu:latest
命令可以看到 ubuntu image 里面都有什么文件。內(nèi)容會(huì)顯示為兩側(cè),左邊顯示每一層的信息,右邊顯示當(dāng)前層(會(huì)包含之前的所有層)的文件內(nèi)容,本層新添加的文件會(huì)用黃色來顯示。通過tab
鍵可以切換左右的操作。
一個(gè)非常有用的功能是,按下ctrl
+U
可以只顯示當(dāng)前層相比于前一層增加的內(nèi)容,這樣,就可以看到增加的文件是否是預(yù)期的了。
按ctrl
+Space
可以折疊起來所有的目錄,然后交互式地打開他們查看,就像是 Docker 中的ncdu
。
審核編輯 :李倩
-
進(jìn)程
+關(guān)注
關(guān)注
0文章
204瀏覽量
13979 -
鏡像
+關(guān)注
關(guān)注
0文章
170瀏覽量
10786 -
Docker
+關(guān)注
關(guān)注
0文章
492瀏覽量
11927
原文標(biāo)題:4 個(gè)超實(shí)用的 Docker 鏡像構(gòu)建技巧
文章出處:【微信號(hào):良許Linux,微信公眾號(hào):良許Linux】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論