如何構建 Docker 鏡像:從零開始的完整指南
引言
在本文中,您將學習如何從頭開始構建一個Docker鏡像,并使用Dockerfile將您的應用程序部署并運行為一個Docker容器[1]。
如您所知,Docker 是一種用于打包、部署和運行應用程序的工具,它能夠在輕量級容器中完成這些操作。如果您想了解Docker的基礎知識,請參考《Docker 詳解》[2]博客。
如果您還沒有安裝 Docker, 請檢查 Docker Installation Guide[3]。
開始
Dockerfile 詳解
一個 Docker 鏡像最基礎的建筑塊是一個 Dockerfile。
一個 Dockerfile 是一個帶有指令和參數的簡易的文本文件。Docker 可以通過讀取這些在 Dockerfile 里給出的指令自動構建鏡像。
在一個 Dockerfile 里,在左邊的一切都是指令(Instruction),而在右邊的一切都是賦予給指令的參數(Argement)。還有,要記著這個文件名稱是 Dockerfile 它不需要任何的擴展名。
圖片
以下的表格中包含了重要的 Dockerfile 指令和它們的解釋。
Dockerfile 指令 | 解釋 |
FROM | 可以從容器注冊表拉取的基礎鏡像 ( Docker hub, GCR, Quay, ECR, 等等) |
RUN | 在鏡像構建過程中執行的命令 |
ENV | 在鏡像中設置環境變量。它是構建過程中是可用的,同樣在運行的容器中也是。如果您只想要在構建時間中使用它,請使用 ARG 指令 |
COPY | 拷貝本地文件和目錄到鏡像中 |
EXPOSE | 為 Docker 容器指定特定的要暴露的端口 |
ADD | 它是 COPY 指令的功能更豐富的版本。它還允許從源 URL 復制并將 tar 文件自動提取到鏡像中。但是, |
WORKDIR | 設置當前的工作目錄。您可以在一個 Dockerfile 里面重復使用這個指令去設置一個不同的工作目錄。如果您設置了 ENTRYPOINT,像 RUN,CMD,ADD,COPY,或者 ENTRYPOINT 這樣的指令就會在你的這個目錄里執行 |
VOLUME | 它是用于創建或者掛載卷到 Docker 容器 |
USER | 當運行容器時,設置用戶名稱和 UID 。你可以使用這個指令去設置一個非 root 的容器用戶 |
LABEL | 它是去指定 Docker 鏡像的 |
ARG | 設置構建時,帶有 Key 和 Value 的變量。當容器運行時,ARG 變量將不可用。如果你堅持想要在一個運行的容器中使用一個變量,請使用 ENV |
SHELL | 它被用于為了給其后的 RUN,CMD 和 ENTRYPOINT 去設置 shell 選項和默認 shell 。 |
CMD | 它用于在一個運行的容器中執行一條命令。這里只能有一個 CMD, 如果有多個, |
ENTRYPOINT | 當容器啟動時,指定的命令將會執行。如果您不指定任何 ENTRYPOINT,它默認會是 /bin/sh -c 。您還可以使用 CLI 的 --entrypoint 覆蓋 ENTRYPOINT。為了更多的信息請參考如下網址: |
使用 Dockerfile 構建 Docker 鏡像
在這一節,您將會學習使用一個在現實工作中使用的案例去構建一個 Docker 鏡像。我們將會從頭使用一個自定義的 Index 頁面去創建一個 Nginx Docker 鏡像。
以下的照片展示了鏡像構建過程的工作流。
圖片
跟隨以下給出的步驟去構建一個 Docker 鏡像。
這篇文章中被使用的 Dockerfile 和 configs 被托管在 Docker 鏡像實例 Github repo[4] 上面。您可以克隆它以便參考。
步驟 1: 創建一個必須的文件和文檔
創建一個名為 nginx-image 和一個名為 files 的文件夾
mkdir nginx-image && cd nginx-image
mkdir files創建一個 .dockerignore 文件
touch .dockerignore步驟 2: 創建一個模板 HTML 文件和 config 文件
當您構建一個為實時項目的 Docker 鏡像時,它包含了代碼或者應用配置文件。
用于演示目的,我們將要創建一個簡單的 HTML 文件和 config 文件作為我們的 app 代碼,再使用 Docker 打包。這是一個簡單的 index.html 文件。如果您愿意,您可以創建您自己喜歡的。
cd 進入文件夾。
cd files創建一個 index.html 文件。
vim index.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>創建一個名字為 default 的文件
vim default復制以下的的內容到 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: 選擇一個基礎鏡像
我們在 Dockerfile 中使用 FROM 命令,該命令指示 Docker 根據 Docker 中心或任何使用 Docker 配置的容器注冊表上可用的鏡像創建鏡像。 我們稱它為一個基礎鏡像。
它是和我們在云上如何從一個虛擬機鏡像創建一個虛擬機是相似的。
選擇一個基礎鏡像取決于我們的應用和選擇的 OS 平臺。在我們的例子中,我們選擇 ubuntu:18.04 基礎鏡像。
為了避免潛在的風險,應總是為您的應用使用 official/org 批準的基礎鏡像。最后,我們已經添加了所有的已經認證的容器基礎鏡像的公共倉庫,還有,當它來到生產使用案例時,總是使用 minimal 基礎鏡像類似 Alpine[5](僅僅5Mib) 或者 distroless images[6],Distroless alpine僅僅 2 MiB
步驟 4: 創建一個 Dockerfile
在 nginx-image 文件夾中創建一個 Dockerfile。
vim Dockerfile這里是一份簡單的 Dockerfile 為了我們能夠好的繼續。然后把這些添加到我們的 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 指令, 我們添加了關于維護者的 一些信息。他不是必須的指令哈。
- 2. FROM 指令將會從 Docker hub 拉取 Ubuntu 18.04 版本的鏡像,在第三行,我們正在安裝 Nginx 。
- 3. 然后,我們將 Nginx 的默認配置文件從本地文件目錄拷貝到目標鏡像目錄。
- 4. 下一步,我們將 Index.html 文件從本地目錄拷貝進目標鏡像目錄。它將會覆蓋在 Nginx 安裝過程中被創建的默認的 Index.html 文件。
- 5. 我們暴露了 80 端口,作為 Nginx 服務監聽的 80 端口。
- 6. 最終,當 Docker 鏡像啟動,我們的 Nginx 服務會在運行過程中使用 CMD 指令。
在Docker 容器, 這個 daemon off; 指令會告訴 Nginx 停留在前端。這就意味著 Nginx 進程進會保持運行不會停止,直到你自己停止這個容器。它不允許 Nginx 的 自守護進程行為。-g 選項指定了一個指令給 Nginx 。
我們在前臺運行該進程的原因是將控制臺進程附加到標準輸入、輸出和錯誤。這意味著您可以看到來自 Nginx 進程的日志或消息。
步驟 4: 構建你的第一個 Docker 鏡像
最終的文件夾和文件結構看起來像以下內容。
nginx-image
├── Dockerfile
└── files
├── default
└── index.html現在,我們要使用 Docker 命令構建我們的鏡像。以下的命令會從相同的目錄使用 Dockerfile 構建鏡像。
docker build -t nginx:1.0 .1. -t 是為了給這個鏡像起個名字和指定你的標簽
2. nginx 是這個鏡像的名字
3. 1.0 是這個標簽名稱。如果你不添加任何標簽,它默認的標簽名稱為 Latest
4. . 在末尾的 . 意味著我們會參考 Dokerfile 位置作為我們的 Docker 構建上下文。也就是我們現在的目錄
圖片
如果 Dockerfile 在另一個文件夾,那么你需要明確的把它指定出來,否則會找不到
docker build -t nginx /path/to/folder現在,我們可以使用這個命令列出鏡像
docker images
圖片
我們在這里可以看見標簽是1.0。 如果我們想要弄一個指定的標簽,我們可以這樣寫 image-name:[tag] 。還是那句話,如果你不指定任何標簽,默認會是 Latest 。
docker build -t nginx:2.0 .一個單一的鏡像可以有多個標簽。這里有兩種我們普遍認同的給鏡像打標簽的方法:
1. 穩定的 Tags – 我們可以繼續拉取我們指定的標簽,它會繼續獲得更新。標簽總是不變的,但是鏡像的內容可以改變。
2. 唯一的 Tags – 我們為每一個鏡像使用一個不同和唯一的標簽。有多種方式可以提供唯一標簽,例如日期時間戳、構建編號、提交 ID 等。
當它到生產環境時,一個推薦給 Docker 鏡像打標簽的方法是語義版本控制(Semver) [7]
Docker 緩存了構建步驟,因此如果我們再次構建這個鏡像,過程會移動地快一點。例如,它不會再次下載 Ubuntu 18.04 鏡像
使用體積大的鏡像會讓容器的構建和部署時間變長。如果你想要學習更過關于優化 Docker 鏡像,請參考 減少 Docker 鏡像[8]指南。
步驟 5: 測試你的 Docker 鏡像
現在,構建過鏡像之后,我們將會運行這個 Docker 鏡像。這個命令是:
docker run -d -p 9090:80 --name webserver nginx:1.0這里
1. -d 這個表示讓容器在后臺運行
2. -p 這個為了指定端口,格式為 本地端口:容器端口
3. --name 指定容器的名稱,webserver 是我們的名稱
我們可以通過以下的命令檢查這個容器
docker ps
圖片
現在在瀏覽器中,如果你去到 http://[host-ip]:9090,您可以看到索引頁,其中顯示了我們添加到 docker 鏡像中的自定義 HTML 頁面中的內容。
圖片
推送 Docker 鏡像到 Docker Hub
推送我們的 Docker 鏡像到 Docker hub[9],我們需要在 Docker hub 創建一個帳號。
從終端執行以下命令登錄。它將會要求輸入一個用戶名和密碼。也支持 Docker hub 憑證。
docker login
圖片
登錄進去之后,現在我們需要用 Docker hub 用戶名給我們的鏡像打標簽,如下所示。
docker tag nginx:1.0 <username>/<image-name>:tag例如,這里的 devopscube 是 Dockerhub 用戶名。
docker tag nginx:1.0 devopscube/nginx:1.0再次運行 docker images 命令,檢查被打了標簽的鏡像將會顯示在這里。
圖片
現在,我們使用以下的命令推送我們的鏡像到 Docker hub 。
docker push devopscube/nginx:1.0現在,你可以在你的 Docker Hub 賬戶中檢查這個鏡像是否可用的。
圖片
圖片
在 Dockerfile 中使用 heredoc
Dockerfile 還支持 heredoc[10] 語法。如果你有多個 RUN 命令,那么你就可以使用 heredoc,如下所示。
RUN <<EOF
apt-get update
apt-get upgrade -y
apt-get install -y nginx
EOF還有, 讓我們聊聊你想要從 Dockerfile 執行的一個 Python 腳本,你可以使用以下的的語法。
RUN python3 <<EOF
with open("/hello", "w") as f:
print("Hello", file=f)
print("World", file=f)
EOF你還可以使用 heredoc 語法去創建一個文件,這里是一個 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>
EOFDockerfile 的最好實踐
這里是一些我們應該遵循 Dockerfile 的通常做法:
1. 使用一個 .dockerignore 文件去排除不必要的文件和目錄,好增強我們的構建性能。
2. 只使用被信任的基礎鏡像,進行定期更新的鏡像。
3. 在 Dockerfile 每一個指令都向 Docker 鏡像添加了額外的一層。通過把指令合并,讓鏡像層盡量以最少的層去構建,有助于增強構建性能和時間。
4. 以一個非 ROOT 用戶去運行,有助于更加安全。
5. 把鏡像體積保持為最小:在你的鏡像中,為了更快的部署, 要避免安裝不必要的工具,以減少鏡像的大小。使用盡可能小的鏡像為了減少攻擊面。
6. 使用特定標簽覆蓋鏡像的最新標簽,以避免隨著時間的推移發生重大變化。
7. 當創建多個緩存的層時,它通常會影響到構建過程的效率,所以應避免使用多個 RUN 命令。
8. 永遠不要往你的 Dockerfile 中共享和拷貝應用程序的憑證或者任何敏感的信息。如果你使用了它,請將其添加它到 .dockerignore。
9. 盡可能在末尾中使用 EXPOSE 和 ENV 命令。
10. 使用一個 linter: 使用一個像 hadolint[11] 的 linter 去檢查你的 Dockerfile,這是為了常見的問題和最好的實踐。
11. 每一個容器只使用一個單獨進程: 每一個容器應該只運行一個單獨的進程。這是為了讓它更容易去管理和監控容器,還有幫助我們保持容器是輕量的。
12. 使用多階段構建:使用多階段構建去創建更小和更有效率的鏡像。
潛在的 Docker 構建問題
1. 如果在 Dockerfile 里面有一個語法錯誤或者一個無效的參數,Docker build 命令將會有一個錯誤信息的失敗。可以檢查語法去解決這個。
2. 始終嘗試使用 docker run 命令為容器命名。如果不指定名稱,Docker 會自動分配一個隨機名稱,這可能會導致一些問題。
3. 端口沖突問題:有時會遇到類似 Bind for 0.0.0.0:8080 failed: port is already allocated 的錯誤,這是因為其他軟件或服務正在使用該端口。可以通過 netstat 或 ss 命令檢查端口占用情況,然后選擇使用其他端口或停止占用端口的服務來解決此問題。
4. 依賴包下載失敗:有時 Docker 會報錯 Failed to download package [package-name],這通常是因為容器無法訪問互聯網或存在其他依賴問題。
Docker 鏡像注冊表
在步驟一中提到過,你應該始終選擇官方認證的基礎鏡像作為應用的鏡像。
以下表格列出了一些公共可用的容器注冊表,你可以在這些注冊表中找到官方認證的基礎鏡像和應用鏡像:
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 鏡像是文件系統和應用依賴的快照。它是一個可執行的軟件包,包含了運行應用所需的一切,比如應用代碼、庫、工具、依賴項和其他文件。你可以將其類比為虛擬機的黃金鏡像。
Docker 鏡像以堆疊在一起的只讀層形式組織。
Docker 容器是 Docker 鏡像的運行實例。就像從虛擬機鏡像創建虛擬機一樣,我們從容器鏡像創建容器。當你從 Docker 鏡像創建容器時,會在現有鏡像層之上創建一個可寫層。
Docker 鏡像和容器之間的主要區別在于容器頂部的可寫層。這意味著,如果你從一個鏡像運行了五個容器,所有容器都會共享鏡像中的相同只讀層,而頂部的可寫層對每個容器來說是獨立的。
因此,當你刪除容器時,其可寫層也會被刪除。
鏡像可以獨立于容器存在,而容器需要鏡像才能運行。我們可以從同一個鏡像創建多個容器,每個容器都有自己獨立的數據和狀態。
圖片
Docker 鏡像構建 FAQs
如何使用來自 Docker hub 以外的容器注冊表的基礎鏡像?
默認情況下,Docker 引擎配置為使用 Docker Hub 作為容器注冊表。因此,如果你只指定鏡像名稱,Docker 會從 Docker Hub 拉取鏡像。然而,如果你想從其他容器注冊表拉取鏡像,則需要提供完整的鏡像 URL。例如:FROM gcr.io/distroless/static-debian11。
什么是 Docker 構建上下文?
Docker 構建上下文是指 Docker 主機上的一個位置,其中包含構建過程中所需的所有代碼、文件、配置和 Dockerfile。你可以使用一個點 [.] 來指定當前目錄作為構建上下文,或者指定其他文件夾的路徑。此外,Dockerfile 也可以與構建上下文位于不同的位置。
作為最佳實踐,構建上下文中應僅包含必需的文件。否則,可能會導致不必要的文件被包含,從而使 Docker 鏡像變得臃腫。
如何從一個 git 倉庫構建 Docker 鏡像?
你可以使用 docker build 命令結合 Git 倉庫來構建 Docker 鏡像。該 Git 倉庫中必須包含 Dockerfile 和所需的文件,否則構建過程會失敗。
下一個是什么呢?
同樣地,你可以嘗試構建多個 Docker 鏡像。
例如,你可以嘗試將一個Java 應用容器化[16] 并運行它。
為了進一步學習,你可以嘗試容器化以下應用,從而掌握更多知識。
1. Python
2. NodeJS
3. Django
4. FastAPI
總結
在這篇文章中,我們討論了如何構建 Docker 鏡像,并使用 Dockerfile 將應用作為 Docker 容器運行。
我們詳細介紹了 Dockerfile,并分享了一些編寫 Dockerfile 的最佳實踐。
作為一名 DevOps 工程師[17],在項目中應用 Docker 之前,深入理解 Docker 的最佳實踐非常重要。此外,學習 Kubernetes[18] 也需要掌握構建容器鏡像的工作流程。
Podman 是另一個容器管理工具。如果你想了解更多,可以參考 Podman 教程[19]。
結語
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 鏡像實例 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 應用容器化: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/

































