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