跳至主要内容

4.9 Dockerfile 內容解析

終於進入撰寫 Dockerfile 的章節了,Dockerfile 就是拿來執行容器時的說明書,也建置映像檔的步驟,我們使用 robeeerto/whoami 這個前面不斷使用的映像檔來做說明,詳細的檔案放在本書的 GitHub 儲存庫中 ch-04 的 build-image-example,需要的讀者們可以自己去下載。

這個應用程式本身是使用 Ruby 這個程式語言編寫,但就算完全不懂 Ruby 也沒關係,重點是 Dockerfile 上。

FROM ruby:3.1.2-alpine
ENV AUTHOR=robertchang

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"]

從最上面的指令開始一步一步地解說:

FROM

每一個映像檔都必須以其他的映像檔作為基底,這邊因為要執行的是 Ruby 所撰寫的應用程式,故選用了 ruby:3.1.2-alpine 這個映像檔作為基底。

反之,若你的應用程式是 PHP 所撰寫,則會選用 php:zts-alpine,若是使用現在很紅的前端框架 React & Vue 則會使用 node:alpine3.16。

至於標籤的選擇則取決於你在開發這個應用程式時所需要的版本限制,這就是自己在撰寫時需要衡量的部分,並沒有一定的公式可以套用,但是使用 FROM 作為 Dockerfile 的起手式是一件一定會做的事情。

ENV

此指令用來設定運行成容器後的環境變數,以 key=value 的方式設定。

用上方的例子來說,運行成容器後,作業系統中就存在一個 $AUTHOR 的環境變數,且值為 robertchang。

也可以看到 nginx 官方 Dockerfile 中的環境變數,寫著 ENV NGINX_VERSION=1.23.1,具備實驗精神的我們,當然要進入容器之中,並且呼叫這個環境變數來看看!

$ docker container run --interactive --tty nginx bash
root@16032243d8e3:/# echo $NGINX_VERSION
1.23.1
root@16032243d8e3:/#

所以可以看到,設置好的環境變數,會在映像檔運行成容器後存在檔案系統之中。

RUN

終端機所執行的指令,以範例中的 apk add --update --no-cache build-base curl 為例,就是希望在接下來容器的環境中安裝 build-base curl 這兩個工具,指令並不侷限在安裝工具,也可以是 Linux 系統常見的改變權限 chown 或是 add group 等等。

只要是能夠被該作業系統所接受的指令都可以寫在 RUN 裡面,能夠被作業系統所接受的意思是指,當今天使用的是 alpine 這個作業系統時,就要使用 apk 這個套件管理工具,而若是使用 Ubuntu 時,則會改用 apt 這個套件管理工具,舉一反三,用 macOS 則是使用 brew 這個工具。

為什麼 RUN 指令後面會有 \ 這個符號呢?

這就得回到 4-5 映像檔快取的秘密 中提到映像檔是由映像層一層一層堆疊出來的,每一個指令都代表了一個新的映像層,FROM 是一層,RUN 也是一層,但如果安裝 build-base 以及 curl 如下面所示分開執行的話:

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

這樣會導致映像檔的映像層變多,畢竟從一個映像層轉為兩個映像層,而在軟體工業中,要的就是又小又快,這樣不必要的浪費是不被允許的。

為什麼 RUN 指令中間有 && 這個符號呢?

這個符號在程式語言的世界中很常見,代表的是若前面的指令執行結果沒有出錯,則接著執行後方的指令,那為什麼要用 && 來串接指令呢?

其實就和反斜線符號的道理是一樣的,希望可以把指令濃縮到一個映像層之中,進而降低映像檔本身的層數。

WORKDIR

這個指令是建立一個資料夾,並且以這個資料夾作為預設的工作目錄;會有人問,那這個和我直接執行 RUN mkdir app 有什麼樣子的區別呢?

區別在於 RUN mkdir app 確實會建立一個 app 的資料夾,但預設的工作目錄還是在根目錄,但以 WORKDIR /app 來說,做的是兩件事,第一件就是 mkdir app 建立 app 資料夾,並且 cd app 進入這個資料夾內,緊接著 Dockerfile 內排在 WORKDIR 後面的指令都是在 /app 這個資料夾內執行。

COPY

從本機的檔案系統中複製資料到容器內的檔案系統,這邊的例子是 COPY . . ,點的符號代表的是此處的意思。

所以這整個指令翻譯成人話就是從 Dockerfile 身處的資料夾複製所有的檔案到容器內部檔案系統的當前工作目錄

用 . . 的方式一開始確實很容易混肴,這邊舉一個簡單的例子,下方是假設的檔案目錄,只有 Dockerfile 以及一個 txt 檔:

.
├── Dockerfile
├── example.txt

則可以寫成:

COPY example.txt example.txt
# 左邊為本機的檔案名稱,右邊則為運行成容器後的檔案名稱

若是不希望他在容器內叫做 example.txt,而是叫做 happy.txt,我們也可以這樣寫:

COPY example.txt happy.txt

重要的是檔案的內容,而不是檔案的名稱。

EXPOSE

這就是運行成容器後預設打開的 port,也是為什麼我們在剛開始學習使用容器時都不會去更動右邊的 port 的原因,是因為這個數值已經在撰寫映像檔的階段就做好了設定。

上網查看 nginx 的 Dockerfile 中就有寫到 EXPOSE 80,意味著這個映像檔執行成容器後,就預設打開了 port 80,其他的都是關閉的狀態,所以就算想要強制對應到容器的其他 port 也是沒辦法的事。

CMD

此指令是 2.2 一探究竟容器內部 中有提過的啟動指令,也就是在映像檔運行成為容器時所執行的第一個指令,也關係到容器是否進入退出狀態的指令。

以本書的範例來說,使用了 ruby 這個動詞來執行應用程式;反之,若是應用程式是用 node.js 撰寫的,就會用 node 作為動詞來執行應用程式,若是使用 golang 撰寫的,則會用 go run 當作動詞。

當然 CMD 這個指令並沒有強迫一定要執行某個應用程式,它也可以是一段 Linux 的指令,例如 ls、pwd 等等,端看你這個 Dockerfile 所要執行的目的以及功能是什麼。