Dockerfile 指令:建立 Docker 映像檔

Dockerfile 是一個純文字檔案,裡面包含了一系列指令,告訴 Docker 引擎如何一步步建置出一個映像檔。你可以把它想像成一份「自動化食譜」。

掌握 Dockerfile 的指令,才能建置出穩定、高效且安全的容器環境。

核心指令

FROM:指定基礎映像檔

所有的 Dockerfile 都必須以 FROM 開頭。

FROM python:3.11-slim

WORKDIR:設定工作目錄

類似於 Linux 的 cd 指令。後續的指令(如 RUN, CMD)都會在這個目錄下執行。如果目錄不存在,Docker 會自動建立。

WORKDIR /app

COPY 與 ADD:複製檔案

  • COPY (推薦):將主機的檔案或目錄複製到容器內。
  • ADD:功能更強大但較不透明。它支援從 URL 下載,或自動解壓縮 .tar 檔案。通常建議使用 COPY,除非你真的需要自動解壓功能。
COPY requirements.txt .
COPY . .

RUN:建置時執行的指令

用於安裝套件、編譯程式碼等。每一條 RUN 指令都會產生一個新的映像檔層。

# 建議將多個指令合併,減少層數
RUN apt-get update && apt-get install -y \
    curl \
    git \
    && rm -rf /var/lib/apt/lists/*

ENV 與 ARG:變數管理

  • ENV:環境變數。在「建置時」與「容器運作時」都有效。常用於設定程式碼的路徑、埠號或 API 金鑰。
  • ARG:建置參數。只在「建置時」有效,容器啟動後就看不到了。常用於下載套件的版本號碼。

變數引用與預設值

在 Dockerfile 中引用變數時,建議使用 ${變數名稱} 的語法。此外,你還可以設定預設值:

  • ${VAR}:直接引用變數。
  • ${VAR:-default}:如果 VAR 未設定,則使用 default
# 使用 ARG 定義版本,並在 FROM 中引用
ARG PY_VERSION=3.11
FROM python:${PY_VERSION}-slim

# 使用 ENV 設定環境變數
ENV APP_PORT=8000
# 在後續指令中引用埠號
EXPOSE ${APP_PORT}

LABEL 與 VOLUME:註記與掛載

  • LABEL:為映像檔加上中繼資料(Metadata),如作者、版本或描述。
  • VOLUME:資料持久化。Docker 容器內的檔案預設是隨容器刪除即消失的,VOLUME 就像是在容器內挖一個「對外窗口」,聲明某個目錄(如資料庫儲存點)的資料應該保存在容器之外,避免資料遺失。
LABEL maintainer="mike@example.com"
LABEL version="1.0"

# 聲明 /data 為掛載點,適合放資料庫或日誌檔案
VOLUME ["/data"]

當你在 Dockerfile 寫下這行指令時,會發生以下三件事:

  • 建立掛載點:在容器內部建立 /data 這個目錄。
  • 繞過聯合法映象分層(Union FS):通常容器的資料是寫在「可寫層」上,但被宣告為 VOLUME 的目錄會繞過這個機制,直接寫入宿主機(Host)的硬碟。
  • 自動建立匿名磁碟卷(Anonymous Volume):如果你在啟動容器(docker run)時沒有手動用 -v 指定掛載位置,Docker 會自動在宿主機的 /var/lib/docker/volumes/ 下生成一個隨機 ID 的資料夾來存放這些資料。

關鍵差異:CMD vs. ENTRYPOINT

這是新手最容易混淆的地方。

  • CMD:容器啟動時的「預設」指令。如果你在執行 docker run 時後面帶了其他指令,CMD 就會被覆蓋。
  • ENTRYPOINT:容器啟動時的「主要」執行檔。通常不會被覆蓋,執行 docker run 時帶的參數會被當作 ENTRYPOINT 的參數傳進去。

最佳實踐建議:

# 使用 ENTRYPOINT 指定執行檔,CMD 指定預設參數
ENTRYPOINT ["python", "app.py"]
CMD ["--port", "8080"]

優化與安全性指令

.dockerignore 檔案

這不是 Dockerfile 內部的指令,但它極為重要。在建置映像檔時,Docker 會將指令目錄下的檔案發送給守護進程 (Daemon)。

在專案根目錄建立 .dockerignore,排除掉不需要打包進映像檔的檔案,可以:

  1. 縮小映像檔體積:不打包 node_modulesvenv
  2. 保護隱私:不打包 .env 或密鑰。
  3. 加快建置速度:減少發送給守護進程的資料量。

一個典型的 .dockerignore 範例:

.git
.env
node_modules/
venv/
*.log
__pycache__/
Dockerfile
.dockerignore

.dockerignore 的作用非常類似於 Git 中的 .gitignore。簡單來說,它告訴 Docker:「在建置映像檔(Build Image)時,哪些檔案或目錄應該被忽略,不要傳送給 Docker Daemon。」

當你執行 docker build . 時,指令最後那個 . 代表目前的目錄。Docker CLI 會把這個目錄下的所有檔案打包,發送給 Docker Daemon(負責跑建置的引擎)。

如果你有一個 1GB 的 node_modules 或大型資料庫備份檔在目錄裡,即便你的 Dockerfile 沒用到它們,Docker 還是會先把這 1GB 傳給引擎,這就是為什麼有時候畫面會卡在 Sending build context to Docker daemon 很久的原因。

USER:切換使用者

預設情況下,Docker 以 root 使用者執行容器。這有安全風險。

# 建立一個非 root 使用者並切換過去
RUN useradd -m myuser
USER myuser

EXPOSE:聲明埠號

這僅僅是個「聲明」,告訴使用者這個容器預計會監聽哪個埠號。它並不會自動開啟埠號映射,還是需要 docker run -p

EXPOSE 8080

實戰範例:完整的 Dockerfile

這是一個基於 Node.js 的現代化專案範例,結合了上述多個指令與最佳實踐:

# 第一階段:定義版本變數
ARG NODE_VERSION=18

# 使用官方輕量化映像檔
FROM node:${NODE_VERSION}-alpine

# 加上中繼資訊
LABEL org.opencontainers.image.authors="Fooish Editor"

# 設定容器內的工作目錄
WORKDIR /app

# 善用快取:先複製 dependency 定義,再安裝
COPY package*.json ./
RUN npm install --production

# 複製其餘程式碼
COPY . .

# 設定非 root 使用者以提高安全性
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

# 設定預設環境變數
ENV PORT=3000
ENV NODE_ENV=production

# 宣告埠號
EXPOSE ${PORT}

# 啟動命令
CMD ["npm", "start"]

建置並執行容器

當你準備好 Dockerfile.dockerignore 後,就可以開始動手建置了。

docker build:建置映像檔

使用 build 指令將 Dockerfile 轉換為映像檔:

# -t:給映像檔取名與標籤 (name:tag)
# . :代表 Dockerfile 所在的內容目錄
docker build -t my-web-app:1.0 .
如果你的 Dockerfile 檔名不是 Dockerfile(例如 Dockerfile.dev),可以使用 -f 參數指定:
docker build -t my-web-app:dev -f Dockerfile.dev .

docker run:啟動容器

建置完成後,使用 run 指令將映像檔跑起來:

# -d:背景執行 (detached mode)
# -p:埠號映射 (主機埠號:容器埠號)
# --name:指定容器名稱
docker run -d \
  -p 8080:3000 \
  --name my-running-app \
  my-web-app:1.0

啟動後,你就可以在瀏覽器輸入 http://localhost:8080 來造訪你的應用程式了!