# Docker 學習筆記 (四) — 如何撰寫 Dockerfile

# Docker 的問世,翻轉了人們對用環境部署與開發的習慣,學會 Docker 絕對是一項不虧本的投資

上一篇 我們已經講述了 Docker 常用指令的使用方式。

本篇我們要來討論一下 Docker 創建 Image 的另一個方法: 使用 Dockerfile

Dockerfile 是一個以 YAML 語法格式撰寫的純文字檔案。

用以描述 Docker 產生鏡像檔所需要的內容、以及要做的事情。

Docker 預設會找尋目錄中的 “Dockerfile” 這個名字的檔案作為要產生 Image 的內容,因此不建議自行命名 (當然你要建立自己命名的 Dockerfile 也行,只是在 docker build 時就要指定它要讀哪個檔案)。

詳細的 Dockerfile 文件撰寫規範可以參考 Docker 官方的 Dockerfile Document

本篇將以一個範例作為 Dockerfile 的講解。

### Dockerfile
# Use an official Python runtime as a parent image
FROM python:3.7.3-stretch
# Set the working directory to /app
WORKDIR /app
# Install any needed packages specified in requirements.txt
COPY requirements.txt ./
COPY gunicorn.py ./
RUN pip install --trusted-host pypi.python.org -r requirements.txt \
    && python3 -O -m compileall -b ./app \
    && find ./app -name "*.py"|xargs rm -rf \
    && python3 -O -m compileall -b ./config.py \
    && rm ./config.py
# Make port 5000 available to the world outside this container
EXPOSE 5000
# Define environment variable
ENV NAME World
# Run app.py when the container launches
CMD ["gunicorn", "-c", "gunicorn.py", "run:app"]

以上的 Dockerfile 範例描述了我們在 Build Image 時要做哪些事情。

  1. 使用 python-3.7.3-stretch 作為基底鏡像
  2. 設置檔案當前目錄為 app
  3. 將專案的 requirement.txt, gunicorn.py 複製到 docker 內。
  4. 執行 pip install
  5. 宣告開放 5000 PORT。
  6. 設置環境變數。
  7. 設定 Container 啟動時要執行的指令。

以下我們將逐行講解這些指令的詳細用法。

# #:使用 # 代表註解

文件可以使用 # 來進行註解。

# FROM

以哪個 Image 為基底進行改良

格式: FROM <image name>FROM <image name>:<tag>

一般來說我們創建一個自己的 Image,都是希望 Docker 環境能夠直接執行專案。

我們不希望從架設環境開始創建 Docker Image,因此大多時候,我們會直接從 DockerHub 找尋已經架好環境的 Image 作為基底,創建自己的 Image。

From 關鍵字會先從本機的 Docker image 中找尋符合的 Image,若找不到則從 DockerHub 上下載。

以我們這邊的例子便是以 Python:3.7.3-stretch 作為基底 Image 來建置我們自己的 Image。

# WORKDIR

設定當前工作目錄。

格式: WORKDIR <path>

使用了 WORKDIR Docker 會將指定的目錄設定為當前的工作目錄。之後使用到的各項指令諸如:RUN、CMD、ENTRYPOINT、COPY… 等,都會以此目錄為主。

另外, 如果此目錄並不存在,WORKDIR 會自動幫你創立

# RUN

執行命令。

格式: RUN <command>

RUN 用來執行 Shell 指令,例如本文範例便是使用 RUN 調用 pip 安裝 python 的相關套件。

這邊有一點要值得注意的是, Dockerfile 中的每一個指令都是啟動一個 container 、執行命令、之後以 Commit 的方式提交修改,直到完成整個 Image。

很多剛接觸 Dockerfile 的使用者,會將 RUN 當成一般的 Shell 腳本撰寫,這樣的理解就有可能踩到以下這個例子的坑。

RUN cd /app
RUN echo "hello" > world.txt

這種寫法,在 build Dockerfile 時,會輸出「 找不到 /app/world.txt 」的錯誤或是 world.txt 的內容不是 hello ,其原因便是因為 兩條指令存在於不同的 container ,兩者互不影響。

# COPY

複製「來源文件 \ 目錄」到的 Image 中的「文件 \ 目錄」中。

格式: COPY [--chown=<user>:<group>] <source path>... <dist path>

dist path 可以是 Container 內的絕對路徑,也可以是相對於 WORKDIR 的相對路徑。與 WORKDIR 一樣,若是當下 Image 中沒有此目錄,則複製文件前 Docker 會自動建立缺少的目錄。

另外,使用 COPY 會保留來源數據的各種屬性,諸如: 讀寫權限執行權限文件變更時間 … 等等。若希望更改文件的所屬 user 或是 group,可以在 COPY 時加入 --chown=<user>:<group> 來自訂。

補充:

與此指令相仿的還有 ADDADD 可以當作是 COPY 的強化版。

格式: ADD [--chown=<user>:<group>] <source path>... <dist path>

ADD 允許我們的 source path 是一個 URL。

此時, Docker 將會自動去網路上下載這個 URL 的文件到 dist path 中,並且文件權限自動設置為 600

另外,如果下載的是一個壓縮檔, ADD 會自動將其 解壓縮

儘管 ADD 是如此方便,但也因為其過多的預設功能使其在 Docker 的官方文檔中推薦盡可能使用 COPY 而非 ADD 。除非在需要自動解壓縮檔案的場合才使用。

# CMD

CMD 指令與 RUN 相似,用於指定 Container 的啟動時首要執行的命令。

格式: CMD <command>CMD [“command name”, argv1, argv2, …]

以我們的範例來說,便是當 Container 啟動時,便會先行執行 gunicorn -c gunicorn.py run:app 這行 shell script。

值得一提的是,CMD 可以在 RUN 一個 Container 時,以 -it <其他指令> 來取代掉。

例如:若我們今天以 docker run -it test_container bash 啟動 Container 的話,我們將會直接進入到 bash 而非執行 gunicorn

# ENV

設置環境變數

格式: ENV <key> <value> 或是 ENV <key1>=<value1> <key2>=<value2> ...

若專案中有需要使用到環境變數,則可以使用此指令設置環境變數。

# EXPOSE

開放 PORT。

格式: Expose <PORT1> <PORT2> …

EXPOSE 指令是宣告 Container 運行時對外可以使用哪些 PORT。

這只是一個宣告,在運行時並不會因為這個宣告就讓專案開啟這些 PORT 的服務,一切還是以專案怎麼撰寫為主。

要搞清楚的是,在運行 Container 時,所使用的 -p <host port>:<container port> 是映射 host 的 port 與 container 的 port,而 EXPOSE 僅是宣告 Container 打算使用什麼 PORT 。並不會自動映射到 host 的 port。

# 結語

本節中,我們以一個範例講述的 Dockerfile 如何撰寫。

並且講述了每個指令的意義與使用方法。

若是需要更加深入的撰寫教學,可以查看官網、或是此文隨附的參考目錄。

應該可以對於 Dockerfile 有更深入的理解。

下一篇我們將 以一個小範例作為 Docker 操作的實戰講解

# 參考條目

  • Docker — 从入门到实践
  • Docker 教程
  • Dockerfile reference

Like z20240z's work