如何構(gòu)建 Docker 鏡像:從零開始的完整指南
引言
在本文中,您將學(xué)習(xí)如何從頭開始構(gòu)建一個(gè)Docker鏡像,并使用Dockerfile將您的應(yīng)用程序部署并運(yùn)行為一個(gè)Docker容器[1]。
如您所知,Docker 是一種用于打包、部署和運(yùn)行應(yīng)用程序的工具,它能夠在輕量級容器中完成這些操作。如果您想了解Docker的基礎(chǔ)知識(shí),請參考《Docker 詳解》[2]博客。
如果您還沒有安裝 Docker, 請檢查 Docker Installation Guide[3]。
開始
Dockerfile 詳解
一個(gè) Docker 鏡像最基礎(chǔ)的建筑塊是一個(gè) Dockerfile。
一個(gè) Dockerfile 是一個(gè)帶有指令和參數(shù)的簡易的文本文件。Docker 可以通過讀取這些在 Dockerfile 里給出的指令自動(dòng)構(gòu)建鏡像。
在一個(gè) Dockerfile 里,在左邊的一切都是指令(Instruction),而在右邊的一切都是賦予給指令的參數(shù)(Argement)。還有,要記著這個(gè)文件名稱是 Dockerfile 它不需要任何的擴(kuò)展名。
圖片
以下的表格中包含了重要的 Dockerfile 指令和它們的解釋。
Dockerfile 指令 | 解釋 |
FROM | 可以從容器注冊表拉取的基礎(chǔ)鏡像 ( Docker hub, GCR, Quay, ECR, 等等) |
RUN | 在鏡像構(gòu)建過程中執(zhí)行的命令 |
ENV | 在鏡像中設(shè)置環(huán)境變量。它是構(gòu)建過程中是可用的,同樣在運(yùn)行的容器中也是。如果您只想要在構(gòu)建時(shí)間中使用它,請使用 ARG 指令 |
COPY | 拷貝本地文件和目錄到鏡像中 |
EXPOSE | 為 Docker 容器指定特定的要暴露的端口 |
ADD | 它是 COPY 指令的功能更豐富的版本。它還允許從源 URL 復(fù)制并將 tar 文件自動(dòng)提取到鏡像中。但是, |
WORKDIR | 設(shè)置當(dāng)前的工作目錄。您可以在一個(gè) Dockerfile 里面重復(fù)使用這個(gè)指令去設(shè)置一個(gè)不同的工作目錄。如果您設(shè)置了 ENTRYPOINT,像 RUN,CMD,ADD,COPY,或者 ENTRYPOINT 這樣的指令就會(huì)在你的這個(gè)目錄里執(zhí)行 |
VOLUME | 它是用于創(chuàng)建或者掛載卷到 Docker 容器 |
USER | 當(dāng)運(yùn)行容器時(shí),設(shè)置用戶名稱和 UID 。你可以使用這個(gè)指令去設(shè)置一個(gè)非 root 的容器用戶 |
LABEL | 它是去指定 Docker 鏡像的 |
ARG | 設(shè)置構(gòu)建時(shí),帶有 Key 和 Value 的變量。當(dāng)容器運(yùn)行時(shí),ARG 變量將不可用。如果你堅(jiān)持想要在一個(gè)運(yùn)行的容器中使用一個(gè)變量,請使用 ENV |
SHELL | 它被用于為了給其后的 RUN,CMD 和 ENTRYPOINT 去設(shè)置 shell 選項(xiàng)和默認(rèn) shell 。 |
CMD | 它用于在一個(gè)運(yùn)行的容器中執(zhí)行一條命令。這里只能有一個(gè) CMD, 如果有多個(gè), |
ENTRYPOINT | 當(dāng)容器啟動(dòng)時(shí),指定的命令將會(huì)執(zhí)行。如果您不指定任何 ENTRYPOINT,它默認(rèn)會(huì)是 /bin/sh -c 。您還可以使用 CLI 的 --entrypoint 覆蓋 ENTRYPOINT。為了更多的信息請參考如下網(wǎng)址: |
使用 Dockerfile 構(gòu)建 Docker 鏡像
在這一節(jié),您將會(huì)學(xué)習(xí)使用一個(gè)在現(xiàn)實(shí)工作中使用的案例去構(gòu)建一個(gè) Docker 鏡像。我們將會(huì)從頭使用一個(gè)自定義的 Index 頁面去創(chuàng)建一個(gè) Nginx Docker 鏡像。
以下的照片展示了鏡像構(gòu)建過程的工作流。
圖片
跟隨以下給出的步驟去構(gòu)建一個(gè) Docker 鏡像。
這篇文章中被使用的 Dockerfile 和 configs 被托管在 Docker 鏡像實(shí)例 Github repo[4] 上面。您可以克隆它以便參考。
步驟 1: 創(chuàng)建一個(gè)必須的文件和文檔
創(chuàng)建一個(gè)名為 nginx-image 和一個(gè)名為 files 的文件夾
mkdir nginx-image && cd nginx-image
mkdir files
創(chuàng)建一個(gè) .dockerignore 文件
touch .dockerignore
步驟 2: 創(chuàng)建一個(gè)模板 HTML 文件和 config 文件
當(dāng)您構(gòu)建一個(gè)為實(shí)時(shí)項(xiàng)目的 Docker 鏡像時(shí),它包含了代碼或者應(yīng)用配置文件。
用于演示目的,我們將要?jiǎng)?chuàng)建一個(gè)簡單的 HTML 文件和 config 文件作為我們的 app 代碼,再使用 Docker 打包。這是一個(gè)簡單的 index.html 文件。如果您愿意,您可以創(chuàng)建您自己喜歡的。
cd 進(jìn)入文件夾。
cd files
創(chuàng)建一個(gè) index.html 文件。
vim index.html
復(fù)制以下的內(nèi)容到 index.html 再保存這個(gè)文件。
<html>
<head>
<title>Dockerfile</title>
</head>
<body>
<div class="container">
<h1>My App</h1>
<h2>This is my first app</h2>
<p>Hello everyone, This is running via Docker container</p>
</div>
</body>
</html>
創(chuàng)建一個(gè)名字為 default 的文件
vim default
復(fù)制以下的的內(nèi)容到 default 文件
server {
listen 80 default_server;
listen [::]:80 default_server;
root /usr/share/nginx/html;
index index.html index.htm;
server_name _;
location / {
try_files $uri $uri/ =404;
}
}
步驟 3: 選擇一個(gè)基礎(chǔ)鏡像
我們在 Dockerfile 中使用 FROM 命令,該命令指示 Docker 根據(jù) Docker 中心或任何使用 Docker 配置的容器注冊表上可用的鏡像創(chuàng)建鏡像。 我們稱它為一個(gè)基礎(chǔ)鏡像。
它是和我們在云上如何從一個(gè)虛擬機(jī)鏡像創(chuàng)建一個(gè)虛擬機(jī)是相似的。
選擇一個(gè)基礎(chǔ)鏡像取決于我們的應(yīng)用和選擇的 OS 平臺(tái)。在我們的例子中,我們選擇 ubuntu:18.04 基礎(chǔ)鏡像。
為了避免潛在的風(fēng)險(xiǎn),應(yīng)總是為您的應(yīng)用使用 official/org 批準(zhǔn)的基礎(chǔ)鏡像。最后,我們已經(jīng)添加了所有的已經(jīng)認(rèn)證的容器基礎(chǔ)鏡像的公共倉庫,還有,當(dāng)它來到生產(chǎn)使用案例時(shí),總是使用 minimal 基礎(chǔ)鏡像類似 Alpine[5](僅僅5Mib) 或者 distroless images[6],Distroless alpine僅僅 2 MiB
步驟 4: 創(chuàng)建一個(gè) Dockerfile
在 nginx-image 文件夾中創(chuàng)建一個(gè) Dockerfile。
vim Dockerfile
這里是一份簡單的 Dockerfile 為了我們能夠好的繼續(xù)。然后把這些添加到我們的 Dockerfile。
FROM ubuntu:18.04
LABEL maintainer="contact@devopscube.com"
RUN apt-get -y update && apt-get -y install nginx
COPY files/default /etc/nginx/sites-available/default
COPY files/index.html /usr/share/nginx/html/index.html
EXPOSE 80
CMD ["/usr/sbin/nginx", "-g", "daemon off;"]
這里是每一步的解釋:
- 1. 使用 LABEL 指令, 我們添加了關(guān)于維護(hù)者的 一些信息。他不是必須的指令哈。
- 2. FROM 指令將會(huì)從 Docker hub 拉取 Ubuntu 18.04 版本的鏡像,在第三行,我們正在安裝 Nginx 。
- 3. 然后,我們將 Nginx 的默認(rèn)配置文件從本地文件目錄拷貝到目標(biāo)鏡像目錄。
- 4. 下一步,我們將 Index.html 文件從本地目錄拷貝進(jìn)目標(biāo)鏡像目錄。它將會(huì)覆蓋在 Nginx 安裝過程中被創(chuàng)建的默認(rèn)的 Index.html 文件。
- 5. 我們暴露了 80 端口,作為 Nginx 服務(wù)監(jiān)聽的 80 端口。
- 6. 最終,當(dāng) Docker 鏡像啟動(dòng),我們的 Nginx 服務(wù)會(huì)在運(yùn)行過程中使用 CMD 指令。
在Docker 容器, 這個(gè) daemon off; 指令會(huì)告訴 Nginx 停留在前端。這就意味著 Nginx 進(jìn)程進(jìn)會(huì)保持運(yùn)行不會(huì)停止,直到你自己停止這個(gè)容器。它不允許 Nginx 的 自守護(hù)進(jìn)程行為。-g 選項(xiàng)指定了一個(gè)指令給 Nginx 。
我們在前臺(tái)運(yùn)行該進(jìn)程的原因是將控制臺(tái)進(jìn)程附加到標(biāo)準(zhǔn)輸入、輸出和錯(cuò)誤。這意味著您可以看到來自 Nginx 進(jìn)程的日志或消息。
步驟 4: 構(gòu)建你的第一個(gè) Docker 鏡像
最終的文件夾和文件結(jié)構(gòu)看起來像以下內(nèi)容。
nginx-image
├── Dockerfile
└── files
├── default
└── index.html
現(xiàn)在,我們要使用 Docker 命令構(gòu)建我們的鏡像。以下的命令會(huì)從相同的目錄使用 Dockerfile 構(gòu)建鏡像。
docker build -t nginx:1.0 .
1. -t 是為了給這個(gè)鏡像起個(gè)名字和指定你的標(biāo)簽
2. nginx 是這個(gè)鏡像的名字
3. 1.0 是這個(gè)標(biāo)簽名稱。如果你不添加任何標(biāo)簽,它默認(rèn)的標(biāo)簽名稱為 Latest
4. . 在末尾的 . 意味著我們會(huì)參考 Dokerfile 位置作為我們的 Docker 構(gòu)建上下文。也就是我們現(xiàn)在的目錄
圖片
如果 Dockerfile 在另一個(gè)文件夾,那么你需要明確的把它指定出來,否則會(huì)找不到
docker build -t nginx /path/to/folder
現(xiàn)在,我們可以使用這個(gè)命令列出鏡像
docker images
圖片
我們在這里可以看見標(biāo)簽是1.0。 如果我們想要弄一個(gè)指定的標(biāo)簽,我們可以這樣寫 image-name:[tag] 。還是那句話,如果你不指定任何標(biāo)簽,默認(rèn)會(huì)是 Latest 。
docker build -t nginx:2.0 .
一個(gè)單一的鏡像可以有多個(gè)標(biāo)簽。這里有兩種我們普遍認(rèn)同的給鏡像打標(biāo)簽的方法:
1. 穩(wěn)定的 Tags – 我們可以繼續(xù)拉取我們指定的標(biāo)簽,它會(huì)繼續(xù)獲得更新。標(biāo)簽總是不變的,但是鏡像的內(nèi)容可以改變。
2. 唯一的 Tags – 我們?yōu)槊恳粋€(gè)鏡像使用一個(gè)不同和唯一的標(biāo)簽。有多種方式可以提供唯一標(biāo)簽,例如日期時(shí)間戳、構(gòu)建編號(hào)、提交 ID 等。
當(dāng)它到生產(chǎn)環(huán)境時(shí),一個(gè)推薦給 Docker 鏡像打標(biāo)簽的方法是語義版本控制(Semver) [7]
Docker 緩存了構(gòu)建步驟,因此如果我們再次構(gòu)建這個(gè)鏡像,過程會(huì)移動(dòng)地快一點(diǎn)。例如,它不會(huì)再次下載 Ubuntu 18.04 鏡像
使用體積大的鏡像會(huì)讓容器的構(gòu)建和部署時(shí)間變長。如果你想要學(xué)習(xí)更過關(guān)于優(yōu)化 Docker 鏡像,請參考 減少 Docker 鏡像[8]指南。
步驟 5: 測試你的 Docker 鏡像
現(xiàn)在,構(gòu)建過鏡像之后,我們將會(huì)運(yùn)行這個(gè) Docker 鏡像。這個(gè)命令是:
docker run -d -p 9090:80 --name webserver nginx:1.0
這里
1. -d 這個(gè)表示讓容器在后臺(tái)運(yùn)行
2. -p 這個(gè)為了指定端口,格式為 本地端口:容器端口
3. --name 指定容器的名稱,webserver 是我們的名稱
我們可以通過以下的命令檢查這個(gè)容器
docker ps
圖片
現(xiàn)在在瀏覽器中,如果你去到 http://[host-ip]:9090,您可以看到索引頁,其中顯示了我們添加到 docker 鏡像中的自定義 HTML 頁面中的內(nèi)容。
圖片
推送 Docker 鏡像到 Docker Hub
推送我們的 Docker 鏡像到 Docker hub[9],我們需要在 Docker hub 創(chuàng)建一個(gè)帳號(hào)。
從終端執(zhí)行以下命令登錄。它將會(huì)要求輸入一個(gè)用戶名和密碼。也支持 Docker hub 憑證。
docker login
圖片
登錄進(jìn)去之后,現(xiàn)在我們需要用 Docker hub 用戶名給我們的鏡像打標(biāo)簽,如下所示。
docker tag nginx:1.0 <username>/<image-name>:tag
例如,這里的 devopscube 是 Dockerhub 用戶名。
docker tag nginx:1.0 devopscube/nginx:1.0
再次運(yùn)行 docker images 命令,檢查被打了標(biāo)簽的鏡像將會(huì)顯示在這里。
圖片
現(xiàn)在,我們使用以下的命令推送我們的鏡像到 Docker hub 。
docker push devopscube/nginx:1.0
現(xiàn)在,你可以在你的 Docker Hub 賬戶中檢查這個(gè)鏡像是否可用的。
圖片
圖片
在 Dockerfile 中使用 heredoc
Dockerfile 還支持 heredoc[10] 語法。如果你有多個(gè) RUN 命令,那么你就可以使用 heredoc,如下所示。
RUN <<EOF
apt-get update
apt-get upgrade -y
apt-get install -y nginx
EOF
還有, 讓我們聊聊你想要從 Dockerfile 執(zhí)行的一個(gè) Python 腳本,你可以使用以下的的語法。
RUN python3 <<EOF
with open("/hello", "w") as f:
print("Hello", file=f)
print("World", file=f)
EOF
你還可以使用 heredoc 語法去創(chuàng)建一個(gè)文件,這里是一個(gè) Nginx 例子。
FROM nginx
COPY <<EOF /usr/share/nginx/html/index.html
<html>
<head>
<title>Dockerfile</title>
</head>
<body>
<div class="container">
<h1>My App</h1>
<h2>This is my first app</h2>
<p>Hello everyone, This is running via Docker container</p>
</div>
</body>
</html>
EOF
Dockerfile 的最好實(shí)踐
這里是一些我們應(yīng)該遵循 Dockerfile 的通常做法:
1. 使用一個(gè) .dockerignore 文件去排除不必要的文件和目錄,好增強(qiáng)我們的構(gòu)建性能。
2. 只使用被信任的基礎(chǔ)鏡像,進(jìn)行定期更新的鏡像。
3. 在 Dockerfile 每一個(gè)指令都向 Docker 鏡像添加了額外的一層。通過把指令合并,讓鏡像層盡量以最少的層去構(gòu)建,有助于增強(qiáng)構(gòu)建性能和時(shí)間。
4. 以一個(gè)非 ROOT 用戶去運(yùn)行,有助于更加安全。
5. 把鏡像體積保持為最小:在你的鏡像中,為了更快的部署, 要避免安裝不必要的工具,以減少鏡像的大小。使用盡可能小的鏡像為了減少攻擊面。
6. 使用特定標(biāo)簽覆蓋鏡像的最新標(biāo)簽,以避免隨著時(shí)間的推移發(fā)生重大變化。
7. 當(dāng)創(chuàng)建多個(gè)緩存的層時(shí),它通常會(huì)影響到構(gòu)建過程的效率,所以應(yīng)避免使用多個(gè) RUN 命令。
8. 永遠(yuǎn)不要往你的 Dockerfile 中共享和拷貝應(yīng)用程序的憑證或者任何敏感的信息。如果你使用了它,請將其添加它到 .dockerignore。
9. 盡可能在末尾中使用 EXPOSE 和 ENV 命令。
10. 使用一個(gè) linter: 使用一個(gè)像 hadolint[11] 的 linter 去檢查你的 Dockerfile,這是為了常見的問題和最好的實(shí)踐。
11. 每一個(gè)容器只使用一個(gè)單獨(dú)進(jìn)程: 每一個(gè)容器應(yīng)該只運(yùn)行一個(gè)單獨(dú)的進(jìn)程。這是為了讓它更容易去管理和監(jiān)控容器,還有幫助我們保持容器是輕量的。
12. 使用多階段構(gòu)建:使用多階段構(gòu)建去創(chuàng)建更小和更有效率的鏡像。
潛在的 Docker 構(gòu)建問題
1. 如果在 Dockerfile 里面有一個(gè)語法錯(cuò)誤或者一個(gè)無效的參數(shù),Docker build 命令將會(huì)有一個(gè)錯(cuò)誤信息的失敗??梢詸z查語法去解決這個(gè)。
2. 始終嘗試使用 docker run 命令為容器命名。如果不指定名稱,Docker 會(huì)自動(dòng)分配一個(gè)隨機(jī)名稱,這可能會(huì)導(dǎo)致一些問題。
3. 端口沖突問題:有時(shí)會(huì)遇到類似 Bind for 0.0.0.0:8080 failed: port is already allocated 的錯(cuò)誤,這是因?yàn)槠渌浖蚍?wù)正在使用該端口??梢酝ㄟ^ netstat 或 ss 命令檢查端口占用情況,然后選擇使用其他端口或停止占用端口的服務(wù)來解決此問題。
4. 依賴包下載失敗:有時(shí) Docker 會(huì)報(bào)錯(cuò) Failed to download package [package-name],這通常是因?yàn)槿萜鳠o法訪問互聯(lián)網(wǎng)或存在其他依賴問題。
Docker 鏡像注冊表
在步驟一中提到過,你應(yīng)該始終選擇官方認(rèn)證的基礎(chǔ)鏡像作為應(yīng)用的鏡像。
以下表格列出了一些公共可用的容器注冊表,你可以在這些注冊表中找到官方認(rèn)證的基礎(chǔ)鏡像和應(yīng)用鏡像:
Registry | Base Images |
Docker | Docker hub base images[12] |
Google Cloud | Distroless base images[13] |
AWS | ECR public registry[14] |
Redhat Quay | Quay Registry[15] |
Docker Image vs. Containers
Docker 鏡像是文件系統(tǒng)和應(yīng)用依賴的快照。它是一個(gè)可執(zhí)行的軟件包,包含了運(yùn)行應(yīng)用所需的一切,比如應(yīng)用代碼、庫、工具、依賴項(xiàng)和其他文件。你可以將其類比為虛擬機(jī)的黃金鏡像。
Docker 鏡像以堆疊在一起的只讀層形式組織。
Docker 容器是 Docker 鏡像的運(yùn)行實(shí)例。就像從虛擬機(jī)鏡像創(chuàng)建虛擬機(jī)一樣,我們從容器鏡像創(chuàng)建容器。當(dāng)你從 Docker 鏡像創(chuàng)建容器時(shí),會(huì)在現(xiàn)有鏡像層之上創(chuàng)建一個(gè)可寫層。
Docker 鏡像和容器之間的主要區(qū)別在于容器頂部的可寫層。這意味著,如果你從一個(gè)鏡像運(yùn)行了五個(gè)容器,所有容器都會(huì)共享鏡像中的相同只讀層,而頂部的可寫層對每個(gè)容器來說是獨(dú)立的。
因此,當(dāng)你刪除容器時(shí),其可寫層也會(huì)被刪除。
鏡像可以獨(dú)立于容器存在,而容器需要鏡像才能運(yùn)行。我們可以從同一個(gè)鏡像創(chuàng)建多個(gè)容器,每個(gè)容器都有自己獨(dú)立的數(shù)據(jù)和狀態(tài)。
圖片
Docker 鏡像構(gòu)建 FAQs
如何使用來自 Docker hub 以外的容器注冊表的基礎(chǔ)鏡像?
默認(rèn)情況下,Docker 引擎配置為使用 Docker Hub 作為容器注冊表。因此,如果你只指定鏡像名稱,Docker 會(huì)從 Docker Hub 拉取鏡像。然而,如果你想從其他容器注冊表拉取鏡像,則需要提供完整的鏡像 URL。例如:FROM gcr.io/distroless/static-debian11。
什么是 Docker 構(gòu)建上下文?
Docker 構(gòu)建上下文是指 Docker 主機(jī)上的一個(gè)位置,其中包含構(gòu)建過程中所需的所有代碼、文件、配置和 Dockerfile。你可以使用一個(gè)點(diǎn) [.] 來指定當(dāng)前目錄作為構(gòu)建上下文,或者指定其他文件夾的路徑。此外,Dockerfile 也可以與構(gòu)建上下文位于不同的位置。
作為最佳實(shí)踐,構(gòu)建上下文中應(yīng)僅包含必需的文件。否則,可能會(huì)導(dǎo)致不必要的文件被包含,從而使 Docker 鏡像變得臃腫。
如何從一個(gè) git 倉庫構(gòu)建 Docker 鏡像?
你可以使用 docker build 命令結(jié)合 Git 倉庫來構(gòu)建 Docker 鏡像。該 Git 倉庫中必須包含 Dockerfile 和所需的文件,否則構(gòu)建過程會(huì)失敗。
下一個(gè)是什么呢?
同樣地,你可以嘗試構(gòu)建多個(gè) Docker 鏡像。
例如,你可以嘗試將一個(gè)Java 應(yīng)用容器化[16] 并運(yùn)行它。
為了進(jìn)一步學(xué)習(xí),你可以嘗試容器化以下應(yīng)用,從而掌握更多知識(shí)。
1. Python
2. NodeJS
3. Django
4. FastAPI
總結(jié)
在這篇文章中,我們討論了如何構(gòu)建 Docker 鏡像,并使用 Dockerfile 將應(yīng)用作為 Docker 容器運(yùn)行。
我們詳細(xì)介紹了 Dockerfile,并分享了一些編寫 Dockerfile 的最佳實(shí)踐。
作為一名 DevOps 工程師[17],在項(xiàng)目中應(yīng)用 Docker 之前,深入理解 Docker 的最佳實(shí)踐非常重要。此外,學(xué)習(xí) Kubernetes[18] 也需要掌握構(gòu)建容器鏡像的工作流程。
Podman 是另一個(gè)容器管理工具。如果你想了解更多,可以參考 Podman 教程[19]。
結(jié)語
ok, guys, see you next time
引用鏈接
[1] 容器:https://devopscube.com/what-is-a-container-and-how-does-it-work/
[2]《Docker 詳解》:https://devopscube.com/what-is-docker/
[3]Docker Installation Guide:https://devopscube.com/how-to-install-and-configure-docker/
[4]Docker 鏡像實(shí)例 Github repo:https://github.com/techiescamp/docker-image-examples/tree/main
[5]Alpine:https://hub.docker.com/_/alpine
[6]distroless images:https://github.com/GoogleContainerTools/distroless
[7]語義版本控制(Semver) :https://semver.org/
[8]減少 Docker 鏡像:https://devopscube.com/reduce-docker-image-size/
[9]Docker hub:https://hub.docker.com/
[10]heredoc:https://tldp.org/LDP/abs/html/here-docs.html
[11]hadolint:https://devopscube.com/lint-dockerfiles-using-hadolint/
[12]Docker hub base images:https://hub.docker.com/search?badges=official
[13]Distroless base images:https://github.com/GoogleContainerTools/distroless
[14]ECR public registry:https://gallery.ecr.aws/?verified=verified&operatingSystems=Linux&page=1
[15]Quay Registry:https://quay.io/search
[16]Java 應(yīng)用容器化:https://devopscube.com/dockerize-java-application/
[17]DevOps 工程師:https://devopscube.com/become-devops-engineer/
[18]Kubernetes:https://devopscube.com/learn-kubernetes-complete-roadmap/
[19]Podman 教程:https://devopscube.com/podman-tutorial-beginners/