隨著應(yīng)用的容器化、上云后,將伴隨著 Docker 鏡像的構(gòu)建,構(gòu)建 Docker 鏡像成為了最基本的一步,其中 Dockerfile 便是用來(lái)構(gòu)建鏡像的一種文本文件,鏡像的優(yōu)劣全靠 Dockerfile 編寫(xiě)的是否合理、合規(guī)。本文將講述編寫(xiě) Dockerfile 的一些最佳實(shí)踐和技巧,讓我們的鏡像更小、更優(yōu)。
1、Docker 鏡像是如何工作的
首先,我們一起回顧下 Docker 鏡像的相關(guān)概念及工作流程吧。
1.1 鏡像
鏡像(image)是一堆只讀層(read-only layer)的統(tǒng)一視角,也許這個(gè)定義有些難以理解,下面的這張圖能夠幫助您理解鏡像的定義。
從左邊我們看到了多個(gè)只讀層,它們重疊在一起。除了最下面一層,其它層都會(huì)有一個(gè)指針指向下一層。這些層是 Docker 內(nèi)部的實(shí)現(xiàn)細(xì)節(jié),并且能夠在主機(jī)的文件系統(tǒng)上訪問(wèn)到。統(tǒng)一文件系統(tǒng)技術(shù)能夠?qū)⒉煌膶诱铣梢粋€(gè)文件系統(tǒng),為這些層提供了一個(gè)統(tǒng)一的視角,這樣就隱藏了多層的存在,在用戶的角度看來(lái),只存在一個(gè)文件系統(tǒng)。我們可以在圖片的右邊看到這個(gè)視角的形式。
您可以在您的主機(jī)文件系統(tǒng)上找到有關(guān)這些層的文件。需要注意的是,在一個(gè)運(yùn)行中的容器內(nèi)部,這些層是不可見(jiàn)的。在我的主機(jī)上,我發(fā)現(xiàn)它們存在于 /var/lib/docker/overlay2
目錄下。
1.2 鏡像分層結(jié)構(gòu)
為什么說(shuō)是鏡像分層結(jié)構(gòu),因?yàn)?Docker 鏡像是以層來(lái)組織的,可以通過(guò)命令 docker image inspect
或者 docker inspect
來(lái)查看鏡像包含哪些層。
例如,鏡像 busybox :
xcbeyond@xcbeyonddeMacBook-Pro ~ % docker inspect busybox
[
{
"Id": "sha256:3c277069c6ae3f3572998e727b973ff7418c3962b9403de4b3a3f8624399b8fa",
"RepoTags": [
"busybox:latest"
],
"RepoDigests": [
"busybox@sha256:d2b53584f580310186df7a2055ce3ff83cc0df6caacf1e3489bff8cf5d0af5d8"
],
"Parent": "",
"Comment": "",
"Created": "2022-04-14T00:39:25.923517152Z",
"Container": "39aaf4eecc48824531078c316f5b16e97549417e07c8f90b26ae16053111ea57",
"ContainerConfig": {
"Hostname": "39aaf4eecc48",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"/bin/sh",
"-c",
"#(nop) ",
"CMD [\"sh\"]"
],
"Image": "sha256:3289bc85dc0eba79657979661460c7f6f97688ad8a4f93174e0cabdd6b09a365",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": null,
"OnBuild": null,
"Labels": {}
},
"DockerVersion": "20.10.12",
"Author": "",
"Config": {
"Hostname": "",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"sh"
],
"Image": "sha256:3289bc85dc0eba79657979661460c7f6f97688ad8a4f93174e0cabdd6b09a365",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": null,
"OnBuild": null,
"Labels": null
},
"Architecture": "arm64",
"Variant": "v8",
"Os": "linux",
"Size": 1411540,
"VirtualSize": 1411540,
"GraphDriver": {
"Data": {
"MergedDir": "/var/lib/docker/overlay2/e89181e7cadd3a6ee49f66bae34fed369621a1a5cfbe0003ce4621d0eec020e6/merged",
"UpperDir": "/var/lib/docker/overlay2/e89181e7cadd3a6ee49f66bae34fed369621a1a5cfbe0003ce4621d0eec020e6/diff",
"WorkDir": "/var/lib/docker/overlay2/e89181e7cadd3a6ee49f66bae34fed369621a1a5cfbe0003ce4621d0eec020e6/work"
},
"Name": "overlay2"
},
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:31a5597e16d3c5adaaf5826162216e256126d2fbf1beaa2b6c45c1822a2b9ca3"
]
},
"Metadata": {
"LastTagTime": "0001-01-01T00:00:00Z"
}
}
]
其中,RootFS 就是鏡像 busybox:latest 的鏡像層,只有一層,這層數(shù)據(jù)是存儲(chǔ)在宿主機(jī)哪里的呢?動(dòng)手實(shí)踐的同學(xué)會(huì)在上面的輸出中看到一個(gè)叫做 GraphDriver 的字段內(nèi)容如下:
"GraphDriver": {
"Data": {
"MergedDir": "/var/lib/docker/overlay2/e89181e7cadd3a6ee49f66bae34fed369621a1a5cfbe0003ce4621d0eec020e6/merged",
"UpperDir": "/var/lib/docker/overlay2/e89181e7cadd3a6ee49f66bae34fed369621a1a5cfbe0003ce4621d0eec020e6/diff",
"WorkDir": "/var/lib/docker/overlay2/e89181e7cadd3a6ee49f66bae34fed369621a1a5cfbe0003ce4621d0eec020e6/work"
},
"Name": "overlay2"
}
GraphDriver 負(fù)責(zé)鏡像本地的管理和存儲(chǔ)以及運(yùn)行中的容器生成鏡像等工作,可以將 GraphDriver 理解成鏡像管理引擎,我們這里的例子對(duì)應(yīng)的引擎名字是 overlay2(overlay 的優(yōu)化版本)。除了 overlay 之外,Docker 的 GraphDriver 還支持 btrfs、aufs、devicemapper、vfs 等。
我們可以看到其中的 Data 包含了多個(gè)部分,這個(gè)對(duì)應(yīng) OverlayFS 的鏡像組織形式,雖然我們上面的例子中的 busybox 鏡像只有一層,但是正常情況下很多鏡像都是由多層組成的。
1.3 Dockerfile、鏡像、容器間的關(guān)系
Dockerfile 是軟件的原材料,Docker 鏡像是軟件的交付品,而 Docker 容器則可以認(rèn)為是軟件的運(yùn)行態(tài)。從應(yīng)用軟件的角度來(lái)看,Dockerfile、Docker 鏡像與 Docker 容器分別代表軟件的三個(gè)不同階段,Dockerfile 面向開(kāi)發(fā),Docker 鏡像成為交付標(biāo)準(zhǔn),Docker 容器則涉及部署與運(yùn)維,三者缺一不可,合力充當(dāng) Docker 體系的基石。
簡(jiǎn)單來(lái)講,Dockerfile 構(gòu)建出 Docker鏡像,通過(guò) Docker 鏡像運(yùn)行Docker容器。
我們可以從 Docker 容器的角度,來(lái)反推三者的關(guān)系,如下圖:
2、Dockerfile
Dockerfile 是一個(gè)用來(lái)構(gòu)建鏡像的文本文件,文本內(nèi)容包含了一條條構(gòu)建鏡像所需的指令和說(shuō)明,它是構(gòu)建鏡像的關(guān)鍵。
一個(gè) Docker 鏡像包含了很多只讀層,每一層都由一個(gè) Dockerfile 指令構(gòu)成,這些層堆疊在一起,每一層都是前一層變化的增量。例如:
FROM ubuntu:18.04COPY . /appRUN make /appCMD python /app/app.py
每條指令都會(huì)創(chuàng)建一層:
- FROM:從 ubuntu:18.04 Docker 鏡像創(chuàng)建了一層,也作為基礎(chǔ)鏡像層。
- COPY:從 Docker 客戶端的當(dāng)前目錄添加文件。
- RUN:執(zhí)行 make 命令.
- CMD:指定要在容器中運(yùn)行的命令。
上述就是一個(gè)簡(jiǎn)單的 Dockerfile 文件,再通過(guò) docker build -t
命令便可直接構(gòu)建出鏡像。
在這里就不過(guò)多介紹 Dockerfile 的各個(gè)指令的用法,更多更詳細(xì)的可參考:Dockerfile reference
3、Dockerfile 的最佳實(shí)踐
本節(jié)將列舉出一些最佳實(shí)踐技巧,來(lái)幫助我們更好的寫(xiě)好 Dockerfile。
3.1 盡可能使用官方鏡像作為基礎(chǔ)鏡像
Docker 鏡像是基于基礎(chǔ)鏡像構(gòu)建而來(lái),因此選擇的基礎(chǔ)鏡像越恰當(dāng),我們要做的底層工作就越少。比如,如果構(gòu)建一個(gè) Java 應(yīng)用鏡像,選擇一個(gè) openjdk 鏡像作為基礎(chǔ)比選擇一個(gè) alpine 鏡像更簡(jiǎn)單。
盡可能使用當(dāng)前的官方鏡像作為基礎(chǔ)鏡像,無(wú)論是從鏡像大小,還是安全性來(lái)講,都是比較可靠的。
下面的一些鏡像,可根據(jù)使用場(chǎng)景來(lái)選擇合適的基礎(chǔ)鏡像:
鏡像名稱 | 大小 | 說(shuō)明和使用場(chǎng)景 |
---|---|---|
busybox | 754.7 KB | 一個(gè)超級(jí)簡(jiǎn)化版嵌入式 Linux 系統(tǒng)。臨時(shí)測(cè)試用。 |
alpine | 2.68 MB | 一個(gè)面向安全的、輕量級(jí)的Linux系統(tǒng),基于musl libc 和 busybox。主要用于測(cè)試,也可用于生產(chǎn)環(huán)境。 |
centos | 79.65 MB | 主要用于生產(chǎn)環(huán)境,支持CentOS/Red Hat,常用于追求穩(wěn)定性的企業(yè)應(yīng)用。 |
ubuntu | 29.01 MB | 主要用于生產(chǎn)環(huán)境,常用于人工智能計(jì)算和企業(yè)應(yīng)用。 |
debian | 52.4 MB | 主要用于生產(chǎn)環(huán)境。 |
openjdk | 161.02 MB | 主要用于 Java 應(yīng)用。 |
3.2 減少 Dockerfile 指令的行數(shù)
Dockerfile 中每一行指令都代表了一層,多一層都可能帶來(lái)鏡像大小變大。
因此,在實(shí)際編寫(xiě) Dockerfile 時(shí),可以將同類(lèi)操作放在一起來(lái)避免多行指令,更有助于促進(jìn)層緩存。比如將多條 RUN 操作進(jìn)行合并,并用 ;\\
或者 &&
連接在一起。
(減少指令行數(shù),并不意味著越少越好,需要從改動(dòng)頻繁程度來(lái)決定是否合并為一條指令。)
例如下面的 Dockerfile,會(huì)執(zhí)行多條命令,通過(guò) ;\\
連接將其用一條 RUN 指令來(lái)完成。
FROM node:6.14LABEL MAINTAINER xcbeyondRUN npm install gitbook-cli -g;\\
gitbook -V; \\
npm install svgexport -g --unsafe-permCMD ["/bin/sh"]
3.3 改動(dòng)不頻繁的內(nèi)容往前放
對(duì)于 Docker 鏡像而言,每一層都代表了 Dockerfile 中的一行指令,每一層都是前一層變化的增量。例如一個(gè) Docker 鏡像有ABCD 四層,B 層修改了,那么 BCD 都會(huì)變化。
因此,在編寫(xiě) Dockerfile 時(shí),盡量將改動(dòng)不頻繁的內(nèi)容往前放,即:將系統(tǒng)依賴往前寫(xiě),因?yàn)橄?apt, yum 這些安裝的東西,是很少修改的。然后寫(xiě)應(yīng)用的庫(kù)依賴,比如 pip install,最后 copy 應(yīng)用,編譯應(yīng)用。
例如下面這個(gè) Dockerfile,就會(huì)在每次代碼改變的時(shí)候都重新 Build 大部分層,即使只改了一個(gè)頁(yè)面的標(biāo)題。
FROM python:3.7-buster # copy sourceRUN mkdir -p /opt/appCOPY myapp /opt/app/myapp/WORKDIR /opt/app# install dependencies nginxRUN apt-get update && apt-get install nginxRUN pip install -r requirements.txtRUN chown -R www-data:www-data /opt/app # start serverEXPOSE 8020STOPSIGNAL SIGTERMCMD ["/opt/app/start-server.sh"]
我們可以改成,先安裝 Nginx,再單獨(dú) copy requirements.txt,然后安裝 pip 依賴,最后 copy 應(yīng)用代碼。
FROM python:3.7-buster # install dependencies nginxRUN apt-get update && apt-get install nginxCOPY myapp/requirements.txt /opt/app/myapp/requirements.txtRUN pip install -r requirements.txt # copy sourceRUN mkdir -p /opt/appCOPY myapp /opt/app/myapp/WORKDIR /opt/app RUN chown -R www-data:www-data /opt/app # start serverEXPOSE 8020STOPSIGNAL SIGTERMCMD ["/opt/app/start-server.sh"]
3.4 編譯和運(yùn)行需分離
我們?cè)诰幾g應(yīng)用時(shí)很多時(shí)候會(huì)用到很多編譯工具、編譯環(huán)境,例如:node、Golang 等,但是編譯后,運(yùn)行時(shí)卻不再需要。這樣的編譯環(huán)境往往占用很大,使得鏡像額外變大。
因此,可以將應(yīng)用事先在某個(gè)固定編譯環(huán)境編譯完成,得到編譯后的二進(jìn)制文件,再將其 COPY 到鏡像中即可,這樣鏡像中只包含應(yīng)用的運(yùn)行二進(jìn)制文件。
例如下面這個(gè) Dockerfile,將 Golang 程序編譯好的二進(jìn)制文件 app,構(gòu)建到鏡像中:
FROM alpine:latestLABEL maintainer xcbeyondWORKDIR /appCOPY app /appCMD ["/app/app"]
3.5 刪除不需要的依賴項(xiàng)
Docker 鏡像應(yīng)該盡可能小。在編寫(xiě) Dockerfile 時(shí)僅包含基本內(nèi)容,不要引入無(wú)關(guān)內(nèi)容,從而使得鏡像大小更小、構(gòu)建速度更快,并且減少受攻擊的可能面。
鏡像更小,也更利于存放到鏡像倉(cāng)庫(kù),減少網(wǎng)絡(luò)帶寬開(kāi)銷(xiāo)。
不要安裝應(yīng)用程序?qū)嶋H不使用的任何包、庫(kù)。
3.6 避免憑證構(gòu)建到鏡像
這是最常見(jiàn)和最危險(xiǎn)的 Dockerfile 問(wèn)題之一。在構(gòu)建鏡像過(guò)程中,復(fù)制配置文件可能很誘人,但你切記可能會(huì)引入很大的安全隱患。
在 Dockerfile 中通過(guò) COPY 指令將任何配置文件內(nèi)容都復(fù)制到你的鏡像,并且任何可以訪問(wèn)它的人都可以訪問(wèn)它。如果這個(gè)配置文件中,無(wú)意間包含了數(shù)據(jù)庫(kù)密碼配置,那么你就徹底將這些密碼暴露給了所有使用該鏡像的所有人。
為了避免這類(lèi)問(wèn)題,必須將配置密鑰、敏感數(shù)據(jù)只能提供給具體的容器,而不是提供給構(gòu)建它們的鏡像??墒褂铆h(huán)境變量、掛載卷等方式在容器啟動(dòng)時(shí)注入數(shù)據(jù)。這樣就避免了意外的信息暴露,并確保你的鏡像可跨環(huán)境重復(fù)使用。
感謝您的閱讀,也歡迎您發(fā)表關(guān)于這篇文章的任何建議,關(guān)注我,技術(shù)不迷茫!
-
容器
+關(guān)注
關(guān)注
0文章
511瀏覽量
22458 -
鏡像
+關(guān)注
關(guān)注
0文章
178瀏覽量
11251 -
Docker
+關(guān)注
關(guān)注
0文章
515瀏覽量
12974
發(fā)布評(píng)論請(qǐng)先 登錄
變量聲明最佳實(shí)踐?
虛幻引擎的紋理最佳實(shí)踐
全面詳解Dockerfile文件
DevOps最佳實(shí)踐
鏡像構(gòu)建Dockerfile的介紹
SAN設(shè)計(jì)和最佳實(shí)踐指南

Windows 10遷移的最佳實(shí)踐

評(píng)論