了解 Docker build 指令

前十天,我們使用 Docker 官方的 image 作為執行指令或開服務的環境,以這個角度來介紹 Docker 可以如何使用。接下來十天,將介紹如何建置自定義 image。

Docker image 基本概念

最開始,得先了解 Docker image 的基本概念。首先最基本的,Docker 是採用 Union 檔案系統(簡稱 UnionFS)來儲存 image。

UnionFS 的特色如下:

  1. 分層式的檔案系統
  2. 支援把「檔案系統的修改」作為 commit,讓多個 commit 一層層堆疊
  3. 支援把多個目錄掛載到同一個虛擬檔案系統下

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 名稱
  • commit 後會產生一個 sha256 digest

還記得三個主要元件的特性嗎?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 同步程式

Dockerfiledocker 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 上標上有意義的文字,如 debianphpnodewordpress 等,甚至是額外的版本資訊,如 php:7.2php:7.3 等。而沒有 tag 資訊的時候,預設則會代 latest,如 vimvim: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 上安裝 PHPnode.js。其中 PHP 上的 Wordpress 非常多人使用,因此為它建立一個專用的 image。

上述過程的示意圖如下:

從這個示意圖可以發現,PHP 與 node.js 的 image 有共用到 Debian 的 image 內容。因 docker pull 是把 commit 各別下載的,所以如何在下載 image 時,先下載 PHP 再下載 node.js,剛好又遇到有共用 image 的話,它會有段訊息會提醒使用者 commit 已下載完成。

今天先有這些基本概念,明天就要正式為自己的服務建置 image 了

  • 了解 UnionFS,以及它能做到什麼事
  • 了解 image 與 container 之間的關係
  • 練習 docker buildDockerfile
  • 練習 docker commit