隨著應(yīng)用的容器化、上云后,將伴隨著 Docker 鏡像的構(gòu)建,構(gòu)建 Docker 鏡像成為了最基本的一步,其中 Dockerfile 便是用來構(gòu)建鏡像的一種文本文件,鏡像的優(yōu)劣全靠 Dockerfile 編寫的是否合理、合規(guī)。本文將講述編寫 Dockerfile 的一些最佳實踐和技巧,讓我們的鏡像更小、更優(yōu)。
1、Docker 鏡像是如何工作的
首先,我們一起回顧下 Docker 鏡像的相關(guān)概念及工作流程吧。
1.1 鏡像
鏡像(image)是一堆只讀層(read-only layer)的統(tǒng)一視角,也許這個定義有些難以理解,下面的這張圖能夠幫助您理解鏡像的定義。
從左邊我們看到了多個只讀層,它們重疊在一起。除了最下面一層,其它層都會有一個指針指向下一層。這些層是 Docker 內(nèi)部的實現(xiàn)細(xì)節(jié),并且能夠在主機的文件系統(tǒng)上訪問到。統(tǒng)一文件系統(tǒng)技術(shù)能夠?qū)⒉煌膶诱铣梢粋€文件系統(tǒng),為這些層提供了一個統(tǒng)一的視角,這樣就隱藏了多層的存在,在用戶的角度看來,只存在一個文件系統(tǒng)。我們可以在圖片的右邊看到這個視角的形式。
您可以在您的主機文件系統(tǒng)上找到有關(guān)這些層的文件。需要注意的是,在一個運行中的容器內(nèi)部,這些層是不可見的。在我的主機上,我發(fā)現(xiàn)它們存在于 /var/lib/docker/overlay2
目錄下。
1.2 鏡像分層結(jié)構(gòu)
為什么說是鏡像分層結(jié)構(gòu),因為 Docker 鏡像是以層來組織的,可以通過命令 docker image inspect
或者 docker inspect
來查看鏡像包含哪些層。
例如,鏡像 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ù)是存儲在宿主機哪里的呢?動手實踐的同學(xué)會在上面的輸出中看到一個叫做 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é)鏡像本地的管理和存儲以及運行中的容器生成鏡像等工作,可以將 GraphDriver 理解成鏡像管理引擎,我們這里的例子對應(yīng)的引擎名字是 overlay2(overlay 的優(yōu)化版本)。除了 overlay 之外,Docker 的 GraphDriver 還支持 btrfs、aufs、devicemapper、vfs 等。
我們可以看到其中的 Data 包含了多個部分,這個對應(yīng) OverlayFS 的鏡像組織形式,雖然我們上面的例子中的 busybox 鏡像只有一層,但是正常情況下很多鏡像都是由多層組成的。
1.3 Dockerfile、鏡像、容器間的關(guān)系
Dockerfile 是軟件的原材料,Docker 鏡像是軟件的交付品,而 Docker 容器則可以認(rèn)為是軟件的運行態(tài)。從應(yīng)用軟件的角度來看,Dockerfile、Docker 鏡像與 Docker 容器分別代表軟件的三個不同階段,Dockerfile 面向開發(fā),Docker 鏡像成為交付標(biāo)準(zhǔn),Docker 容器則涉及部署與運維,三者缺一不可,合力充當(dāng) Docker 體系的基石。
簡單來講,Dockerfile 構(gòu)建出 Docker鏡像,通過 Docker 鏡像運行Docker容器。
我們可以從 Docker 容器的角度,來反推三者的關(guān)系,如下圖:
2、Dockerfile
Dockerfile 是一個用來構(gòu)建鏡像的文本文件,文本內(nèi)容包含了一條條構(gòu)建鏡像所需的指令和說明,它是構(gòu)建鏡像的關(guān)鍵。
一個 Docker 鏡像包含了很多只讀層,每一層都由一個 Dockerfile 指令構(gòu)成,這些層堆疊在一起,每一層都是前一層變化的增量。例如:
FROM ubuntu:18.04COPY . /appRUN make /appCMD python /app/app.py
每條指令都會創(chuàng)建一層:
- FROM:從 ubuntu:18.04 Docker 鏡像創(chuàng)建了一層,也作為基礎(chǔ)鏡像層。
- COPY:從 Docker 客戶端的當(dāng)前目錄添加文件。
- RUN:執(zhí)行 make 命令.
- CMD:指定要在容器中運行的命令。
上述就是一個簡單的 Dockerfile 文件,再通過 docker build -t
命令便可直接構(gòu)建出鏡像。
在這里就不過多介紹 Dockerfile 的各個指令的用法,更多更詳細(xì)的可參考:Dockerfile reference
3、Dockerfile 的最佳實踐
本節(jié)將列舉出一些最佳實踐技巧,來幫助我們更好的寫好 Dockerfile。
3.1 盡可能使用官方鏡像作為基礎(chǔ)鏡像
Docker 鏡像是基于基礎(chǔ)鏡像構(gòu)建而來,因此選擇的基礎(chǔ)鏡像越恰當(dāng),我們要做的底層工作就越少。比如,如果構(gòu)建一個 Java 應(yīng)用鏡像,選擇一個 openjdk 鏡像作為基礎(chǔ)比選擇一個 alpine 鏡像更簡單。
盡可能使用當(dāng)前的官方鏡像作為基礎(chǔ)鏡像,無論是從鏡像大小,還是安全性來講,都是比較可靠的。
下面的一些鏡像,可根據(jù)使用場景來選擇合適的基礎(chǔ)鏡像:
鏡像名稱 | 大小 | 說明和使用場景 |
---|---|---|
busybox | 754.7 KB | 一個超級簡化版嵌入式 Linux 系統(tǒng)。臨時測試用。 |
alpine | 2.68 MB | 一個面向安全的、輕量級的Linux系統(tǒng),基于musl libc 和 busybox。主要用于測試,也可用于生產(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)境,常用于人工智能計算和企業(yè)應(yīng)用。 |
debian | 52.4 MB | 主要用于生產(chǎn)環(huán)境。 |
openjdk | 161.02 MB | 主要用于 Java 應(yīng)用。 |
3.2 減少 Dockerfile 指令的行數(shù)
Dockerfile 中每一行指令都代表了一層,多一層都可能帶來鏡像大小變大。
因此,在實際編寫 Dockerfile 時,可以將同類操作放在一起來避免多行指令,更有助于促進(jìn)層緩存。比如將多條 RUN 操作進(jìn)行合并,并用 ;\\
或者 &&
連接在一起。
(減少指令行數(shù),并不意味著越少越好,需要從改動頻繁程度來決定是否合并為一條指令。)
例如下面的 Dockerfile,會執(zhí)行多條命令,通過 ;\\
連接將其用一條 RUN 指令來完成。
FROM node:6.14LABEL MAINTAINER xcbeyondRUN npm install gitbook-cli -g;\\
gitbook -V; \\
npm install svgexport -g --unsafe-permCMD ["/bin/sh"]
3.3 改動不頻繁的內(nèi)容往前放
對于 Docker 鏡像而言,每一層都代表了 Dockerfile 中的一行指令,每一層都是前一層變化的增量。例如一個 Docker 鏡像有ABCD 四層,B 層修改了,那么 BCD 都會變化。
因此,在編寫 Dockerfile 時,盡量將改動不頻繁的內(nèi)容往前放,即:將系統(tǒng)依賴往前寫,因為像 apt, yum 這些安裝的東西,是很少修改的。然后寫應(yīng)用的庫依賴,比如 pip install,最后 copy 應(yīng)用,編譯應(yīng)用。
例如下面這個 Dockerfile,就會在每次代碼改變的時候都重新 Build 大部分層,即使只改了一個頁面的標(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,再單獨 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īng)用時很多時候會用到很多編譯工具、編譯環(huán)境,例如:node、Golang 等,但是編譯后,運行時卻不再需要。這樣的編譯環(huán)境往往占用很大,使得鏡像額外變大。
因此,可以將應(yīng)用事先在某個固定編譯環(huán)境編譯完成,得到編譯后的二進(jìn)制文件,再將其 COPY 到鏡像中即可,這樣鏡像中只包含應(yīng)用的運行二進(jìn)制文件。
例如下面這個 Dockerfile,將 Golang 程序編譯好的二進(jìn)制文件 app,構(gòu)建到鏡像中:
FROM alpine:latestLABEL maintainer xcbeyondWORKDIR /appCOPY app /appCMD ["/app/app"]
3.5 刪除不需要的依賴項
Docker 鏡像應(yīng)該盡可能小。在編寫 Dockerfile 時僅包含基本內(nèi)容,不要引入無關(guān)內(nèi)容,從而使得鏡像大小更小、構(gòu)建速度更快,并且減少受攻擊的可能面。
鏡像更小,也更利于存放到鏡像倉庫,減少網(wǎng)絡(luò)帶寬開銷。
不要安裝應(yīng)用程序?qū)嶋H不使用的任何包、庫。
3.6 避免憑證構(gòu)建到鏡像
這是最常見和最危險的 Dockerfile 問題之一。在構(gòu)建鏡像過程中,復(fù)制配置文件可能很誘人,但你切記可能會引入很大的安全隱患。
在 Dockerfile 中通過 COPY 指令將任何配置文件內(nèi)容都復(fù)制到你的鏡像,并且任何可以訪問它的人都可以訪問它。如果這個配置文件中,無意間包含了數(shù)據(jù)庫密碼配置,那么你就徹底將這些密碼暴露給了所有使用該鏡像的所有人。
為了避免這類問題,必須將配置密鑰、敏感數(shù)據(jù)只能提供給具體的容器,而不是提供給構(gòu)建它們的鏡像。可使用環(huán)境變量、掛載卷等方式在容器啟動時注入數(shù)據(jù)。這樣就避免了意外的信息暴露,并確保你的鏡像可跨環(huán)境重復(fù)使用。
感謝您的閱讀,也歡迎您發(fā)表關(guān)于這篇文章的任何建議,關(guān)注我,技術(shù)不迷茫!
-
容器
+關(guān)注
關(guān)注
0文章
509瀏覽量
22440 -
鏡像
+關(guān)注
關(guān)注
0文章
178瀏覽量
11236 -
Docker
+關(guān)注
關(guān)注
0文章
515瀏覽量
12934
發(fā)布評論請先 登錄
評論