跳至主要内容

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。

  1. 進入本書的 GitHub 儲存庫內 ch-04 的 build-image-practice 可以看到所有檔案並以編輯器打開
  2. 手動建立一個 Dockerfile 的檔案
  3. 使用 node:16-alpine 做為基礎映像檔
  4. 在 alpine 的作業系統下安裝 libc6-compat 這個套件
  5. 複製所有檔案至映像檔的檔案系統內
  6. 使用 yarn install 這個指令安裝相關套件
  7. 打開 port 3000
  8. 加入 yarn dev -p 3000 的初始指令
  9. 使用 docker image build 的指令建置映像檔
  10. 使用 docker container run 的指令確定映像檔的可運行性
  11. 輸入網址 http://localhost:3000 並看到『 恭喜你成功打包成映像檔,並運行成容器!』字樣表示成功
  12. 重新替映像檔貼上可以上傳到 DockerHub 的標籤,並上傳至自己的 DockerHub
  13. 刪掉本地端的映像檔,使用 docker container run 的方式從 DockerHub 使用取用自製的映像檔並運行成容器

建置映像檔及運行容器練習解答

  1. 進入資料夾後,用編輯器打開資料夾。
  2. 接著手動建立一個 Dockerfile。
$ touch Dockerfile # 手動建立也可以
  1. 使用 node:16-alpine 做為基礎映像檔,自此開始進入 Dockerfile 內編輯。
FROM node:16-alpine
  1. 需要在 alpine 作業系統下安裝 libc6-compat 這個套件
FROM node:16-alpine
RUN apk add libc6-compat
  1. 複製所有檔案至映像檔的檔案系統內
FROM node:16-alpine
RUN apk add libc6-compat
COPY . .
  1. 使用 yarn install 這個指令安裝相關套件
FROM node:16-alpine
RUN apk add libc6-compat
COPY . .
RUN yarn install
  1. 打開 port 3000
FROM node:16-alpine
RUN apk add libc6-compat
COPY . .
RUN yarn install
EXPOSE 3000
  1. 加入 yarn dev -p 3000 的初始指令
FROM node:16-alpine
RUN apk add libc6-compat
COPY . .
RUN yarn install
EXPOSE 3000
CMD [ "yarn", "dev", "-p", "3000" ]

以上就是依照步驟而完成的 Dockerfile,可以在利用之前章節提過的變動率來移動這個 Dockerfile 的執行順序,變成以下的順序:

FROM node:16-alpine
EXPOSE 3000
CMD [ "yarn", "dev", "-p", "3000" ]
RUN apk add libc6-compat
COPY . .
RUN yarn install
  1. 使用 docker image build 的指令建置映像檔,這裡使用 docker image build --tag whatever . ,將映像檔暫時命名為 whatever。
$ docker image build --tag whatever .
[+] Building 120.6s (10/10) FINISHED
=> [internal] load build definition from Dockerfile 0.1s
=> => transferring dockerfile: 160B 0.0s
=> [internal] load .dockerignore 0.2s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/node:16-alpine 3.2s
=> [auth] library/node:pull token for registry-1.docker.io 0.3s
=> CACHED [1/4] FROM docker.io/library/node:16-alpine@sha256:2c..... 0.0s
=> [internal] load build context 0.8s
=> => transferring context: 196.06kB 0.8s
=> [2/4] RUN apk add libc6-compat 2.4s
=> [3/4] COPY . . 0.1s
=> [4/4] RUN yarn install 82.6s
=> exporting to image 41.7s
=> => exporting layers 41.6s
=> => writing image sha256:sha256:f33b1b1615f340532a61525749c.... 0.0s
=> => naming to docker.io/library/whatever
  1. 使用 docker container run 的指令確定映像檔的可運行性,這裡使用 docker container run --publish 3000:3000 去對應到我們設定打開的 port
$ docker container run --publish 3000:3000 whatever
yarn run v1.22.19
$ next dev -p 3000
ready - started server on 0.0.0.0:3000, url: http://localhost:3000
info - SWC minify release candidate enabled. https://nextjs.link/swcmin
event - compiled client and server successfully in 6.6s (178 modules)
wait - compiling...
event - compiled client and server successfully in 2.2s (178 modules)
Attention: Next.js now collects completely anonymous telemetry regarding usage.
This information is used to shape Next.js' roadmap and prioritize features.
You can learn more, including how to opt-out if you'd not like to participate in this anonymous program, by visiting the following URL:
https://nextjs.org/telemetry

wait - compiling /_error (client and server)...
wait - compiling / (client and server)...
event - compiled client and server successfully in 2.6s (182 modules)
warn - Fast Refresh had to perform a full reload. Read more: https://nextjs.org/docs/basic-features/fast-refresh#how-it-works
  1. 輸入網址 http://localhost:3000 會看到『 恭喜你成功打包成映像檔,並運行成容器!』
  2. 重新替映像檔貼上可以上傳到 DockerHub 的標籤,並上傳至自己的 DockerHub。
  3. 以我個人來說,就會把標籤換成 robeeerto 開頭,這樣才可以上傳至我的 DockerHub。
$ docker image tag whatever robeeerto/whatever:latest
# 沒反應是正常的

$ docker image push robeeerto/whatever:latest
9bd16c17a62b: Pushed
316042a1553e: Pushed
65573d05bfcd: Pushed
f1ed0bba6314: Mounted from library/node
2808ff9120f2: Mounted from library/node
cb6eda6d73f0: Mounted from library/node
994393dc58e7: Pushed
latest: digest: sha256:adcb3323755b080f1cef... size: 1792
  1. 刪掉本地端的映像檔,首先我們把本地的映像檔刪掉,以解答的例子來說,我們要刪掉 whatever 以及 robeeerto/whatever 這兩個映像檔。
$ docker image rm whatever robeeerto/whatever
Untagged: whatever:latest
Untagged: robeeerto/whatever:latest
Untagged: robeeerto/whatever@sha256:adcb3323755b080f1cefc8.......
Deleted: sha256:f33b1b1615f340532a61525749c90cfdcaee67417.....
  1. 使用 docker container run 的方式從 DockerHub 使用取用自製的映像檔並運行成容器。

以解答的例子是使用 robeeerto/whatever 這個映像檔作為啟動容器的說明書,因為上傳至 DockerHub 的關係,即時本地端沒有這個映像檔,Docker 也會主動幫我們從 DockerHub 上拉下來並建立容器。

$ docker container run --publish 3000:3000 robeeerto/whatever:latest
Unable to find image 'robeeerto/whatever:latest' locally
latest: Pulling from robeeerto/whatever
213ec9aee27d: Already exists
864b973d1bf1: Already exists
80fe61ad56f5: Already exists
e3887ab559e6: Already exists
17e084a3f122: Already exists
330558c46229: Already exists
5ff3f9684e51: Already exists
Digest: sha256:adcb3323755b080f1cefc832b25583673e96d287e54baa5b6fad2081f905ea93
Status: Downloaded newer image for robeeerto/whatever:latest
yarn run v1.22.19
$ next dev -p 3000
ready - started server on 0.0.0.0:3000, url: http://localhost:3000
info - SWC minify release candidate enabled. https://nextjs.link/swcmin
event - compiled client and server successfully in 7.1s (178 modules)
wait - compiling...
event - compiled client and server successfully in 2.3s (178 modules)
Attention: Next.js now collects completely anonymous telemetry regarding usage.
This information is used to shape Next.js' roadmap and prioritize features.
You can learn more, including how to opt-out if you'd not like to participate in this anonymous program, by visiting the following URL:
https://nextjs.org/telemetry

接著就算打開 http://localhost:3000 一樣會看到成功的畫面!