iT邦幫忙

2025 iThome 鐵人賽

DAY 28
0
生成式 AI

打造基於 MCP 協議與 n8n 工作流的會議處理 Agent系列 第 28

Day 28 Docker 映像檔最佳化 — 實現輕量與敏捷

  • 分享至 

  • xImage
  •  

昨天我成功將 M2A Agent 應用程式容器化,實現了使用單一指令 docker-compose up 就能啟動系統。但是在這個過程中,我發現映像檔的大小不只大,建構時間也長。

因此今天的目標是深入探索 Docker 映像檔最佳化技術,透過「多階段建構(Multi-stage builds)」與「分層快取(Layer Caching)」等進階技術,讓我們的 M2A Agent 變得更加輕量與敏捷。

今天的目標與挑戰

  • 實作進階的多階段建構,分離建構與執行環境
  • 深入理解並最佳化

Step 1:分析當前 Dockerfile 的最佳化空間

在開始優化之前,我需要先深入分析昨天建立的 Dockerfile,分析潛在的改善點。

1-1 當前 Dockerfile 問題分析

檢視昨天的 Dockerfile,我發現了幾個關鍵的優化機會:

建構效率問題

  • pip install 步驟總是重新執行,沒有利用分層快取的優勢
  • 依賴套件安裝與原始碼複製混合在一起,破壞了快取的連續性
  • 沒有充分利用多階段建構來優化最終映像檔大小

映像檔大小問題

  • 包含了不必要的建構工具與 pip 快取檔案
  • 沒有適當清理暫存檔案與編譯過程產生的中間檔案
  • 基礎映像檔的選擇仍有最佳化空間

1-2 建立映像檔分析基準

為了量化最佳化效果,我先記錄當前映像檔的基準數據:

# 查看映像檔大小
docker images m2aagent-m2a-agent

# 使用 docker history 查看各層大小
docker history m2aagent-m2a-agent

Docker m2aagent-m2a-agent images & history

透過這個分析,我可以識別哪些層佔用最多空間,以及哪些操作導致了不必要的膨脹。Docker 分層快取機制

  • 重新設計 Dockerfile 指令順序,最大化快取效益
  • 整合 pip cache mount 技術,加速重複建構過程
  • 建立映像檔大小監控與分析機制

Step 1:分析目前 Dockerfile 的最佳化空間

在開始之前,我需要先分析昨天建立的 Dockerfile,了解潛在的改善點。

1-1 目前 Dockerfile 問題分析

檢視昨天的 Dockerfile,我發現了幾個可最佳化的問題

建構效率問題

  • pip install 步驟總是重新執行,沒有利用分層快取的優勢
  • 依賴套件安裝與原始碼複製混合在一起,破壞了快取的連續性
  • 沒有充分利用多階段建構來優化最終映像檔大小

映像檔大小問題

  • 沒有適當清理暫存檔案與編譯過程產生的中間檔案
  • 基礎映像檔的選擇仍有最佳化空間

1-2 建立映像檔分析基準

為了量化最佳化效果,我先記錄當前映像檔的數據

# 查看映像檔大小
docker images m2aagent-m2a-agent

# 使用 docker history 查看各層大小
docker history m2aagent-m2a-agent

Docker m2aagent-m2a-agent images & history

透過這這些操作,我可以了解哪些層佔用最多空間,以及哪些操作導致了不必要的膨脹。


Step 2:設計進階多階段建構架構

根據分析結果,我重新設計 Dockerfile 的多階段建構架構。

2-1 重構 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"]

2-2 多階段建構設計說明

Stage 1 - Dependencies(依賴套件建構階段)

  • 專一職責:僅負責安裝和建構 Python 依賴套件
  • 虛擬環境隔離:使用 Python venv 避免系統環境污染,提高安全性
  • 建構工具管理:包含 build-essential 等編譯工具,但不會進入最終映像檔
  • 快取友善設計:優先複製 pyproject.toml,最大化 pip 安裝步驟的快取重用率

Stage 2 - Runtime(應用程式執行階段)

  • 輕量基礎:同樣使用 python:3.12-slim-bookworm,但不包含建構工具
  • 環境隔離:從 Stage 1 複製完整的虛擬環境,確保依賴完整性
  • 安全考量:建立專用的 appuser,避免以 root 權限執行應用程式
  • 檔案順序優化:依據修改頻率排序複製操作,提高快取效率

Step 3:實作分層快取最佳化策略

Docker 的分層快取是提升建構效率的關鍵,因此我需要深入了解其運作機制並善加利用。

3-1 分層快取機制深度解析

Docker 建構映像檔時,每個指令都會建立一個新的層(Layer),而 Docker 會根據以下條件決定是否重用快取:

  1. 指令內容一致性:指令的文字內容必須完全相同
  2. 檔案內容一致性COPYADD 指令涉及的檔案內容未變更
  3. 層級依賴性:前一層的快取必須有效,當前層才能使用快取

3-2 強化 .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

3-3 實作 BuildKit Cache Mount 技術

我要提升建構效率,因此我使用 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 的快取,即使映像檔層被重建,下載過的套件仍可重複使用。


Step 4 更新 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:

Step 5:建構、驗證與端對端測試

所有設定檔都已設定完畢,現在就來進行建構與驗證流程。

5-1 執行建構與驗證快取效益

使用 docker-compose build 來建構新的映像檔,並連續執行兩次以驗證快取效果。

第一次建構

docker-compose build

Docker First M2A Agent Build

第二次建構

docker-compose build

Docker Second M2A Agent Build

透過 Dockerfile 最佳化與快取技術,成功將映像檔第二次建構時間從 196.1 秒大幅縮短至 2.8 秒。

5-2 啟動服務與監控服務健康狀態

建構成功後,就可以啟動完整的服務,並進行端對端測試。

# 使用 -d 旗標在背景啟動所有服務
docker-compose up --build -d

# 觀察容器狀態,m2a-agent-app 應該顯示 (healthy)
docker-compose ps
  • --build:強制重新建構 m2a-agent 服務的映像檔
  • -d:啟動所有服務,並在背景執行

Docker Third M2A Agent Build
Docker-compose ps (starting)
Docker-compose ps (healthy)

容器狀態順利轉為 (healthy),驗證了應用程式已準備就緒,能穩定運作。

為何 (healthy) 狀態如此重要?

容器的 (healthy) 狀態是其服務就緒 (service readiness) 的關鍵指標。這個狀態由 Dockerfile 中定義的 HEALTHCHECK 指令驅動,它不僅確認容器行程已啟動,更重要的是驗證內部應用程式能正常處理請求

與僅表示「運行中」的 running 狀態不同,healthy 狀態意味著:

  • 應用程式已完全初始化:包括載入模型等耗時操作都已完成。
  • 服務可正常訪問:健康檢查(如 curl 指令)已成功與應用程式端點通訊,確認其響應正常。

在自動化部署及容器編排系統(如 Docker Swarm 或 Kubernetes)中,這個狀態滿重要的,系統會根據健康檢查結果自動管理容器生命週期,例如重啟無響應的容器,或將流量僅導向健康的實例,從而確保服務的高可用性與可靠性。

因此,確認容器達到 (healthy) 狀態,是驗證應用程式已準備好對外提供完整服務的明確信號。

5-3 端對端測試

流程全都正常!😁


今天的成果總結

完成項目

  • 實作進階的多階段建構,將 Dockerfile 重構成「建構」與「執行」兩個階段
  • 調整 Dockerfile 的指令順序,並達到深度最佳化分層快取
  • 強化 .dockerignore
  • 驗證了映像檔建構時間從 196.1 秒 驟降至 2.8 秒,量化快取成果顯著!
  • Dockerfiledocker-compose.yml 中加入了 healthcheck,讓系統具備了自我健康監測的能力
  • 成功診斷並解決了因 Faster-Whisper 模型載入時間過長,導致容器被誤判為 (unhealthy) 的問題

心得

今天是我在 Docker 技術上透過親手實作多階段建構與分層快取,我不再只是單純地執行 docker build,而是能像一位外科醫生一樣,精準地剖析映像檔的每一層,移除多餘的脂肪(不必要的檔案),留下精實的肌肉(真正需要的執行環境)。

看著第二次建構時間從三分鐘縮短到三秒鐘,那種將複雜問題化繁為簡的成就感,是我最大的快樂。而後續遭遇的 (unhealthy) 狀態,更是一次寶貴的實戰經驗,它讓我體會到,在部署 AI 應用時,我們不只要關注程式碼本身,更要考慮到模型載入的「冷啟動」效能瓶頸,並學會如何透過 healthcheck 的參數來處理這種延遲。

這次的探索,讓我對 Docker 在 AI 工程化(MLOps)領域的重要性有了全新的認識,一個最佳化過的 Docker 環境,不僅是提升開發效率的利器,更是確保 AI 服務在生產環境中穩定、高效運行的基石。

🎯 明天計畫

為專案導入容器可觀測性,整合 Grafana Loki 實現集中式日誌監控與視覺化。


上一篇
Day 27 應用程式容器化 — 使用 Docker 實現一鍵啟動
系列文
打造基於 MCP 協議與 n8n 工作流的會議處理 Agent28
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言