Website logo

Robert Chang

技術部落格

Docker - Dockerfile 的指令解析

終於要進入撰寫 Dockerfile 的階段了,Dockerfile 就是前面提過的 執行容器時的說明書,也是構築映像檔的步驟。

這邊使用 robeeerto/whoami 這個映像檔來做說明,這個應用程式本身是使用 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-alpin,若是使用現在很紅的前端框架 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-case curl 為例,就是希望在接下來容器的環境中安裝這兩個工具,並不侷限在安裝工具,也可以是 Linux 系統常見的改變權限 chown 或是 add group 等等,只要是能夠被該作業系統所接受的指令都可以寫在 RUN 裡面,能夠被作業系統所接受的意思是指,當今天使用的是 alpine 這個作業系統時,就要使用 apk 這個套件管理工具,若是使用 Ubuntu 時,則會改用 apt 這個套件管理工具,舉一反三,用 macOS 則是使用 brew 這個工具。

為什麼 RUN 這個指令後面會有 \ ( 反斜線 ) 這個符號呢?

這就要回到之前提過關於映像檔是由映像層一層一層堆疊出來的,每一個起始的指令都代表了一個新的映像層,FROM 是一層,RUN 也是一層,但如果安裝 build-base 以及 curl 如下面所示分開執行的話:

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

這樣造成映像檔的映像層數變多,畢竟從一個映像層轉為兩個映像層,就像你的千層蛋糕從 10 層變成 20 層一樣,我知道會比較好吃,但在軟體工業中,要的就是又小又快,這樣不必要的資源浪費是不被允許的。

為什麼 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:此指令是在容器的生命週期章節有提過的初始指令,也就是在映像檔運行成為容器時所執行的第一個指令,也關係到容器是否進入停止狀態的指令,以這個範例來說,我使用了 ruby 這個動詞來執行應用程式;反之,若是應用程式是用 node.js 撰寫的,就會用 node 作為動詞來執行應用程式,若是使用 golang 撰寫的,則會用 go run 當作動詞。

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

結語

今天完成了 Dockerfile 的指令解析,明天將根據 Dockerfile 的指令建置映像檔。

上一篇文章Docker - 映像檔的完全名稱以及儲存庫( Registry )

下一篇文章Docker - 建置映像檔