Docker 映像檔是容器的根本,其建置方式直接影響容器的佈署效率、除錯難易度、組態彈性和安全性。不同於虛擬機器映像檔的完整檔案系統虛擬化,Docker 映像檔直接構建於主機檔案系統之上,並充分利用寫入時複製(CoW)技術,實作高效的映像檔儲存和分發。Docker 的分層檔案系統允許使用者以互動方式建置映像檔,並透過 Dockerfile 實作自動化建置流程,提升開發效率。為了最小化映像檔大小,建議選擇精簡的基礎映像檔,例如 Busybox 或 Alpine,並標準化基礎映像檔版本以利於共用。同時,在撰寫 Dockerfile 時,應注意合併指令以減少映像檔層數,並善用多階段建置移除不必要的檔案。此外,透過環境變數和掛載組態檔案,可以有效提升映像檔的可重複使用性,簡化組態管理。
建置映像檔的藝術
在 Docker 基礎架構的建置過程中,掌握建置映像檔的技術至關重要。映像檔是所有容器的基礎,而建置映像檔的方式將直接影響容器的佈署速度、紀錄擷取的難易度、組態彈性以及安全性。儘管建置映像檔的首要任務是確保容器能夠正常運作,但上述因素在生產環境中同樣舉足輕重。
在探討建置映像檔的細節之前,我們需要先了解 Docker 映像檔的實作方式。
並非傳統意義上的映像檔
乍看之下,Docker 映像檔與虛擬機器(VM)映像檔並無太大差異,但實際上兩者的實作方式大相逕庭。虛擬機器映像檔提供完整檔案系統虛擬化,意味著映像檔中的檔案系統可以與存放映像檔的主機檔案系統截然不同。虛擬機器映像檔通常被實作為主機作業系統上的龐大檔案。一旦為虛擬機器分配了磁碟區,虛擬機器中的作業系統就會在此磁碟區上建立並格式化一個或多個分割區。虛擬機器管理程式會將這些分割區以原始磁碟的形式呈現給虛擬機器中的作業系統。
這種虛擬化檔案系統的方法提供了高度的隔離性和彈性,但效率卻有待商榷。例如,當多台虛擬機器共用同一映像檔,或需要建立些微不同的映像檔時,傳統的做法是為新映像檔建立檔案系統的完整複製,以便兩個映像檔中的檔案系統可以各自獨立演變。這種做法不僅耗時耗力,也浪費大量的磁碟空間。因此,虛擬機器供應商紛紛採用寫入時複製(Copy-on-Write, CoW)技術,以提高映像檔的使用效率。
寫入時複製與高效映像檔儲存及分發
寫入時複製是一種節省時間和空間的技術,尤其適用於多個處理程式從相同的基準資料開始執行的情況。在虛擬化領域,寫入時複製技術使得多台虛擬機器可以共用相同的基礎映像檔,無需為每台虛擬機器建立獨立的映像檔複製。以 20 台虛擬機器共用同一基礎映像檔為例,寫入時複製技術允許所有虛擬機器從相同的映像檔啟動,大幅縮短啟動時間並節省磁碟空間。寫入時複製技術為每台虛擬機器的作業系統提供了一個覆寫層(overlay),使它們能夠獨立修改檔案系統,而不會影響基礎映像檔。
當作業系統嘗試修改檔案系統時,虛擬機器管理程式會將待編輯的磁碟區複製到覆寫層,並將複製出來的內容呈現給作業系統。接著,虛擬機器管理程式允許作業系統修改覆寫層中的複製內容,而原始的基礎映像檔保持不變。從此以後,原始的分享複製內容對該虛擬機器不再可見,只有覆寫層中的複製內容可供存取。虛擬機器管理程式為作業系統提供了將覆寫層與基礎映像檔合併後的檔案系統視覺效果,就像是一個完整的磁碟區。
Docker 對寫入時複製的運用
Docker 映像檔原生支援寫入時複製技術,但與傳統虛擬機器不同的是,Docker 映像檔並非完全虛擬化,而是直接建構在主機檔案系統之上。這種做法是否比完全虛擬化更具效能優勢仍有待商榷,端視使用情境而定。例如,在虛擬機器世界中,寫入時複製通常是以磁碟區為基礎,只有基礎映像檔中變更的磁碟區才會被複製和編輯到覆寫層。相較之下,Docker 則是複製和編輯整個檔案。因此,如果大型檔案只有部分內容發生變更,整個檔案都需要被複製。然而,Docker 映像檔無需在客戶端和主機作業系統之間進行檔案系統轉換,這是其一大優勢。
Docker 將寫入時複製技術發揮得淋漓盡致,使得堆積疊多個寫入時複製覆寫層以建立映像檔或相關映像檔家族變得非常容易。
Docker 利用寫入時複製技術達到兩個主要目的。首先,它允許使用者以互動方式逐層建置映像檔。其次,它對映像檔的儲存和分發具有深遠的影響。通常,在建置系統時,我們會根據相同的少數作業系統,並分享一些基本組態。在這種情況下,容器映像檔僅在最後一哩組態上有所不同,而有時候容器甚至使用完全相同的映像檔。寫入時複製技術在這種情境下非常有效,可以節省大量時間和空間。
Docker 還利用寫入時複製技術在覆寫層上執行每個容器,而非直接在映像檔上執行。原始映像檔以唯讀模式使用,容器對檔案系統所做的任何變更都僅限於覆寫層。你可能會在 Docker 文獻中看到 Docker 映像檔是不可變的,這正是這個說法的含義:一旦映像檔建立後,就永遠不會被修改,你只能根據它建立新的映像檔。
Docker 的覆寫層之所以強大,是因為這些覆寫層可以在不同主機之間分享。每個覆寫層都包含對其基礎映像檔的參照,而基礎映像檔本身又是另一個覆寫層。每個覆寫層都有一個唯一的 ID,並且可以選擇性地擁有名稱和版本。Docker 將儲存在映像檔倉函式庫中並分享的覆寫層命名為映像檔。當佈署容器時,Docker 會檢查容器所需的映像檔是否已存在於本地倉函式庫中。如果不存在,Docker 會檢查映像檔倉函式庫中的該映像檔,並下載該映像檔所需的所有覆寫層參照,然後判斷哪些覆寫層已經存在於本地,最後下載缺失的覆寫層。
這種方法可以減少主機上儲存所有所需映像檔所需的空間,並大幅縮短下載新映像檔所需的時間。例如,在執行 10 個容器且這 10 個容器都根據相同的 CentOS 7 基礎映像檔的情境中,主機只需下載一次基礎映像檔,然後下載每個不同的覆寫層,而無需下載 10 個各自包含完整 CentOS 7 的映像檔。同樣地,下載更新的映像檔也只需要下載最新的幾個覆寫層。
我們將在本章後續內容中更詳細地討論如何利用這些功能,但首先讓我們來看看建置映像檔的一個重要導向:讓它能夠正常運作。
程式碼範例:建置 Docker 映像檔
# 使用官方 CentOS 7 作為基礎映像檔
FROM centos:7
# 設定工作目錄
WORKDIR /app
# 複製應用程式檔案到容器中
COPY . /app
# 安裝必要的套件
RUN yum install -y httpd
# 設定 httpd
RUN echo "Hello World!" > /var/www/html/index.html
# 對外暴露 80 連線埠
EXPOSE 80
# 設定容器啟動時執行的指令
CMD ["httpd", "-D", "FOREGROUND"]
內容解密:
FROM centos:7:使用官方 CentOS 7 作為基礎映像檔來建置新的 Docker 映像檔。WORKDIR /app:設定容器內的工作目錄為/app。COPY . /app:將當前目錄下的所有檔案複製到容器內的/app目錄。RUN yum install -y httpd:在容器內安裝 Apache HTTP Server(httpd)。RUN echo "Hello World!" > /var/www/html/index.html:建立一個簡單的網頁,用於測試 httpd 是否正常運作。EXPOSE 80:宣告容器需要對外開放 80 連線埠,以便外部可以存取 httpd。CMD ["httpd", "-D", "FOREGROUND"]:設定容器啟動時執行的預設指令,即以前台模式啟動 httpd。
這個 Dockerfile 範例示範瞭如何從頭開始建置一個簡單的 HTTP Server 映像檔,並說明瞭每個指令的作用和邏輯。
圖表示例:Docker 映像檔分層結構
圖表翻譯: 此圖表展示了 Docker 映像檔的分層結構,從底層到頂層依次為基礎映像檔、第一層 Overlay、第二層 Overlay,直到應用程式層。每新增一層 Overlay,都可以在前一層的基礎上進行修改或新增內容,而無需改變底層的內容。這種分層結構使得 Docker 容器能夠高效地共用和重複使用相同的基礎映像檔,大幅降低儲存空間佔用和提升佈署速度。
映像檔建置基礎
建置容器映像檔(以下簡稱映像檔)最基本上有兩種方法。第一種方法是從基礎映像檔(例如 ubuntu-14.04)啟動容器,在容器內執行一系列命令(如安裝套件和編輯設定檔),然後在達到預期狀態後儲存該容器成為新的映像檔。
互動式建置映像檔
首先,我們來看看如何互動式地建置映像檔。在第一個終端機中,我們使用 ubuntu 基礎映像檔啟動一個容器並執行 /bin/bash:
docker run -ti ubuntu /bin/bash
root@4621ac608b25:/# pwd
/
root@4621ac608b25:/# ls
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
root@4621ac608b25:/# touch docker-was-here
root@4621ac608b25:/#
內容解密:
docker run -ti ubuntu /bin/bash:使用 ubuntu 映像檔啟動一個容器並進入互動式 bash 環境。pwd:顯示當前工作目錄。ls:列出根目錄下的檔案和資料夾。touch docker-was-here:在根目錄下建立一個名為docker-was-here的空檔案。
接著,在第二個終端機中,我們根據第一個容器(ID 為 4621ac608b25)的狀態建立一個新的映像檔 my-new-image:
docker commit 4621ac608b25 my-new-image
6aeffe57ec698e0e5d618bd7b8202adad5c6a826694b26cb95448dda788d4ed8
內容解密:
docker commit 4621ac608b25 my-new-image:將 ID 為 4621ac608b25 的容器提交為一個新的映像檔,命名為my-new-image。
然後,我們啟動一個新的容器,使用剛才建立的 my-new-image,並驗證該映像檔是否包含我們建立的 docker-was-here 檔案:
$ docker run -ti my-new-image /bin/bash
root@50d33db925e4:/# ls
bin boot dev docker-was-here etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
root@50d33db925e4:/# ls -la
total 72
drwxr-xr-x 32 root root 4096 May 1 03:33 .
drwxr-xr-x 32 root root 4096 May 1 03:33 ..
-rwxr-xr-x 1 root root 0 May 1 03:33 .dockerenv
-rwxr-xr-x 1 root root 0 May 1 03:33 .dockerinit
drwxr-xr-x 2 root root 4096 Mar 20 05:22 bin
drwxr-xr-x 2 root root 4096 Apr 10 2014 boot
drwxr-xr-x 5 root root 380 May 1 03:33 dev
-rw-r--r-- 1 root root 0 May 1 03:31 docker-was-here
drwxr-xr-x 64 root root 4096 May 1 03:33 etc
....
drwxr-xr-x 12 root root 4096 Apr 21 22:18 var
root@50d33db925e4:/#
內容解密:
docker run -ti my-new-image /bin/bash:使用my-new-image啟動一個新的容器並進入互動式 bash 環境。ls和ls -la:驗證docker-was-here檔案是否存在於新的容器中。
使用 Dockerfile 建置映像檔
儘管互動式建置映像檔的方法很直觀,但它不便於重複性和自動化。對於生產環境,自動化建置映像檔是十分必要的。Docker 提供了一種根據 Dockerfile 的方法來實作這一點。
Dockerfile 中包含一系列指令,用於指示 Docker 在容器中執行操作以生成映像檔。這些指令可以分為兩類別:修改映像檔檔案系統的指令和修改映像檔元資料的指令。
例如,ADD指令會將遠端 URL 定義的檔案寫入映像檔的檔案系統中,而 RUN指令則會在映像檔中執行命令。另一方面,CMD指令則設定了容器啟動時預設執行的命令及其引數。
當使用 docker build 時,Docker 首先根據 Dockerfile 中的 FROM指令啟動一個臨時容器,然後在該容器的上下文中執行每條指令。對於每條指令,Docker 都會建立一個中間映像檔,以便於增量式地建置映像檔。
例如,以下是一個 Dockerfile,用於建置與之前互動式建置相同的映像檔:
FROM ubuntu
MAINTAINER Me Myself and I
RUN touch /docker-was-here
內容解密:
FROM ubuntu:指定基礎映像檔為 ubuntu。MAINTAINER Me Myself and I:指定維護者資訊。RUN touch /docker-was-here:在容器中執行命令,建立/docker-was-here檔案。
接下來,我們告訴 Docker 使用這個 Dockerfile 建置 my-new-image:
$ docker build -t my-new-image .
內容解密:
docker build -t my-new-image .:使用當前目錄下的 Dockerfile 建置一個名為my-new-image的映像檔。
如果 Dockerfile 名稱不同或位於其他位置,可以使用 -f 指定路徑:
$ docker build -t my-new-image -f my-other-dockerfile .
分層檔案系統與空間節省
正如前一節所討論的,Docker 映像檔採用分層架構,其中每個層都是一組對前一層檔案的增刪改操作。當某一層新增檔案時,會建立新檔案;當某一層刪除檔案時,檔案被標記為已刪除,但仍存在於前面的層中;當某一層修改檔案時,根據 Docker 使用的儲存驅動程式的不同,要麼在新層中重新建立整個檔案,要麼只替換檔案中的部分磁碟扇區。無論哪種方式,舊檔案在前面的層中保持不變,而新層中包含對該檔案的新更改。
這種分層架構在建置和佈署 Docker 映像檔時非常有用,因為它允許增量式地開發映像檔,並且在佈署新映像檔時,只需傳輸新的層,從而節省時間和空間。
圖表說明
以下 Plantuml 圖表展示了 Docker 分層檔案系統的架構: 圖表翻譯: 此圖表展示了 Docker 分層檔案系統的架構,從基礎映像檔開始,每一層都在前一層的基礎上進行修改,最終形成最終的映像檔。這種分層結構使得 Docker 能夠高效地利用儲存空間,並且在佈署新映像檔時,只需傳輸新的層,從而節省時間和頻寬。
最小化Docker映像檔大小的最佳實踐
在建置Docker映像檔時,映像檔的大小是一個重要的考量因素。較小的映像檔可以減少下載時間、節省磁碟空間,並提高開發效率。本章節將探討如何最小化Docker映像檔的大小,並提供相關的最佳實踐。
為什麼映像檔大小很重要
映像檔大小在多個方面都很重要。首先,下載映像檔到主機的時間取決於其大小。其次,較大的映像檔需要更多的磁碟空間。此外,在開發過程中,下載所有必要的映像檔可能需要很長時間,這可能會降低開發效率。
選擇小型基礎映像檔
要最小化映像檔大小,第一步是選擇一個小型基礎映像檔。以下是一些可供選擇的選項:
空檔案系統:您可以從一個空的檔案系統開始,並在其中佈署您的作業系統。然而,這種方法可能不是最廣泛使用的。
微型發行版:
Busybox:大小僅為2.5MB,是專為嵌入式應用程式設計的精簡發行版。它包含基本的Unix工具,但您也可以根據需要自定義Busybox發行版,以新增或刪除命令。Busybox通常足以執行靜態編譯的二進位制檔案,例如用Go語言編寫的程式。
Alpine:根據Busybox,增加了一個注重安全性的核心版本和一個名為apk的套件管理器,並根據Musl libc,一個更輕量級且可能更快的libc版本。Alpine可以用作容器中的通用Linux發行版,但需要更多的組態工作。雖然Alpine提供了套件管理器,但可用的套件數量遠少於像Debian或CentOS這樣的完整發行版。Alpine的優勢在於它具有更少的元件,因此更小、更簡單,也更容易確保安全。
容器最佳化版本的主流Linux發行版:
- 例如Ubuntu或CentOS的容器最佳化版本。這些容器通常執行精簡版的完整發行版,去除了桌面相關的部分,並針對生產伺服器進行了最佳化。這些映像檔的大小通常為幾百MB,例如Ubuntu 14.04約為190MB,CentOS 7約為215MB,但它們提供了完整的作業系統。使用這些映像檔是建置自定義映像檔最簡單的方法,因為它們對套件化服務的支援非常好,並且網路上有大量的檔案和教程可供參考。
標準化基礎映像檔版本
在所有可能的容器中使用相同的基礎映像檔版本是一個好主意。如果主機上的所有容器都具有相同的基本映像檔,那麼在下載第一個容器後,其他容器的下載速度將大大提高,因為它們不需要重新下載基本映像檔層。但需要注意的是,這種最佳化只有在基本映像檔已經被下載的情況下才有效。如果同時下載多個映像檔,而基本映像檔尚未被下載,那麼基本映像檔將被下載多次。
保持映像檔小型的技巧
選擇小型基礎映像檔後,下一步是在執行Dockerfile時保持映像檔的小型化。Dockerfile中的每條命令都會生成一個新的映像檔層。當生成一個層時,會設定一個新的最小映像檔大小;即使您在接下來的命令中刪除了檔案,也不會釋放任何空間,映像檔在主機檔案系統上的大小也不會縮小。
示例:安裝Scala並清理暫存檔案
為了說明這一點,讓我們看一個安裝Scala並清理暫存檔案的例子。下面的Dockerfile指令展示瞭如何在單一步驟中安裝Scala並清理暫存檔案:
RUN curl -o /tmp/scala-2.10.2.tgz http://www.scala-lang.org/files/archive/scala-2.10.2.tgz \
&& tar xzf /tmp/scala-2.10.2.tgz -C /usr/share/ \
&& ln -s /usr/share/scala-2.10.2 /usr/share/scala \
&& for i in scala scalac fsc scaladoc scalap; do ln -s /usr/share/scala/bin/${i} /usr/bin/${i}; done \
&& rm -f /tmp/scala-2.10.2.tgz
內容解密:
- 使用
curl下載Scala安裝包到/tmp/scala-2.10.2.tgz。 - 使用
tar解壓縮下載的安裝包到/usr/share/目錄。 - 建立一個符號連結
/usr/share/scala指向/usr/share/scala-2.10.2,以便於版本切換。 - 為Scala的可執行檔建立符號連結到
/usr/bin/,使得這些命令可以在任何地方被呼叫。 - 刪除下載的
.tgz檔案,以節省空間。
如果將上述命令拆分成多個RUN指令,如下所示:
RUN curl -o /tmp/scala-2.10.2.tgz http://www.scala-lang.org/files/archive/scala-2.10.2.tgz
RUN tar xzf /tmp/scala-2.10.2.tgz -C /usr/share/
RUN ln -s /usr/share/scala-2.10.2 /usr/share/scala
RUN for i in scala scalac fsc scaladoc scalap; do ln -s /usr/share/scala/bin/${i} /usr/bin/${i}; done
RUN rm -f /tmp/scala-2.10.2.tgz
那麼,即使最後刪除了.tgz檔案,該檔案仍然會存在於之前的映像檔層中,從而增加了最終映像檔的大小。
使映像檔可重複使用
組態容器內執行的程式或程式有兩種主要方法:一是透過環境變數傳遞組態,二是將組態檔案和/或目錄掛載到容器中。
使用環境變數
環境變數是一種簡單直接的組態方法。您可以在執行容器時透過-e或--env引數設定環境變數,或者在Dockerfile中使用ENV指令設定預設值。例如:
ENV DATABASE_URL="localhost:5432"
掛載組態檔案
對於更複雜的組態,您可以將組態檔案掛載到容器中。這可以透過在執行容器時使用-v或--volume引數來實作。例如:
docker run -v /path/to/config:/etc/myapp/config myimage
這種方法允許您在不修改映像檔的情況下更改組態。
隨著容器技術的不斷發展,我們可以期待出現更多最佳化Docker映像檔大小和提高其可重複使用性的方法。同時,開發者社群將繼續推動相關工具和最佳實踐的發展,以滿足日益增長的容器化需求。
圖表示例:Docker 映像層結構圖
@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle
title Docker 映像檔建置最佳實踐
package "Docker 架構" {
actor "開發者" as dev
package "Docker Engine" {
component [Docker Daemon] as daemon
component [Docker CLI] as cli
component [REST API] as api
}
package "容器運行時" {
component [containerd] as containerd
component [runc] as runc
}
package "儲存" {
database [Images] as images
database [Volumes] as volumes
database [Networks] as networks
}
cloud "Registry" as registry
}
dev --> cli : 命令操作
cli --> api : API 呼叫
api --> daemon : 處理請求
daemon --> containerd : 容器管理
containerd --> runc : 執行容器
daemon --> images : 映像檔管理
daemon --> registry : 拉取/推送
daemon --> volumes : 資料持久化
daemon --> networks : 網路配置
@enduml
圖表翻譯:
此圖示呈現了Docker映像層的基本結構,從基礎映像層開始,逐步疊加應用程式層和組態層,最終形成執行中的容器。每個層都是根據前一層構建的,這種分層結構使得Docker能夠高效地管理和共用不同的層,從而節省儲存空間並加速容器的建立過程。