鏡像是 Docker 容器的基石,容器是鏡像的運行實例,有了鏡像才能啟動容器。為什么我們要討論鏡像的內部結構?
如果只是使用鏡像,當然不需要了解,直接通過 docker 命令下載和運行就可以了。
但如果我們想創建自己的鏡像,或者想理解 Docker 為什么是輕量級的,就非常有必要學習這部分知識了。
一、最小的鏡像
1、運行hello-world鏡像
hello-world 是 Docker 官方提供的一個鏡像,通常用來驗證 Docker 是否安裝成功。
我們先通過 docker pull
從 Docker Hub 下載它。

用 docker images
命令查看鏡像的信息。

hello-world 鏡像竟然還不到 14KB! 通過 docker run
運行。

其實我們更關心 hello-world 鏡像包含哪些內容。
2. hello-world鏡像內容
Dockerfile 是鏡像的描述文件,定義了如何構建Docker鏡像。Dockerfile的語法簡潔且可讀性強,后面我們會專門討論如何編寫Dockerfile。
hello-world 的 Dockerfile 內容如下:

只有短短三條指令。
#1、此鏡像是從白手起家,從 0 開始構建。
FROM scratch
#2、將文件“hello”復制到鏡像的根目錄。
COPY hello /
#3、容器啟動時,執行 /hello
CMD ["/hello"]
鏡像 hello-world 中就只有一個可執行文件 “hello”,其功能就是打印出 “Hello from Docker ......” 等信息。
/hello 就是文件系統的全部內容,連最基本的 /bin,/usr, /lib, /dev 都沒有。
hello-world 雖然是一個完整的鏡像,但它並沒有什么實際用途。通常來說,我們希望鏡像能提供一個基本的操作系統環境,用戶可以根據
需要安裝和配置軟件。這樣的鏡像我們稱作 base 鏡像。我們下一節討論 base 鏡像。
二、base 鏡像
1.base鏡像含義
base 鏡像有兩層含義:
1、不依賴其他鏡像,從 scratch 構建。
2、其他鏡像可以之為基礎進行擴展。
所以,能稱作 base 鏡像的通常都是各種 Linux 發行版的 Docker 鏡像,比如 Ubuntu, Debian, CentOS 等。
2.base鏡像內容
我們以 CentOS 為例考察 base 鏡像包含哪些內容。
下載鏡像:docker pull centos
查看鏡像信息

鏡像大小 231MB。等一下!一個 CentOS 才 200MB ?平時我們安裝一個 CentOS 至少都有幾個 GB,怎么可能才 200MB !
相信這是幾乎所有 Docker 初學者都會有的疑問,包括我自己。下面我們來解釋這個問題。
Linux 操作系統由內核空間
和用戶空間
組成。如下圖所示:

內核空間是kernel
,Linux 剛啟動時會加載 bootfs 文件系統,之后 bootfs 會被卸載掉。用戶空間的文件系統是 rootfs
,包含我們熟悉的
/dev, /proc, /bin 等目錄。對於 base 鏡像來說,底層直接用 Host 的 kernel,自己只需要提供 rootfs 就行了。
而對於一個精簡的 OS,rootfs 可以很小,只需要包括最基本的命令、工具和程序庫就可以了。相比其他 Linux 發行版,CentOS 的 rootfs
已經算臃腫的了,alpine 還不到 10MB。我們平時安裝的 CentOS 除了 rootfs 還會選裝很多軟件、服務、圖形桌面等,需要好幾個 GB 就
不足為奇了。下面是 CentOS 鏡像的 Dockerfile 的內容:

第二行 ADD 指令添加到鏡像的 tar 包就是 CentOS 7 的 rootfs。在制作鏡像時,tar包會自動解壓到 / 目錄下,生成 /dev, /porc, /bin 等目錄。
不同 Linux 發行版的區別主要就是 rootfs。
比如 Ubuntu 14.04 使用 upstart 管理服務,apt 管理軟件包;而 CentOS 7 使用 systemd 和 yum。這些都是用戶空間上的區別,
Linux kernel 差別不大。所以 Docker 可以同時支持多種 Linux 鏡像,模擬出多種操作系統環境。

上圖 Debian 和 BusyBox(一種嵌入式 Linux)上層提供各自的 rootfs,底層共用 Docker Host 的 kernel。
這里需要說明的是:
容器只能使用 Host 的 kernel,並且不能修改。
所有容器都共用 host 的 kernel,在容器中沒辦法對 kernel 升級。如果容器對 kernel 版本有要求(比如應用只能在某個 kernel 版本下運行),則不建議用容器,這種場景虛擬機可能更合適。
下一節我們討論鏡像的分層結構。
三、鏡像的分層結構
Docker 支持通過擴展現有鏡像,創建新的鏡像。
1、鏡像分層示例
實際上,Docker Hub 中 99% 的鏡像都是通過在 base 鏡像中安裝和配置需要的軟件構建出來的。比如我們現在構建一個新的鏡像,Dockerfile 如下:

① 新鏡像不再是從 scratch 開始,而是直接在 Debian base 鏡像上構建。
② 安裝 emacs 編輯器。
③ 安裝 apache2。
④ 容器啟動時運行 bash。
構建過程如下圖所示:

可以看到,新鏡像是從 base 鏡像一層一層疊加生成的。每安裝一個軟件,就在現有鏡像的基礎上增加一層。
2、鏡像分層好處
問什么 Docker 鏡像要采用這種分層結構呢?
最大的一個好處就是 - 共享資源
。
比如:有多個鏡像都從相同的 base 鏡像構建而來,那么 Docker Host 只需在磁盤上保存一份 base 鏡像;同時內存中也只需加載一份 base
鏡像,就可以為所有容器服務了。而且鏡像的每一層都可以被共享,我們將在后面更深入地討論這個特性。
這時可能就有人會問了:如果多個容器共享一份基礎鏡像,當某個容器修改了基礎鏡像的內容,比如 /etc 下的文件,這時其他容器的 /etc
是否也會被修改?
答案是不會!
修改會被限制在單個容器內。
這就是我們接下來要學習的容器 Copy-on-Write 特性。
3、Copy-on-Write 特性
可寫的容器層
當容器啟動時,一個新的可寫層被加載到鏡像的頂部。這一層通常被稱作“容器層”,“容器層”之下的都叫“鏡像層”。

所有對容器的改動 - 無論添加、刪除、還是修改文件都只會發生在容器層中。
只有容器層是可寫的,容器層下面的所有鏡像層都是只讀的
。
下面我們深入討論容器層的細節。
鏡像層數量可能會很多,所有鏡像層會聯合在一起組成一個統一的文件系統。如果不同層中有一個相同路徑的文件,比如 /a,上層的 /a 會
覆蓋下層的 /a,也就是說用戶只能訪問到上層中的文件 /a。在容器層中,用戶看到的是一個疊加之后的文件系統。
添加文件
在容器中創建文件時,新文件被添加到容器層中。
讀取文件
在容器中讀取某個文件時,Docker 會從上往下依次在各鏡像層中查找此文件。一旦找到,打開並讀入內存。
修改文件
在容器中修改已存在的文件時,Docker 會從上往下依次在各鏡像層中查找此文件。一旦找到,立即將其復制到容器層,然后修改之。
刪除文件
在容器中刪除文件時,Docker 也是從上往下依次在鏡像層中查找此文件。找到后,會在容器層中記錄下此刪除操作。
只有當需要修改時才復制一份數據,這種特性被稱作 Copy-on-Write。可見容器層保存的是鏡像變化的部分,不會對鏡像本身進行任何修改。
這樣就解釋了我們前面提出的問題:*容器層記錄對鏡像的修改,所有鏡像層都是只讀的,不會被容器修改,所以鏡像可以被多個容器共享。
理解了鏡像的原理和結構,下一節我們學習如何構建鏡像。
四、構建鏡像
1、為何要構建鏡像
對於 Docker 用戶來說,最好的情況是不需要自己創建鏡像。幾乎所有常用的數據庫、中間件、應用軟件等都有現成的 Docker 官方鏡像或
其他人和組織創建的鏡像,我們只需要稍作配置就可以直接使用。
使用現成鏡像的好處除了省去自己做鏡像的工作量外,更重要的是可以利用前人的經驗。特別是使用那些官方鏡像,因為 Docker 的工程師
知道如何更好的在容器中運行軟件。
當然,某些情況下我們也不得不自己構建鏡像,比如:
1. 找不到現成的鏡像,比如自己開發的應用程序。
2. 需要在鏡像中加入特定的功能,比如官方鏡像幾乎都不提供 ssh。
所以本節我們將介紹構建鏡像的方法。同時分析構建的過程也能夠加深我們對前面鏡像分層結構的理解。
2、構建鏡像方法
Docker 提供了兩種構建鏡像的方法:
1. docker commit 命令
2. Dockerfile 構建文件
3、docker commit構建鏡像
docker commit 命令是創建新鏡像最直觀的方法,其過程包含三個步驟:
1. 運行容器
2. 修改容器
3. 將容器保存為新的鏡像
舉個例子:在 ubuntu base 鏡像中安裝 vi 並保存為新鏡像。
1)、第一步:運行容器

-it
參數的作用是以交互模式進入容器,並打開終端。6e2d389d4576 是容器的內部 ID。
2)、第二步:安裝 vi

確認 vi 沒有安裝。開始安裝 apt-get install -y vim

3)、第三步:保存新鏡像
在新窗口中查看當前運行的容器。

gifted_stallman 是 Docker 為我們的容器隨機分配的名字。
執行 docker commit
命令將容器保存為鏡像。

新鏡像命名為 ubuntu-with-vi
。
查看新鏡像的屬性。

從 size 上看到鏡像因為安裝了軟件而變大了。從新鏡像啟動容器,驗證 vi 已經可以使用。

以上演示了如何用 docker commit 創建新鏡像。然而,Docker 並不建議用戶通過這種方式構建鏡像。原因如下:
1. 這是一種手工創建鏡像的方式,容易出錯,效率低且可重復性弱。比如要在 debian base 鏡像中也加入 vi,還得重復前面的所有步驟。
2. 更重要的:使用者並不知道鏡像是如何創建出來的,里面是否有惡意程序。也就是說無法對鏡像進行審計,存在安全隱患。
既然 docker commit 不是推薦的方法,我們干嘛還要花時間學習呢?
原因是:即便是用 Dockerfile(推薦方法)構建鏡像,底層也 docker commit 一層一層構建新鏡像的。學習 docker commit 能夠幫助我們
更加深入地理解構建過程和鏡像的分層結構。
下一節我們學習如何通過 Dockerfile 構建鏡像。
五、Dockerfile 構建鏡像
Dockerfile 是一個文本文件,記錄了鏡像構建的所有步驟。
1、Dockerfile 構建鏡像
1)創建Dockerfile文件
touch Dockerfile
2)用 Dockerfile 創建上節的 ubuntu-with-vi,其內容則為:

3)構建鏡像
docker build -t ubuntu-with-vi-dockerfile .
ubuntu-with-vi-dockerfile是構建鏡像所取的名字

運行 docker build
命令,-t
將新鏡像命名為 ubuntu-with-vi-dockerfile,命令末尾的 .
指明 build context 為當前目錄。
Docker 默認會從 build context 中查找 Dockerfile 文件,我們也可以通過 -f 參數指定 Dockerfile 的位置。
4)鏡像構建成功
通過 docker images 查看鏡像信息。

可以看到新鏡像已經構建成功,而且大小跟之前docker commit 構建的大小是一樣大的。
參考
本博客所有內容均來自 《每天5分鍾玩轉 Docker 容器技術》書籍