Docker 是一個容器化平台,能夠協助開發人員在不同環境中輕鬆部署和執行應用程式。 其中,Dockerfile 是一個關鍵的工具,它是一個文本檔案,包含一系列指令,用於定義 Docker 映像檔的建立流程。 Dockerfile 包括了從基本映像檔開始,逐步添加應用程式、相依套件、環境變數及相關設定,使您能夠建立一個高度客製化的容器環境,同時確保應用程式在不同的環境中能保持一致性。

Dockerfile 的基本結構

一個簡單的 Dockerfile 通常包含了以下結構:

            
                # 使用一個映像檔作為基礎
                FROM base_image

                # 作者資訊
                LABEL maintainer="email"

                # 在容器中安裝相依套件
                RUN apt-get update \
                && apt-get install -y torch torchvision torchaudio \
                && apt-get clean

                # 複製程式碼到容器中
                COPY app /app

                # 定義容器運行時的命令
                CMD python server.py
            
        

請注意,如果您再 dockerfile 安裝套件時,發現找不到要安裝的套件,可以先執行 apt-get update 更新一下套件資訊再安裝。

Dockerfile 常用指令

本節介紹常用的 Dockerfile 指令,並且過濾不建議使用或不常使用的指令,如需了解完整的指令說明,請參考下方官方鏈結。

FROM

指定基礎映像檔。

格式:

                
                    FROM [--platform=<platform>] <image> [AS <name>]
                    FROM [--platform=<platform>] <image>[:<tag>] [AS <name>]
                    FROM [--platform=<platform>] <image>[@<digest>] [AS <name>]
                
            

LABEL

加入 meta 資料,通常是一些映像檔資訊,與建構映像檔沒有什麼太大關係。

格式:

                
                    LABEL <key>=<value> <key>=<value> <key>=<value> ...
                
            

範例:

                
                    # 請務必使用雙引號,不要使用單引號
                    LABEL version="1.0" description="test"
                
            

RUN

在 docker build 時執行的指令。

格式:

                
                    # Shell Form
                    RUN <command>

                    # Exec Form (官方推薦)
                    RUN ["executable", "param1", "param2"]
                
            

範例:

                
                    # 以上兩個範例執行同樣的任務
                    RUN ["wget", "https://domain.com/file/"]
                    RUN wget https://domain.com/file/
                
            

CMD

在 docker run 時執行的指令,在 Dockerfile 檔案中只能有一個 CMD 指令,如果有多個 CMD 指令,僅最後一個會被執行。

格式:

                
                    # Shell Form
                    CMD command param1 param2 (shell form)

                    # Exec Form (官方推薦)
                    CMD ["executable","param1","param2"]

                    # 作為 ENTRYPOINT 的預設參數
                    CMD ["param1","param2"]
                
            

範例:

                
                    # 以上兩個範例執行同樣的任務
                    CMD ["python", "server.py"]
                    CMD python server.py
                
            

ENTRYPOINT

在 docker run 時執行的指令,與 CMD 類似,通常兩者會同時使用讓容器傳遞參數。

格式:

                
                    # Shell Form
                    ENTRYPOINT command param1 param2

                    # Exec Form (官方推薦)
                    ENTRYPOINT ["executable", "param1", "param2"]
                
            

範例:

                
                    # Dockerfile
                    ENTRYPOINT ["ls"]
                    CMD ["-a"]

                    # [正常執行]
                    # 執行指令: docker run image
                    # 組合後的指令:ls -a

                    # [取代 CMD]
                    # 執行指令: docker run image -al
                    # 組合後的指令:ls -al
                    # 將完全取代 CMD 的內容

                    # [取代 ENTRYPOINT]
                    # 執行指令: docker run --entrypoint ps image
                    # 組合後的指令:ps
                    # 將完全取代 ENTRYPOINT 的內容

                    # [取代 ENTRYPOINT 及 CMD]
                    # 執行指令: docker run --entrypoint ps image aux
                    # 組合後的指令:ps aux
                    # 將完全取代 ENTRYPOINT 及 CMD 的內容
                
            

請注意使用 --entrypoint 設定,CMD 的參數就會消失。 此外,--entrypoint 後面只能接單一指令,不能有參數,但 CMD 後面可以接多個參數,請參考下方說明:

                
                    # 正確
                    docker run --entrypoint ps image
                    docker run --entrypoint="ps" image

                    # 錯誤
                    docker run --entrypoint ps aux image
                    docker run --entrypoint "ps aux" image
                    docker run --entrypoint="ps aux" image

                    # 正確
                    docker run image -a -l

                    # 錯誤
                    docker run image "-a -l"
                
            

EXPOSE

指定在容器內監聽的 Port。

格式:

                
                    EXPOSE <port> [<port>/<protocol>...]
                
            

範例:

                
                    EXPOSE 80/tcp 443/tcp
                
            

在 docker run 時,可以使用 -p 綁定對應的 Port,如下所示:

                
                    # 表示電腦的 8080 Port 對應到容器的 80 Port
                    docker run -p 8080:80 image

                    # 自動使用與容器相同的 Port 對應
                    docker run -P image
                
            

VOLUME

指定在容器中要掛載的目錄。

格式:

                
                    VOLUME ["folder1","folder2"]
                
            

範例:

                
                    VOLUME ["/data","/tmp"]
                
            

在 docker run 時,可以使用 -v 對應,如下所示:

                
                    # 表示電腦的 /root/data 目錄對應到容器 /data 目錄
                    # rw 表示權限可讀可寫
                    docker run -v /root/data:/data:rw  image
                
            

COPY

將文件或目錄複製到映像檔中,與 ADD 指令類似,但 ADD 指令除了可以複製檔案,還可以下載遠端資料至映像檔中。 不過官方不建議使用 ADD 指令,如果您要複製檔案請用 COPY 指令,如果你要下載遠端資料請用 RUN wget URL。

格式:

                
                    COPY [--chown=<user>:<group>] [--chmod=<perms>] <src>... <dest>
                    COPY [--chown=<user>:<group>] [--chmod=<perms>] ["<src>",... "<dest>"]
                
            

範例:

                
                    # 將電腦 /src 目錄中檔案,複製到容器根目錄
                    COPY /src /
                
            

ENV

設定環境變數,該變數在後續 Dockerfile 及容器中都有效。

格式:

                
                    ENV <key>=<value> ...
                
            

範例:

                
                    ENV CUDA_VERSION="12.2" NVIDIA_DRIVER="nvidia-driver-535"

                    # 使用環境變數
                    RUN apt-get update && apt-get install $NVIDIA_DRIVER -y

                    # 持久性的環境變數有時會產生意想不到的問題
                    # 如果僅在 RUN 過程所需的環境變數,可以參考以下指令
                    # DEBIAN_FRONTEND 環境變數僅在這行指令有效
                    RUN DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -y ...
                
            

使用 Shell Form 指令會自動置換環境變數的值,但 Exec Form 指令則不會,請參考下方說明。

                
                    ENV HOME="/home"

                    # Shell Form 將置換 $HOME 變數
                    # 以下指令將輸出 /home
                    CMD echo $HOME

                    # Exec Form 將不會置換環境變數
                    # 以下指令將輸出 $HOME
                    CMD ["echo", "$HOME"]

                    # 讓 Exec Form 置換環境變數需執行 shell
                    # 這種模式與 Shell Form 同形式
                    # 以下指令將輸出 /home
                    CMD ["/bin/sh", "-c", "echo $HOME"]
                
            

ARG

定義在建構映像檔時所使用的變數,與 docker build --build-arg 搭配使用。

格式:

                
                    ARG <name>[=<default value>]
                
            

範例:

                
                    ARG HTTP_PROXY
                    ARG FTP_PROXY
                
            

在建構映像檔時,可以使用 --build-arg 傳遞參數。

                
                    docker build \
                    --build-arg HTTP_PROXY=http://127.0.0.1:1234 \
                    --build-arg FTP_PROXY=http://127.0.0.1:4567 \
                    image
                
            

WORKDIR

指定工作路徑,類似在 command line 的 cd 指令,如果在 Dockerfile 沒有指定 WORKDIR,則預設為 / 根目錄。

格式:

                
                    WORKDIR folder1
                
            

範例:

                
                    WORKDIR /

                    # WORKDIR 可以接續使用
                    # 以下指令最終輸出 /data/tmp
                    WORKDIR /
                    WORKDIR data
                    WORKDIR tmp
                    RUN pwd
                
            

Dockerfile 快取

Dockerfile 中的每個指令都會建立一個分層,且每層執行過後都會產生快取,當重新建構映像檔時,只要資料未經修改就能重複使用這些快取層。

舉了例子:

            
                FROM pytorch/pytorch:latest
                COPY . /
                RUN pip install --no-cache-dir -r requirements.txt
                CMD python api.py
            
        

使用以下指令建構映像檔。

            
                docker build . -t test_image
            
        

第一次建構
不會有任何問題,每一個指令都會順利執行並產生快取。

第二次建構
當我們修改了一些程式碼並重新建構時,由於第 2 行複製的檔案已經改變了,因此從第 2 行開始,包含後面的指令都需重新執行,無法使用快取。

這會有什麼問題?
由於安裝套件通常需要花費較久的時間,且我們只是修改了程式碼,並沒有異動相依套件,但缺需要重新安裝套件。

該如何改善這個問題呢?
您可以將 requirements.txt 與 python 程式碼分開複製,請參考下方範例,這樣再重新建構時,第 1 ~ 3 行就可以使用快取,也就不用重新安裝套件了。

            
                FROM pytorch/pytorch:latest
                COPY requirements.txt /
                RUN pip install --no-cache-dir -r requirements.txt
                COPY . /
                CMD python api.py
            
        

建構映像檔與執行容器

當 Dockerfile 編寫完成之後,可以使用以下指令建構映像檔。

            
                # 建構映像檔
                docker build .

                # 建構映像檔並指定映像檔名稱
                docker build . -t [指定映像檔名稱]

                # 建構映像檔並指定映像檔名稱,不使用快取
                docker build . --no-cache -t [指定映像檔名稱]
            
        

建構好映像檔之後,建議實際運行容器並檢查應用程式是否正確運行。

            
                docker run --name [自訂容器名稱] [映像檔名稱]
            
        

Dockerfile 注意事項

使用官方基本映像檔
盡量使用官方提供的映像檔作為開始,例如您的應用程式是用 Python 開發的,就使用官方提供的 Python 映像檔。 Docker Hub 針對官方映像檔,會標示 Docker Official Image 文字。
減少映像檔分層
Dockerfile 的每個指令都會建立一個分層,過多的分層會增加映像檔的大小和建構時間。 因此,盡量將多個指令組合成一個,並使用 && 鏈接多個指令,以減少層數。例如:
                    
                        # 建議做法
                        RUN apt-get update && apt-get install -y torch torchvision torchaudio && apt-get clean

                        # 不建議做法
                        RUN apt-get update
                        RUN apt-get install -y torch torchvision torchaudio
                        RUN apt-get clean
                    
                
使用 .dockerignore 文件
有時候您可能不希望某些檔案或目錄複製到容器中,例如:node_modules。 此時,您可以在程式碼根目錄中建立 .dockerignore 檔案,在檔案中條列不需要複製到容器中的檔案和目錄。 .dockerignore 的用法與 .gitignore 類似,參考下方說明:
                    
                        # 我是註解

                        # 排除下一層目錄中副檔名為 .tmp 的檔案
                        */*.tmp

                        # 排除下兩層目錄中副檔名為 .tmp 的檔案
                        */*/*.tmp

                        # 排除所有目錄中副檔名為 .tmp 的檔案
                        **/*.tmp

                        # 排除副檔名為 .md 的檔案
                        # README.md 除外 (也就是說 README.md 還是會被複製到容器中)
                        *.md
                        !README.md
                    
                
清理不需要的文件
在 Dockerfile 中,使用 RUN 安裝相依套件或編譯套件,可以在安裝完成後清理不需要的檔案,以減小映像檔的大小。