跳轉到

任務四:Dockerfile 建置映像檔

開始之前

任務目標

在這個任務中,你將學習:

  • 理解 Dockerfile 的用途與建置流程
  • 掌握基礎建置指令(FROM、RUN、COPY、ADD)
  • 掌握環境與路徑設定(WORKDIR、ENV、ARG)
  • 掌握執行與進階設定(EXPOSE、VOLUME、USER、HEALTHCHECK、LABEL)
  • 理解 CMD 與 ENTRYPOINT 的差異與搭配
  • 理解 Layer 機制與快取原理
  • 掌握建置最佳實踐(減少 layer、.dockerignore、Multi-stage build)
  • 學會將映像檔發布至 Docker Hub 或私有 Registry
  • 透過實作練習撰寫完整的 Dockerfile

Dockerfile 是什麼?

Dockerfile 是一個文字檔案,包含一系列指令,用來描述如何建置 Docker 映像檔。它就像是映像檔的「食譜」,讓 Docker 引擎能夠自動化地建置出一致的映像檔。

Dockerfile 的優勢:

  • 自動化建置:透過指令腳本化建置流程,無需手動操作
  • 版本控制:Dockerfile 可納入 Git,追蹤映像檔的變更歷史
  • 可重現性:任何人都能用同一份 Dockerfile 建置出相同的映像檔
  • 文件化:Dockerfile 本身就是映像檔的文件,清楚描述環境配置

基礎建置指令

FROM:指定基底映像檔

FROM 指令指定建置映像檔的基底(base image),這是 Dockerfile 的第一條指令(除了 ARG 可以在 FROM 之前)。

語法:

FROM <image>[:<tag>] [AS <name>]

範例:

# 使用官方 Python 3.12 映像檔
FROM python:3.12

# 使用特定版本的 Ubuntu
FROM ubuntu:22.04

# 使用 Alpine Linux(輕量化發行版)
FROM alpine:3.19

# Multi-stage build:為階段命名
FROM python:3.12 AS builder

最佳實踐:

  • 使用具體的標籤版本(如 python:3.12)而非 latest,確保建置結果一致
  • 考慮使用 Alpine 或 Slim 版本減少映像檔大小
  • 選擇官方或可信任來源的映像檔

RUN:執行命令並建立新 Layer

RUN 指令在映像檔建置時執行命令,並將結果儲存為新的 layer。常用於安裝套件、建立目錄、設定環境等。

語法:

# Shell form(會透過 /bin/sh -c 執行)
RUN <command>

# Exec form(直接執行,不透過 shell)
RUN ["executable", "param1", "param2"]

範例:

# 安裝套件
RUN apt-get update && apt-get install -y \
    curl \
    vim \
    git

# 建立目錄
RUN mkdir -p /app/data

# 下載檔案
RUN curl -O https://example.com/file.tar.gz && \
    tar -xzf file.tar.gz && \
    rm file.tar.gz

# Exec form 範例
RUN ["/bin/bash", "-c", "echo hello"]

最佳實踐:

  • 合併多個指令減少 layer 數量(使用 && 串接)
  • 使用 \ 換行提升可讀性
  • 清理暫存檔案減少映像檔大小(如 apt-get cleanrm -rf /var/lib/apt/lists/*

COPY:複製檔案至映像檔

COPY 指令從建置環境(通常是 Dockerfile 所在目錄)複製檔案或目錄到映像檔內。

語法:

COPY [--chown=<user>:<group>] <src>... <dest>

範例:

# 複製單一檔案
COPY app.py /app/app.py

# 複製整個目錄
COPY ./src /app/src

# 複製多個檔案
COPY requirements.txt setup.py /app/

# 設定檔案擁有者
COPY --chown=1000:1000 config.yml /etc/config.yml

# 使用萬用字元
COPY *.py /app/

重要特性:

  • 來源路徑是相對於建置環境(context)的路徑
  • 目標路徑如果不存在會自動建立
  • 如果目標路徑以 / 結尾,會視為目錄

ADD:功能更豐富的複製指令

ADDCOPY 類似,但提供額外功能:自動解壓縮 tar 檔案、支援 URL 下載。

語法:

ADD [--chown=<user>:<group>] <src>... <dest>

範例:

# 複製並自動解壓縮
ADD archive.tar.gz /app/

# 從 URL 下載檔案
ADD https://example.com/file.tar.gz /tmp/

# 一般檔案複製(與 COPY 相同)
ADD config.json /etc/config.json

COPY vs ADD:

特性 COPY ADD
基本複製
自動解壓縮 tar
支援 URL
推薦使用 一般情況 需要解壓縮或下載時

最佳實踐:

  • 優先使用 COPY,因為功能單純、行為明確
  • 只有在需要自動解壓縮或下載時才使用 ADD
  • 不建議用 ADD 下載檔案,建議改用 RUN curlRUN wget,因為更容易控制錯誤處理

環境與路徑設定

WORKDIR:設定工作目錄

WORKDIR 指令設定後續指令的工作目錄,類似於 cd 指令,但會自動建立目錄(如果不存在)。

語法:

WORKDIR /path/to/workdir

範例:

WORKDIR /app

# 後續指令都在 /app 目錄下執行
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .

# 可以使用多次,路徑會累加
WORKDIR /app
WORKDIR src
# 現在在 /app/src

最佳實踐:

  • 使用絕對路徑避免混淆
  • 在複製檔案前先設定 WORKDIR
  • 避免使用 RUN cd /some/path,應改用 WORKDIR

ENV:設定環境變數

ENV 指令設定環境變數,這些變數在建置時和容器執行時都可用。

語法:

ENV <key>=<value> ...

範例:

# 設定單一變數
ENV NODE_ENV=production

# 設定多個變數
ENV APP_HOME=/app \
    APP_PORT=8000 \
    LOG_LEVEL=info

# 在後續指令中使用
ENV DATA_DIR=/data
RUN mkdir -p $DATA_DIR
WORKDIR $DATA_DIR

容器執行時的使用:

# 環境變數會自動帶入容器
docker run my-image

# 也可以在執行時覆蓋
docker run -e APP_PORT=9000 my-image

最佳實踐:

  • 使用大寫命名慣例(如 NODE_ENV
  • 敏感資訊(如密碼、金鑰)不要寫在 Dockerfile,應該用 secrets 或執行時傳入
  • 可以將常用路徑設為環境變數,方便後續參考

ARG:定義建置時期變數

ARG 定義建置時期的變數,只在建置映像檔時有效,容器執行時無法使用。

語法:

ARG <name>[=<default value>]

範例:

# 定義建置參數(有預設值)
ARG PYTHON_VERSION=3.12
FROM python:${PYTHON_VERSION}

# 定義建置參數(無預設值)
ARG BUILD_DATE
ARG GIT_COMMIT

# 在 RUN 指令中使用
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y nginx

# 將 ARG 轉換為 ENV(如果需要在容器執行時使用)
ARG APP_VERSION=1.0.0
ENV APP_VERSION=${APP_VERSION}

建置時傳入參數:

docker build --build-arg PYTHON_VERSION=3.11 -t my-image .
docker build --build-arg BUILD_DATE=$(date) --build-arg GIT_COMMIT=$(git rev-parse HEAD) -t my-image .

ARG vs ENV:

特性 ARG ENV
作用時期 僅建置時 建置時 + 執行時
可從外部傳入 --build-arg -e
在容器內可用
適用場景 建置階段配置 應用程式配置

最佳實踐:

  • 使用 ARG 讓 Dockerfile 更彈性(如支援多個 Python 版本)
  • ARG 可以放在 FROM 之前,用於指定基底映像檔版本
  • 敏感資訊仍不應透過 --build-arg 傳入,因為會保留在映像檔歷史中

執行與進階設定

EXPOSE:宣告容器監聽的連接埠

EXPOSE 指令宣告容器在執行時會監聽的網路連接埠。這是一種文件化的宣告,並不會實際發布連接埠。

語法:

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

範例:

# 宣告 HTTP 連接埠
EXPOSE 80

# 宣告多個連接埠
EXPOSE 8000 8001

# 指定協定(預設是 TCP)
EXPOSE 53/udp
EXPOSE 8080/tcp

# 同時宣告 TCP 和 UDP
EXPOSE 9000/tcp 9000/udp

實際發布連接埠:

# 使用 -p 發布連接埠
docker run -p 8080:80 my-image

# 使用 -P 自動發布所有 EXPOSE 的連接埠
docker run -P my-image

最佳實踐:

  • 即使 EXPOSE 不是強制的,仍建議宣告,讓使用者知道應用程式使用哪些連接埠
  • EXPOSE 視為文件,幫助維護者理解映像檔

VOLUME:建立掛載點

VOLUME 指令建立掛載點,用於持久化資料或與 Host 共享檔案。

語法:

VOLUME ["/data"]
VOLUME /var/log /var/db

範例:

# 單一 volume
VOLUME /var/lib/mysql

# 多個 volumes
VOLUME /app/logs /app/data

# JSON 格式
VOLUME ["/var/log", "/var/db"]

執行時使用:

# Docker 會自動建立匿名 volume
docker run my-image

# 使用具名 volume
docker run -v mydata:/var/lib/mysql my-image

# 綁定 Host 目錄
docker run -v /host/path:/container/path my-image

最佳實踐:

  • 用於存放需要持久化的資料(如資料庫、日誌)
  • 在 Dockerfile 中宣告 VOLUME 後,建置時寫入該路徑的檔案不會保留在最終映像檔中
  • 通常在執行時透過 -v 明確指定 volume 更有彈性

USER:切換執行使用者

USER 指令設定後續指令執行時使用的使用者(和使用者群組)。預設是 root,但基於安全性考量,建議切換為非特權使用者。

語法:

USER <user>[:<group>]
USER <UID>[:<GID>]

範例:

# 切換為 nobody 使用者
USER nobody

# 切換為特定 UID
USER 1000

# 建立使用者後切換
RUN groupadd -r appuser && useradd -r -g appuser appuser
USER appuser

# 指定使用者和群組
USER appuser:appgroup

安全性考量:

# 不佳的做法:使用 root 執行應用程式
FROM python:3.12
COPY . /app
CMD ["python", "app.py"]  #  root 執行,安全風險高

# 較佳的做法:建立並切換為非特權使用者
FROM python:3.12
RUN useradd -m -u 1000 appuser
COPY . /app
RUN chown -R appuser:appuser /app
USER appuser
CMD ["python", "app.py"]  #  appuser 執行,安全性較高

最佳實踐:

  • 生產環境應避免以 root 執行應用程式
  • USER 之前確保必要的檔案權限已設定(使用 chown
  • 可以使用 --chown 參數在 COPY 時直接設定擁有者

HEALTHCHECK:定義健康檢查

HEALTHCHECK 指令定義如何檢查容器的健康狀態。Docker 會定期執行檢查指令,根據結果判斷容器是否健康。

語法:

HEALTHCHECK [OPTIONS] CMD command
HEALTHCHECK NONE  # 停用繼承的健康檢查

選項:

  • --interval=<duration>:檢查間隔(預設 30s)
  • --timeout=<duration>:單次檢查逾時時間(預設 30s)
  • --start-period=<duration>:容器啟動的緩衝時間(預設 0s)
  • --retries=<number>:連續失敗幾次才判定為不健康(預設 3)

範例:

# HTTP 健康檢查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:8000/health || exit 1

# 使用 wget
HEALTHCHECK CMD wget --quiet --tries=1 --spider http://localhost:8080/ || exit 1

# 檢查特定程式
HEALTHCHECK CMD pgrep nginx || exit 1

# 停用健康檢查
HEALTHCHECK NONE

健康狀態:

  • starting:容器啟動中(在 start-period 期間)
  • healthy:檢查通過
  • unhealthy:連續檢查失敗達到 retries 次數

查看健康狀態:

docker ps  # STATUS 欄位會顯示健康狀態
docker inspect --format='{{.State.Health.Status}}' container_name

最佳實踐:

  • 健康檢查應該輕量、快速執行
  • 檢查應用程式的實際功能,而非只是程式存活(如 HTTP endpoint)
  • 適當設定 start-period,給應用程式足夠的啟動時間

LABEL:為映像檔加入中繼資料

LABEL 指令為映像檔加入 key-value 格式的中繼資料(metadata),用於標記版本、維護者、授權資訊等。

語法:

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

範例:

# 單一標籤
LABEL version="1.0.0"

# 多個標籤
LABEL maintainer="developer@example.com" \
      description="My awesome application" \
      version="1.0.0"

# 使用命名空間(推薦做法)
LABEL org.opencontainers.image.title="My App" \
      org.opencontainers.image.version="1.0.0" \
      org.opencontainers.image.authors="developer@example.com" \
      org.opencontainers.image.source="https://github.com/username/repo"

# 包含空格的值需用引號
LABEL description="This is a multi-word description"

查看標籤:

docker inspect my-image
docker inspect --format='{{json .Config.Labels}}' my-image | jq

常見的標籤慣例(OCI Image Spec):

  • org.opencontainers.image.title:映像檔標題
  • org.opencontainers.image.version:版本號
  • org.opencontainers.image.authors:作者
  • org.opencontainers.image.source:原始碼位置
  • org.opencontainers.image.licenses:授權資訊
  • org.opencontainers.image.created:建置時間

最佳實踐:

  • 遵循 OCI 標準的命名慣例
  • 使用 ARG 搭配 LABEL 在建置時注入動態資訊(如版本號、commit hash)
  • 標籤有助於管理大量映像檔,可用於自動化腳本過濾或清理

CMD vs ENTRYPOINT:容器啟動指令

CMDENTRYPOINT 都用於定義容器啟動時執行的指令,但行為不同。理解兩者差異是撰寫 Dockerfile 的重要關鍵。

CMD:設定預設執行指令

CMD 提供容器的預設執行指令。如果在 docker run 時指定了指令,CMD 會被覆蓋。

語法:

# Exec form(推薦,不透過 shell)
CMD ["executable", "param1", "param2"]

# Shell form(透過 /bin/sh -c 執行)
CMD command param1 param2

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

範例:

# Exec form
CMD ["python", "app.py"]
CMD ["nginx", "-g", "daemon off;"]

# Shell form
CMD python app.py
CMD echo "Hello, Docker!"

執行時覆蓋 CMD:

# 使用 Dockerfile 中的 CMD
docker run my-image

# 覆蓋 CMD
docker run my-image python other_script.py
docker run my-image bash

ENTRYPOINT:設定容器進入點

ENTRYPOINT 設定容器的主要執行程式。與 CMD 不同,ENTRYPOINT 不會被 docker run 的指令覆蓋(除非使用 --entrypoint)。

語法:

# Exec form(推薦)
ENTRYPOINT ["executable", "param1", "param2"]

# Shell form
ENTRYPOINT command param1 param2

範例:

# 將容器當作可執行檔使用
ENTRYPOINT ["python", "app.py"]

# 搭配 CMD 提供預設參數
ENTRYPOINT ["python"]
CMD ["app.py"]

執行時行為:

# docker run 的參數會附加到 ENTRYPOINT 後面
docker run my-image --debug  # 執行:python app.py --debug

# 使用 --entrypoint 覆蓋
docker run --entrypoint bash my-image

CMD 與 ENTRYPOINT 的差異比較

特性 CMD ENTRYPOINT
覆蓋方式 docker run 指令直接覆蓋 需使用 --entrypoint
主要用途 提供預設指令(可覆蓋) 定義容器的主要執行程式
適用場景 一般應用程式 工具類容器、固定執行邏輯

CMD 與 ENTRYPOINT 搭配使用

組合使用 ENTRYPOINTCMD 可以實現彈性配置:ENTRYPOINT 定義固定的執行程式,CMD 提供可覆蓋的預設參數。

範例一:應用程式容器

FROM python:3.12
WORKDIR /app
COPY app.py .

# ENTRYPOINT 固定執行 python
ENTRYPOINT ["python"]

# CMD 提供預設參數(腳本名稱)
CMD ["app.py"]

執行時:

# 使用預設參數,執行:python app.py
docker run my-image

# 覆蓋參數,執行:python other.py
docker run my-image other.py

# 傳入額外參數,執行:python app.py --verbose
docker run my-image app.py --verbose

範例二:工具類容器

FROM alpine:3.19
RUN apk add --no-cache curl

# 固定執行 curl
ENTRYPOINT ["curl"]

# 預設參數
CMD ["--help"]

執行時:

# 顯示 curl 說明
docker run my-curl

# 使用容器抓取網頁
docker run my-curl https://example.com

# 傳入 curl 參數
docker run my-curl -I https://example.com

範例三:啟動腳本

FROM python:3.12
WORKDIR /app
COPY app.py entrypoint.sh .
RUN chmod +x entrypoint.sh

# 使用啟動腳本
ENTRYPOINT ["./entrypoint.sh"]

# 預設執行應用程式
CMD ["python", "app.py"]

entrypoint.sh 內容:

#!/bin/bash
# 執行初始化任務
echo "Initializing..."
exec "$@"  # 執行 CMD 傳入的指令

選擇建議:

  • 只需要預設指令且允許完全覆蓋:單獨使用 CMD
  • 容器功能固定(如工具類):單獨使用 ENTRYPOINT
  • 需要固定執行邏輯 + 彈性參數ENTRYPOINT + CMD 組合

Layer 機制與快取原理

理解 Docker 的 Layer(層)機制與快取原理,能幫助你撰寫出建置更快、映像檔更小的 Dockerfile。

什麼是 Layer?

Docker 映像檔是由多個唯讀 layer 堆疊而成的。Dockerfile 中的每條指令(如 FROMRUNCOPY)都會產生一個新的 layer。

Layer 堆疊範例:

# Layer 1: Ubuntu 基底(可能不只一層,要看 base image 本身的 layer 數)
FROM ubuntu:22.04

# Layer 2: 更新套件清單
RUN apt-get update

# Layer 3: 安裝 Python
RUN apt-get install -y python3

# Layer 4: 複製應用程式
COPY app.py /app/

# Layer 5: 設定啟動指令
CMD ["python3", "/app/app.py"]

每個 layer 只儲存與上一層的差異,這種設計帶來幾個好處:

  • 節省空間:多個映像檔可以共享相同的 layer
  • 加速傳輸:pull/push 映像檔時只需傳輸缺少的 layer
  • 加速建置:利用快取重用未變更的 layer

查看映像檔 layers:

docker history my-image
docker inspect my-image

建置快取原理

Docker 在建置映像檔時會檢查每條指令:

  1. 如果指令與上次建置完全相同,且之前的 layer 仍存在,就重用該 layer(快取命中)
  2. 如果指令改變,或之前的快取失效,就重新執行指令並建立新 layer

快取行為範例:

第一次建置:

# 從遠端下載
FROM python:3.12

# 執行安裝
RUN pip install flask

# 複製檔案
COPY app.py /app/

第二次建置(只改了 app.py):

# ✓ 使用快取
FROM python:3.12

# ✓ 使用快取
RUN pip install flask

# ✗ 重新複製(app.py 改變)
COPY app.py /app/

關鍵觀念:一旦某個 layer 的快取失效,後續所有 layer 都必須重新建置。

快取失效的情況

指令本身改變

# 原本
RUN pip install flask

# 改為這樣指令改變,快取失效
RUN pip install flask django

COPY/ADD 的檔案內容改變

COPY requirements.txt /app/  # 如果 requirements.txt 內容改變,快取失效
RUN pip install -r /app/requirements.txt  # 前一層失效,這層也必須重建

Docker 會計算檔案的 checksum,只要內容改變(即使檔案時間戳相同)就會失效。

使用 --no-cache 參數

docker build --no-cache -t my-image .  # 強制重新建置所有 layer

基底映像檔更新

FROM python:3.12  # 如果遠端的 python:3.12 映像檔更新,快取會失效

善用快取加速建置

原則:將不常變動的指令放在前面,常變動的放在後面。

不佳的做法

FROM python:3.12
WORKDIR /app

# 先複製所有檔案(程式碼經常變動)
COPY . .

# 安裝相依套件(requirements.txt 較少變動)
RUN pip install -r requirements.txt

CMD ["python", "app.py"]

app.py 改變時,COPY . . 這層快取失效,連帶使 pip install 也必須重新執行(即使 requirements.txt 沒變)。

較佳的做法

FROM python:3.12
WORKDIR /app

# 先複製 requirements.txt(較少變動)
COPY requirements.txt .

# 安裝相依套件(可善用快取)
RUN pip install -r requirements.txt

# 最後才複製程式碼(經常變動)
COPY . .

CMD ["python", "app.py"]

這樣當 app.py 改變時,pip install 那層的快取仍然有效,可以節省大量建置時間。

原理總結:

  • 相依套件安裝通常很耗時,應優先執行並善用快取
  • 應用程式碼經常變動,應放在後面

建置最佳實踐

撰寫高效的 Dockerfile 不只是讓映像檔能用,還要關注建置速度、映像檔大小、安全性與可維護性。

減少 Layer 數量

每條 RUNCOPYADD 都會產生新 layer。雖然 Docker 的 layer 機制很高效,但過多 layer 仍會增加映像檔大小與複雜度。

合併 RUN 指令:

不佳的做法

RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y vim
RUN apt-get install -y git
RUN apt-get clean

較佳的做法

RUN apt-get update && apt-get install -y \
    curl \
    vim \
    git \
  && apt-get clean \
  && rm -rf /var/lib/apt/lists/*

重點:

  • 使用 && 串接指令,形成單一 layer
  • 使用 \ 換行提升可讀性
  • 在同一層清理暫存檔案(如 apt cache)

注意快取平衡點:

如果某個指令經常改變,考慮獨立成一層以善用快取:

# 系統套件較少變動,合併為一層
RUN apt-get update && apt-get install -y \
    curl \
    vim \
    git \
  && rm -rf /var/lib/apt/lists/*

# 應用程式相依套件可能經常變動,獨立為一層
RUN pip install -r requirements.txt

使用 .dockerignore 排除不需要的檔案

.dockerignore 類似 .gitignore,用於排除不需要複製進映像檔的檔案。

為什麼需要?

  • 減少建置 context 大小,加速上傳
  • 避免將敏感資訊(如 .env、金鑰檔案)複製進映像檔
  • 減少映像檔大小

.dockerignore 範例:

.dockerignore
# 版本控制
.git
.gitignore

# Python
__pycache__
*.pyc
*.pyo
*.pyd
.Python
env/
venv/
.venv/

# 測試與文件
tests/
docs/
*.md
!README.md

# IDE
.vscode/
.idea/
*.swp

# 日誌與暫存檔案
*.log
tmp/
.DS_Store

# 環境變數檔案
.env
.env.local

# Node.js
node_modules/
npm-debug.log

使用建議:

  • 在專案根目錄建立 .dockerignore 檔案
  • 排除開發用檔案、測試、文件
  • 使用 ! 可以例外包含特定檔案

Multi-stage Build:減少最終映像檔大小

Multi-stage build 允許在 Dockerfile 中使用多個 FROM 指令,將建置過程分為多個階段。通常用於:

  1. 建置階段:安裝編譯工具、編譯程式碼
  2. 執行階段:只複製必要的執行檔,不包含建置工具

好處:

  • 大幅減少最終映像檔大小(不包含編譯工具和中間檔案)
  • 提升安全性(攻擊面減少)

範例:Go 應用程式

不使用 Multi-stage build

FROM golang:1.21
WORKDIR /app
COPY . .
RUN go build -o myapp
CMD ["./myapp"]

最終映像檔大小:約 800MB(包含整個 Go 編譯環境)

使用 Multi-stage build

# 階段一:建置
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp

# 階段二:執行
FROM alpine:3.19
WORKDIR /app
COPY --from=builder /app/myapp .
CMD ["./myapp"]

最終映像檔大小:約 10MB(只包含編譯好的執行檔)

範例:Python 應用程式

# 階段一:建置(安裝相依套件)
FROM python:3.12 AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

# 階段二:執行
FROM python:3.12-slim
WORKDIR /app

# 從建置階段複製已安裝的套件
COPY --from=builder /root/.local /root/.local
COPY . .

# 確保 pip 安裝的指令可用
ENV PATH=/root/.local/bin:$PATH

CMD ["python", "app.py"]

範例:Node.js 應用程式

# 階段一:建置
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

# 階段二:執行
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
CMD ["node", "app.js"]

Multi-stage build 技巧:

  • 為階段命名(AS builder)方便引用
  • 使用 COPY --from=<stage> 從前一階段複製檔案
  • 執行階段使用更小的基底映像檔(如 alpineslim
  • 可以有多個建置階段,按需複製所需檔案

其他最佳實踐

選擇適合的基底映像檔

  • alpine:最小(約 5MB),但未必好,有時候為了相容性安裝套件反而比 slim 還大
  • slim:較小(去除不必要的套件),平衡大小與相容性
  • 標準版:功能完整,適合開發與除錯

使用特定版本標籤

# ❌ 不佳:使用 latest(不可預測)
FROM python:latest

# ✅ 較佳:使用具體版本
FROM python:3.12.1-slim

將不變的指令放前面,善用快取

如前述快取原理,妥善安排指令順序。

避免在映像檔中存放敏感資訊

  • 不要將金鑰、密碼寫在 Dockerfile 或 ENV
  • 使用 Docker secrets 或在執行時傳入

使用非特權使用者執行

RUN useradd -m appuser
USER appuser

映像檔發布

建置好映像檔後,可以將其上傳至 Docker Hub 或私有 Registry,方便團隊成員或部署流程使用。

發布至 Docker Hub

Docker Hub 是 Docker 的官方公開映像檔儲存庫,提供免費的公開映像檔儲存。

步驟一:註冊 Docker Hub 帳號

前往 Docker Hub 註冊帳號(免費)。

步驟二:登入 Docker Hub

docker login

系統會提示輸入 Docker Hub 的使用者名稱和密碼(或 Access Token)。

步驟三:標記映像檔

映像檔必須遵循命名規則:username/repository:tag

# 建置映像檔時直接命名
docker build -t myusername/my-app:1.0.0 .

# 或者為已存在的映像檔加上新標籤
docker tag my-app:latest myusername/my-app:1.0.0
docker tag my-app:latest myusername/my-app:latest

命名規則:

  • username:你的 Docker Hub 使用者名稱
  • repository:儲存庫名稱(如 my-app
  • tag:標籤(如 1.0.0latest

步驟四:推送映像檔

docker push myusername/my-app:1.0.0
docker push myusername/my-app:latest

步驟五:使用發布的映像檔

其他人可以直接 pull 你的映像檔:

docker pull myusername/my-app:1.0.0
docker run myusername/my-app:1.0.0

映像檔標籤管理

語意化版本(Semantic Versioning):

# 主版本.次版本.修訂版本
docker tag my-app:latest myusername/my-app:1.0.0
docker tag my-app:latest myusername/my-app:1.0
docker tag my-app:latest myusername/my-app:1
docker tag my-app:latest myusername/my-app:latest

docker push myusername/my-app:1.0.0
docker push myusername/my-app:1.0
docker push myusername/my-app:1
docker push myusername/my-app:latest

使用者可以選擇:

  • myusername/my-app:1.0.0:鎖定特定版本
  • myusername/my-app:1.0:使用 1.0.x 的最新修訂
  • myusername/my-app:1:使用 1.x.x 的最新版本
  • myusername/my-app:latest:使用最新版本(不推薦生產環境使用)

私有 Registry

如果不希望映像檔公開,可以使用私有 Registry:

選項一:Docker Hub 私有儲存庫

Docker Hub 免費帳號提供一個私有儲存庫。推送方式與公開儲存庫相同,只需在 Docker Hub 網站上將儲存庫設為 private。

選項二:自架 Docker Registry

使用 Docker 官方的 Registry 映像檔:

# 啟動本地 Registry
docker run -d -p 5000:5000 --name registry registry:2

# 標記映像檔
docker tag my-app:latest localhost:5000/my-app:latest

# 推送至本地 Registry
docker push localhost:5000/my-app:latest

# 從本地 Registry pull
docker pull localhost:5000/my-app:latest

選項三:雲端服務

  • AWS ECR(Elastic Container Registry)
  • Google Container Registry(GCR)
  • Azure Container Registry(ACR)
  • GitLab Container Registry
  • GitHub Container Registry

推送至 AWS ECR 範例:

# 登入 ECR
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin <account-id>.dkr.ecr.us-east-1.amazonaws.com

# 標記映像檔
docker tag my-app:latest <account-id>.dkr.ecr.us-east-1.amazonaws.com/my-app:latest

# 推送
docker push <account-id>.dkr.ecr.us-east-1.amazonaws.com/my-app:latest

實作練習:為 FastAPI 應用程式撰寫 Dockerfile

現在讓我們動手實作,為一個簡單的 Python FastAPI 應用程式撰寫 Dockerfile 並建置映像檔。

準備應用程式

建立專案目錄與檔案:

mkdir fastapi-demo
cd fastapi-demo

建立檔案:

app.py
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return {"message": "Hello from Docker!"}

@app.get("/health")
def health_check():
    return {"status": "healthy"}
requirements.txt
fastapi==0.128.0
uvicorn[standard]==0.40.0

撰寫 Dockerfile

建立 Dockerfile

Dockerfile
# 使用官方 Python 3.12 slim 映像檔
FROM python:3.12-slim

# 設定工作目錄
WORKDIR /app

# 複製相依套件清單
COPY requirements.txt .

# 安裝相依套件
RUN pip install --no-cache-dir -r requirements.txt

# 複製應用程式碼
COPY app.py .

# 宣告應用程式監聽的連接埠
EXPOSE 8000

# 定義健康檢查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1

# 建立非特權使用者
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
USER appuser

# 啟動應用程式
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]

說明:

  1. 使用 python:3.12-slim 減少映像檔大小
  2. 先複製 requirements.txt 並安裝,善用建置快取
  3. 最後才複製應用程式碼(經常變動)
  4. 使用 EXPOSE 宣告連接埠
  5. 加入 HEALTHCHECK 監控應用程式健康
  6. 建立非特權使用者提升安全性
  7. 使用 CMD 啟動 FastAPI 應用程式

建立 .dockerignore

建立 .dockerignore 排除不需要的檔案:

.dockerignore
__pycache__
*.pyc
.git
.venv
venv/
.env

建置映像檔

執行 docker build 指令建置映像檔:

docker build -t fastapi-demo:1.0.0 .

參數說明:

  • -t fastapi-demo:1.0.0:指定映像檔名稱與標籤
  • .:建置 context(當前目錄)

建置過程輸出範例:

[+] Building 12.3s (11/11) FINISHED
 => [internal] load build definition from Dockerfile
 => => transferring dockerfile: 520B
 => [internal] load .dockerignore
 => [1/6] FROM docker.io/library/python:3.12-slim
 => [2/6] WORKDIR /app
 => [3/6] COPY requirements.txt .
 => [4/6] RUN pip install --no-cache-dir -r requirements.txt
 => [5/6] COPY app.py .
 => [6/6] RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
 => exporting to image
 => => exporting layers
 => => writing image sha256:abc123...
 => => naming to docker.io/library/fastapi-demo:1.0.0

查看建置好的映像檔:

docker images fastapi-demo

執行容器並驗證

啟動容器:

docker run -d -p 8000:8000 --name fastapi-app fastapi-demo:1.0.0

參數說明:

  • -d:在背景執行
  • -p 8000:8000:將 Host 的 8000 連接埠對應到容器的 8000 連接埠
  • --name fastapi-app:為容器命名
  • fastapi-demo:1.0.0:使用的映像檔

驗證應用程式:

開啟瀏覽器訪問 http://localhost:8000,應該會看到:

{"message": "Hello from Docker!"}

或使用 curl

curl http://localhost:8000
curl http://localhost:8000/health

查看自動生成的 API 文件:

FastAPI 內建 OpenAPI 文件,訪問 http://localhost:8000/docs 可以看到互動式 API 文件。

查看容器日誌:

docker logs fastapi-app

查看健康狀態:

docker ps
# STATUS 欄位應顯示 "Up X minutes (healthy)"

停止並清理容器:

docker stop fastapi-app
docker rm fastapi-app

優化版本:Multi-stage Build

為了進一步減少映像檔大小,可以使用 Multi-stage build:

Dockerfile.multi-stage
# 階段一:建置(安裝相依套件)
FROM python:3.12-slim AS builder

WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

# 階段二:執行
FROM python:3.12-slim

WORKDIR /app

# 建立非特權使用者
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
USER appuser

# 從建置階段複製已安裝的套件
COPY --from=builder /root/.local /home/appuser/.local
ENV PATH=/home/appuser/.local/bin:$PATH

EXPOSE 8000

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1

# 複製應用程式碼
COPY app.py .

CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]

建置優化版本:

docker build -f Dockerfile.multi-stage -t fastapi-demo:1.0.0-slim .

比較映像檔大小:

docker images fastapi-demo

你應該會看到 multi-stage build 版本明顯更小。

如果要驗證是否正常可以啟動容器:

docker run -d -p 8000:8000 --name fastapi-app fastapi-demo:1.0.0-slim

任務結束

完成!

恭喜你完成了這個任務!現在你已經學會:

  • 理解 Dockerfile 的用途與建置流程
  • 掌握基礎建置指令(FROM、RUN、COPY、ADD)
  • 掌握環境與路徑設定(WORKDIR、ENV、ARG)
  • 掌握執行與進階設定(EXPOSE、VOLUME、USER、HEALTHCHECK、LABEL)
  • 理解 CMD 與 ENTRYPOINT 的差異與搭配
  • 理解 Layer 機制與快取原理
  • 掌握建置最佳實踐(減少 layer、.dockerignore、Multi-stage build)
  • 學會將映像檔發布至 Docker Hub 或私有 Registry
  • 透過實作練習撰寫完整的 Dockerfile

你現在已經能夠為應用程式撰寫高效、安全的 Dockerfile,並建置出生產環境可用的映像檔。繼續保持練習,嘗試為不同類型的應用程式(Node.js、Go、Java 等)撰寫 Dockerfile,深化你的容器化技能!