昨天我成功將 M2A Agent 應用程式容器化,實現了使用單一指令 docker-compose up
就能啟動系統。但是在這個過程中,我發現映像檔的大小不只大,建構時間也長。
因此今天的目標是深入探索 Docker 映像檔最佳化技術,透過「多階段建構(Multi-stage builds)」與「分層快取(Layer Caching)」等進階技術,讓我們的 M2A Agent 變得更加輕量與敏捷。
在開始優化之前,我需要先深入分析昨天建立的 Dockerfile,分析潛在的改善點。
檢視昨天的 Dockerfile,我發現了幾個關鍵的優化機會:
建構效率問題:
pip install
步驟總是重新執行,沒有利用分層快取的優勢映像檔大小問題:
為了量化最佳化效果,我先記錄當前映像檔的基準數據:
# 查看映像檔大小
docker images m2aagent-m2a-agent
# 使用 docker history 查看各層大小
docker history m2aagent-m2a-agent
透過這個分析,我可以識別哪些層佔用最多空間,以及哪些操作導致了不必要的膨脹。Docker 分層快取機制
在開始之前,我需要先分析昨天建立的 Dockerfile,了解潛在的改善點。
檢視昨天的 Dockerfile,我發現了幾個可最佳化的問題
pip install
步驟總是重新執行,沒有利用分層快取的優勢為了量化最佳化效果,我先記錄當前映像檔的數據
# 查看映像檔大小
docker images m2aagent-m2a-agent
# 使用 docker history 查看各層大小
docker history m2aagent-m2a-agent
透過這這些操作,我可以了解哪些層佔用最多空間,以及哪些操作導致了不必要的膨脹。
根據分析結果,我重新設計 Dockerfile 的多階段建構架構。
Dockerfile
在 Dockerfile
裡,採用進階的多階段建構策略
# ===================================
# Stage 1:依賴套件建構階段
# ===================================
FROM python:3.12-slim-bookworm AS dependencies
# 設定工作目錄
WORKDIR /app
# 安裝系統依賴套件(僅建構時需要);使用快取掛載強化 apt 套件下載
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && apt-get install -y \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# 先複製依賴檔案,最大化 Docker 快取效益
COPY pyproject.toml ./
COPY src/ ./src/
# 建立虛擬環境,避免系統 Python 污染
RUN python -m venv /opt/venv
# 啟用虛擬環境並安裝依賴套件;使用快取掛載強化 pip 套件下載與安裝
ENV PATH="/opt/venv/bin:$PATH"
RUN --mount=type=cache,target=/root/.cache/pip,sharing=locked \
pip install --upgrade pip \
&& pip install --no-cache-dir -e .
# ===================================
# Stage 2:應用程式執行階段
# ===================================
FROM python:3.12-slim-bookworm AS runtime
# 建立非 root 使用者
RUN useradd --create-home --shell /bin/bash appuser
# 安裝執行階段所需的系統套件
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
# 從建構階段複製虛擬環境
COPY --from=dependencies /opt/venv /opt/venv
# 設定環境變數
ENV PATH="/opt/venv/bin:$PATH"
ENV PYTHONPATH="/app"
ENV PYTHONUNBUFFERED=1
# 設定工作目錄並修改所有權
WORKDIR /app
RUN chown appuser:appuser /app
# 切換至 appuser
USER appuser
# 複製應用程式檔案
COPY --chown=appuser:appuser src/ ./src/
COPY --chown=appuser:appuser app.py ./
COPY --chown=appuser:appuser data/ ./data/
COPY --chown=appuser:appuser recording/ ./recording/
# 建立 .gradio 目錄並設定權限
RUN mkdir -p .gradio
# 暴露應用程式埠號
EXPOSE 7860
# 設定健康檢查
HEALTHCHECK --interval=30s --timeout=30s --start-period=300s --retries=3 \
CMD curl -f http://localhost:7860/healthz || exit 1
# 定義容器啟動指令
CMD ["python", "app.py"]
Stage 1 - Dependencies(依賴套件建構階段):
build-essential
等編譯工具,但不會進入最終映像檔pyproject.toml
,最大化 pip 安裝步驟的快取重用率Stage 2 - Runtime(應用程式執行階段):
python:3.12-slim-bookworm
,但不包含建構工具appuser
,避免以 root 權限執行應用程式Docker 的分層快取是提升建構效率的關鍵,因此我需要深入了解其運作機制並善加利用。
Docker 建構映像檔時,每個指令都會建立一個新的層(Layer),而 Docker 會根據以下條件決定是否重用快取:
COPY
或 ADD
指令涉及的檔案內容未變更.dockerignore
為了提升快取效率並減少建構上下文大小,我更新了 .dockerignore
檔案
# ===== 版本控制相關 =====
.git
.gitignore
.gitattributes
# ===== Docker 相關設定檔 =====
.dockerignore
docker-compose.yml
docker-compose.*.yml
Dockerfile*
# ===== Python 環境與快取 =====
.venv
venv
__pycache__
*.pyc
*.pyo
*.pyd
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# ===== 測試與開發工具 =====
recording/
.pytest_cache/
.coverage
.tox/
.nox/
.mypy_cache/
.dmypy.json
dmypy.json
# ===== 環境變數與機敏資訊 =====
.env
.env.*
*.key
*.pem
# ===== 應用程式特定檔案 =====
n8n_data/
recording/
*.log
logs/
temp/
tmp/
# ===== Gradio 快取 =====
.gradio/
gradio_cached_examples/
# ===== 測試檔案 =====
test_*.py
*_test.py
tests/
# ===== 編輯器與 IDE =====
.vscode/
.idea/
*.swp
*.swo
*~
# ===== 作業系統相關 =====
.DS_Store
Thumbs.db
我要提升建構效率,因此我使用 Docker BuildKit 的 Cache Mount 功能,取代 Step 2-1 中的 dependencies 階段為
# 在 dependencies 階段加入快取掛載
FROM python:3.12-slim-bookworm AS dependencies
# 設定工作目錄
WORKDIR /app
# 使用快取掛載強化 apt 套件下載
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && apt-get install -y \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# 先複製依賴檔案,最大化 Docker 快取效益
COPY pyproject.toml ./
# 建立虛擬環境,避免系統 Python 污染
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# 使用快取掛載強化 pip 套件下載與安裝
RUN --mount=type=cache,target=/root/.cache/pip,sharing=locked \
pip install --upgrade pip \
&& pip install --no-cache-dir -e .
這個技術可以在多次建構之間保持 apt 和 pip 的快取,即使映像檔層被重建,下載過的套件仍可重複使用。
docker-compose.yml
修改 docker-compose.yml
以使用新的 Dockerfile
services:
n8n:
image: n8nio/n8n:latest
container_name: m2a-n8n
restart: unless-stopped
ports:
- "5678:5678"
volumes:
- ./n8n_data:/home/node/.n8n
environment:
- N8N_HOST=0.0.0.0
- N8N_PORT=5678
- N8N_PROTOCOL=http
- WEBHOOK_URL=http://n8n:5678/
- GENERIC_TIMEZONE=Asia/Taipei
- N8N_SECURE_COOKIE=false
- N8N_DIAGNOSTICS_ENABLED=false
networks:
- m2a_network
m2a-agent:
build: .
container_name: m2a-agent-app
restart: unless-stopped
ports:
- "7860:7860"
volumes:
- ./app.py:/app/app.py
- ./src:/app/src
- ./data:/app/data
- ./recording:/app/recording
- ./.gradio:/app/.gradio
env_file:
- .env
depends_on:
- n8n
networks:
- m2a_network
# 新增健康檢查,與 Dockerfile 中的 HEALTHCHECK 指令對應
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:7860/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
networks:
m2a_network:
driver: bridge
volumes:
n8n_data:
所有設定檔都已設定完畢,現在就來進行建構與驗證流程。
使用 docker-compose build
來建構新的映像檔,並連續執行兩次以驗證快取效果。
docker-compose build
docker-compose build
透過 Dockerfile 最佳化與快取技術,成功將映像檔第二次建構時間從 196.1 秒大幅縮短至 2.8 秒。
建構成功後,就可以啟動完整的服務,並進行端對端測試。
# 使用 -d 旗標在背景啟動所有服務
docker-compose up --build -d
# 觀察容器狀態,m2a-agent-app 應該顯示 (healthy)
docker-compose ps
--build
:強制重新建構 m2a-agent 服務的映像檔-d
:啟動所有服務,並在背景執行容器狀態順利轉為 (healthy),驗證了應用程式已準備就緒,能穩定運作。
(healthy)
狀態如此重要?容器的 (healthy)
狀態是其服務就緒 (service readiness) 的關鍵指標。這個狀態由 Dockerfile
中定義的 HEALTHCHECK
指令驅動,它不僅確認容器行程已啟動,更重要的是驗證內部應用程式能正常處理請求。
與僅表示「運行中」的 running
狀態不同,healthy
狀態意味著:
curl
指令)已成功與應用程式端點通訊,確認其響應正常。在自動化部署及容器編排系統(如 Docker Swarm 或 Kubernetes)中,這個狀態滿重要的,系統會根據健康檢查結果自動管理容器生命週期,例如重啟無響應的容器,或將流量僅導向健康的實例,從而確保服務的高可用性與可靠性。
因此,確認容器達到 (healthy)
狀態,是驗證應用程式已準備好對外提供完整服務的明確信號。
流程全都正常!😁
✅ 完成項目
Dockerfile
重構成「建構」與「執行」兩個階段Dockerfile
的指令順序,並達到深度最佳化分層快取.dockerignore
Dockerfile
與 docker-compose.yml
中加入了 healthcheck
,讓系統具備了自我健康監測的能力Faster-Whisper
模型載入時間過長,導致容器被誤判為 (unhealthy)
的問題今天是我在 Docker 技術上透過親手實作多階段建構與分層快取,我不再只是單純地執行 docker build
,而是能像一位外科醫生一樣,精準地剖析映像檔的每一層,移除多餘的脂肪(不必要的檔案),留下精實的肌肉(真正需要的執行環境)。
看著第二次建構時間從三分鐘縮短到三秒鐘,那種將複雜問題化繁為簡的成就感,是我最大的快樂。而後續遭遇的 (unhealthy)
狀態,更是一次寶貴的實戰經驗,它讓我體會到,在部署 AI 應用時,我們不只要關注程式碼本身,更要考慮到模型載入的「冷啟動」效能瓶頸,並學會如何透過 healthcheck
的參數來處理這種延遲。
這次的探索,讓我對 Docker 在 AI 工程化(MLOps)領域的重要性有了全新的認識,一個最佳化過的 Docker 環境,不僅是提升開發效率的利器,更是確保 AI 服務在生產環境中穩定、高效運行的基石。
🎯 明天計畫
為專案導入容器可觀測性,整合 Grafana Loki 實現集中式日誌監控與視覺化。