跳至主要内容

4.10 建置映像檔

上一小節介紹完了映像檔基礎的指令,這邊就要來建置人生的第一個映像檔啦!

再次提醒,在本書的 GitHub 儲存庫中,進入 ch-04 的 build-image-example,就會找到這次範例的所有檔案。

接著進入到檔案裡面:

$ cd docker-whoami
# 這邊給讀者一個清晰的目錄架構
.
├── Dockerfile
├── Gemfile
├── Gemfile.lock
├── LICENSE
├── README.md
├── whoami.rb

確保自己在這個資料夾內後,輸入 docker image build --tag whoami . 的指令:

$ docker image build --tag whoami .
[+] Building 37.1s (11/11) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 529B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/ruby:3.1.2-alpine 4.0s
=> [auth] library/ruby:pull token for registry-1.docker.io 0.0s
=> [1/5] FROM docker.io/library/ruby:3.1.2-alpine@sha256:499a31...... 4.7s
=> => resolve docker.io/library/ruby:3.1.2-alpine@sha256:499a31...... 0.0s
=> => sha256:e543eab4b71b99007ce154c6843c43bab8818efc18d0... 172B / 172B 0.3s
=> => sha256:499a310e8fab835ad47ab6251302aba1f5a5d0ded... 1.65kB / 1.65kB 0.0s
=> => sha256:7dcfcfa70588c4b81a6c81f47fcf6f58d486ae11f... 1.36kB / 1.36kB 0.0s
=> => sha256:6c65fdec191f877ffca756613f1e8acafb9f7258f... 6.10kB / 6.10kB 0.0s
=> => sha256:05c438b754fcec3ffc269d59394b0fa7cd82b09a.. 29.16MB / 29.16MB 0.0s
=> => extracting sha256:05c438b754fcec3ffc269d59394b0fa7cd82b09...... 3.2s
=> => extracting sha256:e543eab4b71b99007ce154c6843c43bab8818ef...... 1.2s
=> [internal] load build context 0.0s
=> => transferring context: 1.99kB 0.0s
=> [2/5] RUN apk add --update --no-cache build-base curl 10.6s
=> [3/5] WORKDIR /app 0.1s
=> [4/5] COPY . . 0.1s
=> [5/5] RUN gem install bundler:2.3.19 && bundle install... 15.5s
=> exporting to image 1.9s
=> => exporting layers 1.8s
=> => writing image sha256:7925639f0e50aa0da9d23674fb2bfb08970.... 0.0s
=> => naming to docker.io/library/whoami 0.0s

接著就建置成功人生的第一個映像檔了,可以測試一下這個映像檔到底可不可以使用,接著就拿出你們學習過啟動容器的方法來運行 whoami 這個映像檔啦!不再多做示範了。

建置映像檔的每個階段

但是單靠 docker image build 還是不夠,我們需要了解在建置映像檔的過程中到底發生了什麼事,以及該注意什麼才是重點。

從最一開始的指令來看:

$ docker image build --tag whoami .

--tag 的作用就是給予要建置的映像檔標籤名稱,而最後面有一個非常重要的 . ( 半形句號 ) 是常常會有人忽略而導致沒辦法建置映像檔的關鍵,這個 . ( 半形句號 ) 所代表的是『 這裡 』的意思,也就是 Docker 預設會在現在這個目錄中尋找 Dockerfile,並以其為主建置映像檔。

那如果根據 staging、production 分別有好幾種不同的 Dockerfile 該怎麼做呢?

Docker 則提供了 --file 的指令給您使用,例如想要建置的是 production 的映像檔,則可以如同下方指令輸入:

$ docker image build --tag whoami:production --file Dockerfile.production . #不換行

要記得,最後那個 . ( 半形句號 ) 還是要加上去,要讓 Docker 知道自己現在處於哪個路徑;當然也不侷限於 . ( 半形句號 ) 的使用,可以指定 Dockerfile 存在的路徑,例如 ./docker/ 的相對路徑,總之不要讓 Docker 迷路,它會不知道自己在哪。

接著根據整個建置映像檔的紀錄來看,第一個階段是從 FROM 這個指令開始,因為本地端沒有 ruby:3.1.2-alpine 這個映像檔,就從 DockerHub 上面抓取了官方的映像檔作為基底。

至於 Dockerfile 中的第二個動作應該是 ENV 為首的指令,為什麼沒有出現在建置過程中的紀錄中呢?

這是因為 ENV 為首的指令雖然製造出了一層映像層 ( 空的,0 KB ),但由於其本身的指令並不會對檔案系統有更動,故沒有出現在建置過程的紀錄上。

這邊說明一個小重點,Dockerfile 中的每一次指令都會製造一個新的映像層,至於會不會出現在建置的紀錄中端看這個指令對於檔案系統是否有更動,例如安裝套件、新增資料夾等等的動作,都會出現在建置的紀錄中;反之,如 EXPOSE、 ENV 這樣的指令就沒有出現在建置的紀錄中,因為其並沒有對檔案系統有任何的更動。

而利用之前介紹過的 docker image history 可以看到映像檔的所有映像層,透過 docker image inspect 則可以看到映像檔的檔案系統層,有興趣的讀者可以自己去研究一下。

接著的二、三、四、五階段,都按照著 Dockerfile 的說明執行動作,所以我才說 映像檔就是運行容器的說明書,只要寫好正確的步驟,就能夠保證應用程式順利地執行,是不是也開始感覺到 Docker 的魅力啦?

建置時的快取機制

看看這個標題,快取機制不是之前就提過了嗎?

Docker 為了提升建置的速度和儲存空間的優化,在每一個映像層都分別都賦予了以 SHA 算出來獨一無二的 ID,以便 Docker 來辨識是否有相同的映像層,若是您有這樣的想法,那代表前面寫的都沒有白費。

沒錯!Docker 在建置映像檔的快取機制就是這樣,那當檔案系統有更動時呢?讓我們來做個實驗吧!

這邊再建置一次一模一樣的映像檔,並確保您還在同一個資料夾內。

$ docker image build --tag whoami .
[+] Building 0.6s (10/10) FINISHED
=> [internal] load build definition from Dockerfile 0.1s
=> => transferring dockerfile: 37B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/ruby:3.1.2-alpine 0.0s
=> [1/5] FROM docker.io/library/ruby:3.1.2-alpine@sha256:499a31...... 0.3s
=> [internal] load build context 0.0s
=> => transferring context: 1.99kB 0.0s
=> CACHED [2/5] RUN apk add --update --no-cache build-base curl 0.0s
=> CACHED [3/5] WORKDIR /app 0.0s
=> CACHED [4/5] COPY . . 0.0s
=> CACHED [5/5] RUN gem install bundler:2.3.19 && bundle install... 0.0s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:7925639f0e50aa0da9d23674fb2bfb08970.... 0.0s
=> => naming to docker.io/library/whoami

可以看到藉由 Docker 優異的快取機制,現在整個建置的過程只剩下 0.6 秒,大約節省了 50 倍的時間。

接著請您打開編輯器,並且將 Dockerfile 中的環境變數 AUTHOR 改成您的英文名字,並且重新在建置一次映像檔。

# Dockerfile
FROM ruby:3.1.2-alpine
ENV AUTHOR=換成你的英文名字

RUN apk add --update --no-cache \
build-base \
curl

WORKDIR /app

COPY . .

RUN gem install bundler:2.3.19 && \
bundle install -j4 --retry 3 && \
bundle clean --force && \
find /usr/local/bundle -type f -name '*.c' -delete && \
find /usr/local/bundle -type f -name '*.o' -delete && \
rm -rf /usr/local/bundle/cache/*.gem

EXPOSE 3000

CMD ["bundle", "exec", "ruby", "whoami.rb", "-p", "3000", "-o", "0.0.0.0"]

重新建置一次。

$ docker image build --tag whoami .
[+] Building 39.9s (11/11) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 529B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/ruby:3.1.2-alpine 2.6s
=> [auth] library/ruby:pull token for registry-1.docker.io 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 2.48kB 0.0s
=> CACHED [1/5] FROM docker.io/library/ruby:3.1.2-alpine@sha256:499a31.. 0.0s
=> [2/5] RUN apk add --update --no-cache build-base curl 11.8s
=> [3/5] WORKDIR /app 0.2s
=> [4/5] COPY . . 0.2s
=> [5/5] RUN gem install bundler:2.3.19 && bundle install... 20.0s
=> exporting to image 5.0s
=> => exporting layers 4.9s
=> => writing image sha256:e496a661edc22d1f59e4401650ec702abee.... 0.0s
=> => naming to docker.io/library/whoami 0.0s

天啊!快取機制下的 0.6 秒怎麼變成快 40 秒了,發生了什麼事呢?唯一快取到的也只有 FROM 一層映像層。

這是因為改動映像層 ( 我們把原先的 AUTHOR 這個變數換成您的英文名字 ) 進而造成 SHA 算出來的 ID 有異,使得映像檔找不到匹配的映像層而是重新建置新的映像層。

您可能會想,就重新建置 ENV 那層映像層就好啦,其他的映像層都沒有改動,檔案也沒有變化,為什麼還要多花那麼多時間重新建置呢?

其實在 Docker 建置映像層中還有一個有趣的機制,若是上層的映像層重新建置,則其以下的所有映像層都將重新建置,在這個案例中,我們更動了 ENV 這個指令的映像層,則其以下的 RUN、WORKDIR、EXPOSE 等等,都將重新建置,也是導致建置時間大幅提升的主因。

白話來說就是 Docker 本身也不願意花那麼多時間去幫你比對每一層的映像層,只要有一層算出來的 ID 找不到匹配的映像層,那之後每一層都重新建置,也不管其他的映像層是不是已經有一模一樣的存在。

這樣子的機制就讓 Dockerfile 撰寫的順序變得十分的重要,在下一個小節,我們將會重新整理 Dockerfile 的執行順序。