任務四: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 之前)。
語法:
範例:
# 使用官方 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 clean、rm -rf /var/lib/apt/lists/*)
COPY:複製檔案至映像檔¶
COPY 指令從建置環境(通常是 Dockerfile 所在目錄)複製檔案或目錄到映像檔內。
語法:
範例:
# 複製單一檔案
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:功能更豐富的複製指令¶
ADD 與 COPY 類似,但提供額外功能:自動解壓縮 tar 檔案、支援 URL 下載。
語法:
範例:
# 複製並自動解壓縮
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 curl或RUN wget,因為更容易控制錯誤處理
環境與路徑設定¶
WORKDIR:設定工作目錄¶
WORKDIR 指令設定後續指令的工作目錄,類似於 cd 指令,但會自動建立目錄(如果不存在)。
語法:
範例:
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 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
容器執行時的使用:
最佳實踐:
- 使用大寫命名慣例(如
NODE_ENV) - 敏感資訊(如密碼、金鑰)不要寫在 Dockerfile,應該用 secrets 或執行時傳入
- 可以將常用路徑設為環境變數,方便後續參考
ARG:定義建置時期變數¶
ARG 定義建置時期的變數,只在建置映像檔時有效,容器執行時無法使用。
語法:
範例:
# 定義建置參數(有預設值)
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 指令宣告容器在執行時會監聽的網路連接埠。這是一種文件化的宣告,並不會實際發布連接埠。
語法:
範例:
# 宣告 HTTP 連接埠
EXPOSE 80
# 宣告多個連接埠
EXPOSE 8000 8001
# 指定協定(預設是 TCP)
EXPOSE 53/udp
EXPOSE 8080/tcp
# 同時宣告 TCP 和 UDP
EXPOSE 9000/tcp 9000/udp
實際發布連接埠:
最佳實踐:
- 即使
EXPOSE不是強制的,仍建議宣告,讓使用者知道應用程式使用哪些連接埠 - 將
EXPOSE視為文件,幫助維護者理解映像檔
VOLUME:建立掛載點¶
VOLUME 指令建立掛載點,用於持久化資料或與 Host 共享檔案。
語法:
範例:
# 單一 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,但基於安全性考量,建議切換為非特權使用者。
語法:
範例:
# 切換為 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 會定期執行檢查指令,根據結果判斷容器是否健康。
語法:
選項:
--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次數
查看健康狀態:
最佳實踐:
- 健康檢查應該輕量、快速執行
- 檢查應用程式的實際功能,而非只是程式存活(如 HTTP endpoint)
- 適當設定
start-period,給應用程式足夠的啟動時間
LABEL:為映像檔加入中繼資料¶
LABEL 指令為映像檔加入 key-value 格式的中繼資料(metadata),用於標記版本、維護者、授權資訊等。
語法:
範例:
# 單一標籤
LABEL version="1.0.0"
# 多個標籤
LABEL maintainer="[email protected]" \
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="[email protected]" \
org.opencontainers.image.source="https://github.com/username/repo"
# 包含空格的值需用引號
LABEL description="This is a multi-word description"
查看標籤:
常見的標籤慣例(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:容器啟動指令¶
CMD 和 ENTRYPOINT 都用於定義容器啟動時執行的指令,但行為不同。理解兩者差異是撰寫 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
範例:
執行時行為:
# 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 搭配使用¶
組合使用 ENTRYPOINT 和 CMD 可以實現彈性配置: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
範例二:工具類容器¶
執行時:
# 顯示 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 內容:
選擇建議:
- 只需要預設指令且允許完全覆蓋:單獨使用
CMD - 容器功能固定(如工具類):單獨使用
ENTRYPOINT - 需要固定執行邏輯 + 彈性參數:
ENTRYPOINT+CMD組合
Layer 機制與快取原理¶
理解 Docker 的 Layer(層)機制與快取原理,能幫助你撰寫出建置更快、映像檔更小的 Dockerfile。
什麼是 Layer?¶
Docker 映像檔是由多個唯讀 layer 堆疊而成的。Dockerfile 中的每條指令(如 FROM、RUN、COPY)都會產生一個新的 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 在建置映像檔時會檢查每條指令:
- 如果指令與上次建置完全相同,且之前的 layer 仍存在,就重用該 layer(快取命中)
- 如果指令改變,或之前的快取失效,就重新執行指令並建立新 layer
快取行為範例:
第一次建置:
第二次建置(只改了 app.py):
關鍵觀念:一旦某個 layer 的快取失效,後續所有 layer 都必須重新建置。
快取失效的情況¶
指令本身改變¶
COPY/ADD 的檔案內容改變¶
COPY requirements.txt /app/ # 如果 requirements.txt 內容改變,快取失效
RUN pip install -r /app/requirements.txt # 前一層失效,這層也必須重建
Docker 會計算檔案的 checksum,只要內容改變(即使檔案時間戳相同)就會失效。
使用 --no-cache 參數¶
基底映像檔更新¶
善用快取加速建置¶
原則:將不常變動的指令放在前面,常變動的放在後面。
❌ 不佳的做法:
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 數量¶
每條 RUN、COPY、ADD 都會產生新 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 範例:
# 版本控制
.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 指令,將建置過程分為多個階段。通常用於:
- 建置階段:安裝編譯工具、編譯程式碼
- 執行階段:只複製必要的執行檔,不包含建置工具
好處:
- 大幅減少最終映像檔大小(不包含編譯工具和中間檔案)
- 提升安全性(攻擊面減少)
範例:Go 應用程式¶
❌ 不使用 Multi-stage build:
最終映像檔大小:約 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>從前一階段複製檔案 - 執行階段使用更小的基底映像檔(如
alpine、slim) - 可以有多個建置階段,按需複製所需檔案
其他最佳實踐¶
選擇適合的基底映像檔¶
alpine:最小(約 5MB),但未必好,有時候為了相容性安裝套件反而比 slim 還大slim:較小(去除不必要的套件),平衡大小與相容性- 標準版:功能完整,適合開發與除錯
使用特定版本標籤¶
將不變的指令放前面,善用快取¶
如前述快取原理,妥善安排指令順序。
避免在映像檔中存放敏感資訊¶
- 不要將金鑰、密碼寫在 Dockerfile 或 ENV
- 使用 Docker secrets 或在執行時傳入
使用非特權使用者執行¶
映像檔發布¶
建置好映像檔後,可以將其上傳至 Docker Hub 或私有 Registry,方便團隊成員或部署流程使用。
發布至 Docker Hub¶
Docker Hub 是 Docker 的官方公開映像檔儲存庫,提供免費的公開映像檔儲存。
步驟一:註冊 Docker Hub 帳號¶
前往 Docker Hub 註冊帳號(免費)。
步驟二:登入 Docker Hub¶
系統會提示輸入 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.0、latest)
步驟四:推送映像檔¶
步驟五:使用發布的映像檔¶
其他人可以直接 pull 你的映像檔:
映像檔標籤管理¶
語意化版本(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 並建置映像檔。
準備應用程式¶
建立專案目錄與檔案:
建立檔案:
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"}
撰寫 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"]
說明:
- 使用
python:3.12-slim減少映像檔大小 - 先複製
requirements.txt並安裝,善用建置快取 - 最後才複製應用程式碼(經常變動)
- 使用
EXPOSE宣告連接埠 - 加入
HEALTHCHECK監控應用程式健康 - 建立非特權使用者提升安全性
- 使用
CMD啟動 FastAPI 應用程式
建立 .dockerignore¶
建立 .dockerignore 排除不需要的檔案:
建置映像檔¶
執行 docker build 指令建置映像檔:
參數說明:
-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
查看建置好的映像檔:
執行容器並驗證¶
啟動容器:
參數說明:
-d:在背景執行-p 8000:8000:將 Host 的 8000 連接埠對應到容器的 8000 連接埠--name fastapi-app:為容器命名fastapi-demo:1.0.0:使用的映像檔
驗證應用程式:
開啟瀏覽器訪問 http://localhost:8000,應該會看到:
或使用 curl:
查看自動生成的 API 文件:
FastAPI 內建 OpenAPI 文件,訪問 http://localhost:8000/docs 可以看到互動式 API 文件。
查看容器日誌:
查看健康狀態:
停止並清理容器:
優化版本:Multi-stage Build¶
為了進一步減少映像檔大小,可以使用 Multi-stage build:
# 階段一:建置(安裝相依套件)
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"]
建置優化版本:
比較映像檔大小:
你應該會看到 multi-stage build 版本明顯更小。
如果要驗證是否正常可以啟動容器:
任務結束¶
完成!
恭喜你完成了這個任務!現在你已經學會:
- 理解 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,深化你的容器化技能!