iT邦幫忙

2025 iThome 鐵人賽

DAY 27
0
生成式 AI

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

Day 27 應用程式容器化 — 使用 Docker 實現一鍵啟動

  • 分享至 

  • xImage
  •  

昨天,我們成功地將專案中的硬編碼(Hardcoded)設定值抽離至 .env 檔案中,實踐了「十二因子應用(The Twelve-Factor App)」的第三項原則:在環境中儲存設定。這不僅提升了安全性與靈活性,更為今天的目標 — 應用程式容器化 — 鋪平了道路。

今天的目標是將我們的 Python AI 會議助理應用程式打包成一個標準化的 Docker 映像檔,並整合進現有的 docker-compose.yml 設定中。最後我的理想狀態是:開發人員只需要透過 docker-compose up 指令,就能「一鍵啟動」包含 n8n 與 Python 應用程式在內的完整系統,大幅簡化部署與開發環境設定的複雜度。

今天的目標與挑戰

  • 撰寫 Dockerfile 以定義標準化的應用程式執行環境
  • 將 M2A Agent 應用程式打包成可攜帶的 Docker 映像檔
  • 升級 docker-compose.yml 以整合並管理新的應用服務
  • 建立容器間的虛擬網路以實現服務間的順暢通訊
  • 實現使用單一指令一鍵啟動與部署完整的系統服務

Step 1:撰寫 Dockerfile 定義應用程式的標準化執行環境

簡單來說 Dockerfile 就像是一張應用程式的「組裝說明書」,它包含了許多的指令,讓 Docker 能夠依據這些指令,自動化地建構出一個包含我們應用程式所有程式碼、相依套件、以及執行環境的標準化映像檔。

我們在專案的根目錄(要與 docker-compose.yml 同層)底下,建立一個名為 Dockerfile 的新檔案,並撰寫以下內容

# Stage 1: 建構基礎執行環境
# 選擇與開發環境版本一致、輕量且穩定的官方 Python 映像檔
FROM python:3.12-slim-bookworm AS base

# 設定工作目錄,後續指令將在此路徑下執行
WORKDIR /app

# 先以 root 身分建立後續要使用的非 root 使用者
RUN useradd --create-home appuser

# Stage 2: 複製專案檔案
# 先將所有建構與執行所需的檔案,以 root 身分複製進來並設定好所有權
# 這樣可以充分利用 Docker 的分層快取(Layer Caching)
COPY --chown=appuser:appuser pyproject.toml ./
COPY --chown=appuser:appuser src/ ./src
COPY --chown=appuser:appuser app.py .
COPY --chown=appuser:appuser recording/ ./recording
COPY --chown=appuser:appuser data/ ./data

# Stage 3: 安裝相依套件
# 在所有原始碼都就緒後,切換為非 root 使用者來執行套件安裝
USER appuser
RUN pip install --no-cache-dir -e .

# Stage 4: 設定執行階段環境
# 開放 Gradio 服務所使用的埠號
EXPOSE 7860

# 定義容器啟動時預設執行的指令
CMD ["python", "app.py"]

Dockerfile 設計說明

  • 多階段建構(Multi-stage builds):雖然目前結構簡單,但採用專業的多階段建構語法(AS base),這為未來更進階的映像檔優化(例如分離建構環境與執行環境)預留了擴充性。
  • 選擇輕量基礎映像檔:選用 python:3.12-slim-bookworm,這是一個體積較小的映像檔版本,能有效減少最終映像檔的大小,加快部署速度。
  • 非 Root 使用者:出於安全性考量,因此建立了一個名為 appuser 的專用使用者來執行應用程式,避免在容器內使用權限過高的 root 使用者,這是公認的安全方式。
  • 檔案權限(--chown:在 COPY 指令中加上 --chown=appuser:appuser,確保複製進容器的檔案所有權都屬於自己建立的 appuser,增強了安全性。
  • 指令順序:我將所有 COPY 指令都集中在 RUN pip install 之前。這是因為 pip 在安裝時需要讀取 pyproject.tomlsrc/ 目錄,確保這些檔案先被複製進來是成功建構的關鍵。
  • 定義執行指令CMD ["python", "app.py"] 明確指定了當容器啟動時,應執行的主要程式。

Step 2:建立 .dockerignore 檔案

.dockerignore.gitignore 的概念類似,.dockerignore 檔案可以告訴 Docker 在建構映像檔時,哪些檔案或目錄應該被忽略。這不僅可以避免將敏感資訊(如 .env)或不必要的檔案(如虛擬環境 venv)打包進映像檔,還能加快建構過程。

在專案根目錄下建立一個名為 .dockerignore 的新檔案,並加入以下內容

# Git 版本控制相關目錄
.git
.gitignore

# Docker 相關設定檔
.dockerignore
docker-compose.yml

# Python 虛擬環境
.venv
venv

# Python 快取與編譯檔案
__pycache__
*.pyc
*.pyo
*.pyd

# 環境變數設定檔 (絕對不能打包進映像檔)
.env

# n8n 的資料儲存區
n8n_data/

# Gradio 執行階段產生的快取與憑證
.gradio/

# 測試用的檔案
test_*.py

Step 3:升級 docker-compose.yml 整合 Python 應用服務

現在我們要來修改核心的 docker-compose.yml 檔案,將剛剛定義好的 Python 應用程式服務(我將其命名為 m2a-agent)加入其中,並設定好與 n8n 之間的網路通訊。

以下為完整的 docker-compose.yml 內容

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

networks:
  m2a_network:
    driver: bridge

volumes:
  n8n_data:

docker-compose.yml 升級說明

  • 新增 m2a-agent 服務
    • build: .:此設定指示 Docker Compose 使用目前目錄下的 Dockerfile 來建構此服務的映像檔。
    • ports: - "7860:7860":將容器內 Gradio 應用程式的 7860 埠,映射到我們主機的 7860 埠,這樣我們才能透過瀏覽器 http://localhost:7860 存取介面。
    • env_file: - .env:它會讀取 .env 檔案中的所有變數,並將它們作為環境變數注入到 m2a-agent 容器中,我們的 Python 程式碼就能透過 os.getenv() 讀取到這些設定。
    • depends_on: - n8n:明確定義 m2a-agent 服務依賴於 n8n 服務,這能確保在啟動時,n8n 會比 m2a-agent 先行啟動。
  • 建立共用網路 (m2a_network
    • 我新增了一個名為 m2a_network 的橋接網路(bridge network),並讓 n8nm2a-agent 兩個服務都加入這個網路。
    • 在 Docker Compose 的網路環境中,服務之間可以直接使用「服務名稱」作為主機名稱(Hostname)來進行通訊。這代表我們的 Python 應用程式可以透過 http://n8n:5678/... 來存取 n8n 的服務,非常方便。

【重要】修改 .env 設定

為了讓容器內的 Python 應用能正確呼叫到同在 Docker 網路中的 n8n 服務,我們需要更新 .env 檔案中的 N8N_WEBHOOK_URL

請打開 .env 檔案,將 N8N_WEBHOOK_URL 的值修改如下:

# LM Studio 相關設定
LM_STUDIO_API_URL=http://<ip>:1234/v1/chat/completions
LM_STUDIO_MODEL=mradermacher/Qwen2.5-Taiwan-7B-Instruct-i1-GGUF

# n8n Webhook 相關設定 (使用服務名稱進行容器間通訊)
N8N_WEBHOOK_URL=http://n8n:5678/webhook/m2a-test

修改說明

我們將原來的 localhost 改成了 n8n,也就是 docker-compose.yml 中定義的 n8n 服務名稱。這樣,m2a-agent 容器就能在 Docker 的內部網路中,準確地找到 n8n 容器。


Step 4:設定 WSL 2 資源上限 — 馴服記憶體怪獸

我在實作時啟動容器後發現一個名為 VmmemWSL 的系統程式佔用了大量的記憶體,嚴重影響我電腦的運作效能,因此要在我們啟動容器之前為 WSL 2 (Windows Subsystem for Linux) 進行資源調校

VmmemWSL 代表了執行我所有 Docker 容器的整個 Linux 虛擬機器,為了對其記憶體與 CPU 使用率進行有效的資源配置,因此我透過 .wslconfig 檔案來設定其運作資源的上限。

以下是操作步驟:

  1. 建立或編輯 .wslconfig 檔案

    • 先開啟檔案總管,在路徑列輸入 %UserProfile% 並按下 Enter,直接進入使用者主目錄。
    • 在這個目錄下建立一個名為 .wslconfig 的新檔案。
  2. 撰寫資源限制內容
    打開 .wslconfig 檔案,並寫入最適合目前電腦硬體與工作負載的資源配置,這是在主機系統的流暢度Docker 容器的效能之間取得平衡:

    [wsl2]
    memory=8GB      # 依據電腦的總記憶體調整,建議設定為總量的 40%
    processors=4    # 依據 CPU 的總核心數調整,建議設定為總量的一半
    
  3. 重啟 WSL 以套用設定
    這個設定並不會馬上生效,因此必須徹底關閉 WSL 才能讓它在下次啟動時讀取新設定。

    • 完全關閉 Docker Desktop
    • 接著開啟 PowerShell,輸入以下指令並執行:
    wsl --shutdown
    

做完以上這些準備後再重新啟動 Docker Desktop。現在 WSL 2 會在我設定的資源限制下運作。


Step 5:驗證與測試

所有設定都已完成後,現在就來測試看看我們剛剛設定的是否都正確。

  1. 啟動服務:打開終端機,移動到專案根目錄,執行以下指令:
docker-compose up --build -d
  • --build:此旗標會強制 Docker Compose 在啟動前,根據我們的 Dockerfile 重新建構 m2a-agent 服務的映像檔。
  • -d:代表在背景(Detached mode)執行,終端機可以繼續做其他操作。
  1. 觀察容器狀態:打開 Docker Desktop,此時會看到在 m2aagent 專案底下,除了原有的 m2a-n8n,還多了一個名為 m2a-agent-app 的容器,並且兩者都處於執行中(Running)的狀態。
  2. 端對端測試
    • 在瀏覽器中開啟 http://localhost:7860,譨看到熟悉的 Gradio 前端介面。
    • 上傳一個音訊檔案,執行一次完整的會議處理流程。
    • 查看所有結果都正常顯示,代表我們的容器化整合大功告成!

今天的成果總結

完成項目

  • 撰寫了邏輯正確、符合安全實踐的 Dockerfile
  • 建立了完整的 .dockerignore 檔案,優化了映像檔的建構流程
  • 升級了 docker-compose.yml,實現了程式碼熱重載與資料持久化
  • 成功解決了建構過程中的錯誤,並對 Windows 上的記憶體佔用問題進行了最佳化
  • 實現了使用單一指令 docker-compose up --build -d 即可「一鍵啟動」整個 AI 會議助理後端系統

心得

今天我完成了從在「本機執行」到在「容器中執行」的突破,這個過程雖然充滿了錯誤除錯的挑戰,但也讓我對 Docker 的運作機制有了更深刻的理解。從 COPY 的時機、volumes 的必要性,到 VmmemWSL 的資源管理,每一步都是邁向專業開發流程的踏實足跡。

現在我的專案擁有了一個標準化、可攜、且易於分享的開發環境,看著兩個服務的啟動,整個系統流暢地正常工作,這種將複雜化繁為簡的感覺,充滿了成就感。

🎯 明天計劃

最佳化 Docker 映像,多階段建構縮減體積、層快取加速建構,使 M2A Agent 更輕量。


上一篇
Day 26 系統設定檔與環境變數管理 — 提升專案靈活性與安全性
下一篇
Day 28 Docker 映像檔最佳化 — 實現輕量與敏捷
系列文
打造基於 MCP 協議與 n8n 工作流的會議處理 Agent28
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言