前十天,我們使用 Docker 官方的 image 作為執行指令或開服務的環境,以這個角度來介紹 Docker 可以如何使用。接下來十天,將介紹如何建置自定義 image。
最開始,得先了解 Docker image 的基本概念。首先最基本的,Docker 是採用 Union 檔案系統(簡稱 UnionFS)來儲存 image。
UnionFS 的特色如下:
Docker 是如何利用 UnionFS 的呢?我們來做點小實驗就會知道了。
首先一樣拿 BusyBox 來實驗,首先先照下面的指令執行:
# Terminal 1
docker run --rm -it --name some busybox
# 進去建立一些檔案
echo new commit > file
cat file
# Terminal 2,commit 檔案系統成為新的 image
docker commit some myimage
# 執行新的 image
docker run --rm -it --name new myimage
# 檢查剛剛建立的檔案是否存在
cat file
這裡有個新指令 docker commit
,它可以把 container 上對檔案系統的修改 commit 成新的 image。
some
為 container 名稱myimage
為 commit 完成後的 image 名稱還記得三個主要元件的特性嗎?image 因為有 digest,所以是不可修改的;container 是可以執行且可讀可寫,所以上面會有對檔案系統的修改。
Container 原本是疊在 BusyBox 上的讀寫層,當下了 docker commit
後,container 的修改內容會變成了不可修改的 image,而原本的 BusyBox 因為不可修改的特性,所以它依然可以拿來 run container,不會因此而消失。
接著來看一個範例,如果讀者有實驗過的話,會發現很多 image 都沒有內建 Vim。的確大多數情境是不需要,但還是希望有個內建 Vim 比較方便。沒關係,今天就來做一個 Vim image:
# Terminal 1
docker run --rm -it --name some alpine
# 進去確認並安裝 Vim
apk add --no-cache vim
# Terminal 2
docker commit some vim
# 執行新的 vim image
docker run --rm -it --name new vim
# 執行剛剛安裝好的 vim
vim
照上面的範例執行完後,先恭喜大家,我們成功基於 Alpine 上做出一個內建 Vim 的 image 了。從這兩個簡單的範例,相信讀者已經了解「分層式的檔案系統」的基本樣貌,以及如何運用 docker commit
來建立自己要的 image。
接著再繼續延伸下去思考:因多個 container 可以基於同一個 image,所以多個 image 基於同個 image 也是可行的,這樣的設計可以減少非常多空間浪費。而「多個目錄掛載到同一個虛擬檔案系統下」這個特點,其實就是之前說明的使用 volume 同步程式。
Dockerfile
與 docker bulid
指令啟動並進入 container 做建置後,再使用 docker commit
的方法,雖然可以完成客製化 image 的需求,但這並不是 Docker 官方建議的做法,最主要的原因是,建置過程無法記錄與自動化建置,這會造成我建的 image 跟你建的 image 有可能會不一樣。因此 Docker 另外設計了 Dockerfile
來描述與執行自動化建置。
在說明之前,我們先整理一下 image 和 container 之間轉換的關係:
docker run
可由 image 產生 container。這個在一開始介紹三個主要元件的時候有提到docker commit
可由 container 產生 image。這個指令是今天一開始提到的Dockerfile
的原理很單純,裡面可以描述 RUN
指令,完成後立即 commit,簡單來說就是不斷執行 run & commit。還是用範例做說明,以剛剛 Vim image 為例,可以先建立 Dockerfile
內容如下:
FROM alpine
RUN apk add --no-cache vim
接著執行 docker build
指令:
# Build 一個新的 image,完成後 tag 為 vim
docker build -t vim .
# 使用新的 image 執行 container
docker run --rm -it --name new vim
跟使用 docker commit
比起來,改用 Dockerfile
後,建置 image 的指令變得非常地簡單,而且 Dockerfile
是純文字檔,可以簽入版本控制。
再來我們觀察裡面幾個訊息,docker build
會把 Dockerfile
拆解成一行一行指令,它的第一個指令是 FROM
,指的是要從哪個 image 為基底開始建置,範例的 a24bb4013296
即為 Alpine image 的 digest。
Step 1/2 : FROM alpine
---> a24bb4013296
接著第二步是執行 RUN
指令,後面接的是安裝 Vim 的指令 apk add --no-cache vim
。
Step 2/2 : RUN apk add --no-cache vim
---> Running in 4d5e354483b8
fetch http://dl-cdn.alpinelinux.org/alpine/v3.12/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.12/community/x86_64/APKINDEX.tar.gz
(1/5) Installing xxd (8.2.0735-r0)
(2/5) Installing lua5.3-libs (5.3.5-r6)
(3/5) Installing ncurses-terminfo-base (6.2_p20200523-r0)
(4/5) Installing ncurses-libs (6.2_p20200523-r0)
(5/5) Installing vim (8.2.0735-r0)
Executing busybox-1.31.1-r16.trigger
OK: 35 MiB in 19 packages
Removing intermediate container 4d5e354483b8
---> f17e5e8e4781
這裡可以看到 Running in 4d5e354483b8
,因為是 running 關鍵字,所以 4d5e354483b8
指的正是 CONTAINER ID
。在安裝完成後會將 container commit,範例的 digest 是 f17e5e8e4781
,接著會把 container 移除。
最後完成的訊息,會提醒 tag 名與 commit digest:
Successfully built f17e5e8e4781
Successfully tagged vim:latest
在建置 image 完成後,中間任一 commit 還是可以拿來做為 image 使用。但單靠 digest 資訊,是無法找到想要的 image,因此 Docker image 有提供一個功能叫 tag,它可以在 commit 上標上有意義的文字,如 debian
、php
、node
、wordpress
等,甚至是額外的版本資訊,如 php:7.2
、php:7.3
等。而沒有 tag 資訊的時候,預設則會代 latest
,如 vim
與 vim:latest
是相同的。
所以從範例和說明可以知道,下面這三個指令的結果是一模一樣的:
docker run --rm -it --name new vim
docker run --rm -it --name new vim:latest
docker run --rm -it --name new f17e5e8e4781
Docker 可以先建立 Debian image,再從 Debian 上安裝 PHP 與 node.js。其中 PHP 上的 Wordpress 非常多人使用,因此為它建立一個專用的 image。
上述過程的示意圖如下:
從這個示意圖可以發現,PHP 與 node.js 的 image 有共用到 Debian 的 image 內容。因 docker pull
是把 commit 各別下載的,所以如何在下載 image 時,先下載 PHP 再下載 node.js,剛好又遇到有共用 image 的話,它會有段訊息會提醒使用者 commit 已下載完成。
今天先有這些基本概念,明天就要正式為自己的服務建置 image 了
docker build
與 Dockerfile
docker commit