4.11 重新整理 Dockerfile 的執行順序
為了讓重新建置的副作用降到最低,我們需要調整 Dockerfile 的指令順序,指令執行的順序並不會影響到容器的啟動,所以不用擔心,但還是有些小地方需要注意。
唯一一個不會更動的就是 FROM 這個指令,之前的章節也有提過,所有的映像檔都是透過另一個映像檔作為基底,所以 FROM 絕對是要擺在最上面的。
而在思考如何調整指令順序時,要知道變動機率越低的指令應該要放在越上面,才可以讓重新建置的副作用降到最低。
首先,變動機率最低的就是 CMD 以及 EXPOSE 這兩個指令,啟動應用程式的啟動指令基本上都會相同,即便更換了版本,或是檔案做了什麼異動,啟動的方式都還是大同小異。
而 EXPOSE 則是在設定好後就很少會進行變動,舉例來說,nginx 也不會突然變成開 678 port,而自己建置的應用程式也應該會有固定啟動的 port 才對。
所以現在的 Dockerfile 變成這樣:
# Dockerfile
FROM ruby:3.1.2-alpine
ENV AUTHOR=robertchang <- 看異動情況取捨
EXPOSE 3000 <- 移動到上面
CMD ["bundle", "exec", "ruby", "whoami.rb", "-p", "3000", "-o", "0.0.0.0"] <- 移動到上面
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
至於 ENV 的異動頻率就要視自己手邊的專案而定,或是可以透過在容器啟動時傳入 ( 傳入環境變數在啟動 postgres 這個容器時就有使用過了 ),總之環境變數有許多種放入容器的方式,怎麼取捨完全是看個人喜好。
接著關於 RUN apk .. 和 WORKDIR 這兩個之間的取捨,肯定是 WORKDIR 會放在比較上面的位置,畢竟我們有可能會需要新的套件,所以 RUN apk .. 這件事情的異動頻率就會比 WORKDIR 來得高,經過一番調整後,Dockerfile 會變成下方這樣。
# Dockerfile
FROM ruby:3.1.2-alpine
ENV AUTHOR=robertchang <- 看異動情況取捨
EXPOSE 3000 <- 移動到上面
CMD ["bundle", "exec", "ruby", "whoami.rb", "-p", "3000", "-o", "0.0.0.0"] <- 移動到上面
WORKDIR /app <- 移動到上面
RUN apk add --update --no-cache \
build-base \
curl
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
接著是 RUN apk ... 以及 COPY 之間的取捨,常理來說,COPY 的異動頻率會比安裝套件來得高,畢竟在開發的情況下,檔案會一直有變動,導致雖然指令本身都是 COPY . . ,但因為編輯過的檔案會導致算出來的 SHA ID 完全不同,進而觸發重新建置的副作用。
而最後則是 RUN gem install ... ,對於不熟悉 Ruby 的朋友們,稍微解釋一下,gem 是 Ruby 圈中的套件管理工具,會根據 Gemfile 這個檔案所描述的套件進行安裝;可以想像成 JavaScript 圈中 yarn 以及 npm 這類的工具根據 package.json 進行安裝是一樣的道理,亦或是 Rust 圈中的 Cargo 等等。
暫且不論這個指令詳細的內容,這不在我們的討論範圍,但我們很清楚他是一個安裝套件的指令,所以可以想像成和 RUN apk ... 是一樣的概念,進而想要把它往上移動,這時就會發生錯誤,讓我們以身試誤,看看錯誤訊息是什麼吧!
# Dockerfile
FROM ruby:3.1.2-alpine
ENV AUTHOR=robertchang <- 看異動情況取捨
EXPOSE 3000 <- 移動到上面
CMD ["bundle", "exec", "ruby", "whoami.rb", "-p", "3000", "-o", "0.0.0.0"] <- 移動到上面
WORKDIR /app <- 移動到上面
RUN apk add --update --no-cache \
build-base \
curl
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
COPY . .
存檔後,進行建置的動作,並看看錯誤是什麼。
$ docker image build --tag whoami .
[+] Building 39.9s (11/11) FINISHED
=> [internal] load build definition from Dockerfile 0.1s
=> => transferring dockerfile: 529B 0.0s
=> [internal] load .dockerignore 0.1s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/ruby:3.1.2-alpine 2.9s
=> [auth] library/ruby:pull token for registry-1.docker.io 0.1s
=> [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] WORKDIR /app 0.1s
=> [3/5] RUN apk add --update --no-cache build-base curl 19.5s
=> ERROR [4/5] RUN gem install bundler:2.3.19 && bundle install... 2.9s
------
> [4/5] RUN gem install bundler:2.3......
#8 2.526 Successfully installed bundler-2.3.19
#8 2.526 1 gem installed
#8 2.838 Could not locate Gemfile
------
executor failed running [/bin/sh -c gem install bundler:2.3.19 && bundle....
在安裝套件的時候出錯了,可以看到錯誤訊息是 Could not locate Gemfile,也就是它找不到那個可以去參照的檔案來安裝套件。
而錯誤的原因非常簡單,之前有提過映像層是一層接著一層堆疊起來的,所以下層會具備上層所擁有的檔案系統以及安裝過的套件,而在 RUN gem install ... 的當下,還沒有把本機的檔案 COPY 到建置的過程中,進而導致執行 RUN gem install ... 的當下根本找不到參照的檔案 ( Gemfile )。
而在本章的開頭我有提到 指令執行的順序不會影響到容器的啟動,指的是我們把 CMD 以及 EXPOSE 等等的指令往前放並不會導致容器啟動時出現問題。
但把 COPY . . 放到最後所產生的錯誤並不是 Docker 本身所導致,而是在使用這個機制上沒有搞清楚建置的運行軌跡所致。
所以最終這個 Dockerfile 能夠訂正到影響最小的版本就如下方所示:
# Dockerfile
FROM ruby:3.1.2-alpine
ENV AUTHOR=robertchang <- 看異動情況取捨
EXPOSE 3000 <- 移動到上面
CMD ["bundle", "exec", "ruby", "whoami.rb", "-p", "3000", "-o", "0.0.0.0"] <- 移動到上面
WORKDIR /app <- 移動到上面
RUN apk add --update --no-cache \
build-base \
curl
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
在這個情形下,就只有更動本機會被 COPY 的檔案才會觸發重新建置的副作用,已經算是把副作用的影響範圍降到最低了。
建置映像檔及運行容器練習
每一個章節都會提供一些小小的練習來熟悉本章的內容,通常會比較難一點;若是想不出來也不需要灰心,有些時候可能是指令不熟悉導致,可以往前翻閱來加強記憶喔,亦或是翻到後面的解 答章節也可以幫助你解惑喔,記得 --help
以及 docs.docker.com 會是你最好的夥伴。
這個練習將會透過前幾個小節所學習到建置映像檔的技術,自己手寫一份 Dockerfile 並且建置它,再把它運行成容器,最後推送到自己的 DockerHub。
- 進入本書的 GitHub 儲存庫內 ch-04 的 build-image-practice 可以看到所有檔案並以編輯器打開
- 手動建立一個 Dockerfile 的檔案
- 使用 node:16-alpine 做為基礎映像檔
- 在 alpine 的作業系統下安裝 libc6-compat 這個套件
- 複製所有檔案至映像檔的檔案系統內
- 使用
yarn install
這個指令安裝相關套件 - 打開 port 3000
- 加入
yarn dev -p 3000
的初始指令 - 使用
docker image build
的指令建置映像檔 - 使用
docker container run
的指令確定映像檔的可運行性 - 輸入網址 http://localhost:3000 並看到『 恭喜你成功打包成映像檔,並運行成容器!』字樣表示成功
- 重新替映像檔貼上可以上傳到 DockerHub 的標籤,並上傳至自己的 DockerHub
- 刪掉本地端的映像檔,使用
docker container run
的方式從 DockerHub 使用取用自製的映像檔並運行成容器