昨天,我們成功地將專案中的硬編碼(Hardcoded)設定值抽離至 .env
檔案中,實踐了「十二因子應用(The Twelve-Factor App)」的第三項原則:在環境中儲存設定。這不僅提升了安全性與靈活性,更為今天的目標 — 應用程式容器化 — 鋪平了道路。
今天的目標是將我們的 Python AI 會議助理應用程式打包成一個標準化的 Docker 映像檔,並整合進現有的 docker-compose.yml
設定中。最後我的理想狀態是:開發人員只需要透過 docker-compose up
指令,就能「一鍵啟動」包含 n8n 與 Python 應用程式在內的完整系統,大幅簡化部署與開發環境設定的複雜度。
Dockerfile
以定義標準化的應用程式執行環境docker-compose.yml
以整合並管理新的應用服務簡單來說 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"]
AS base
),這為未來更進階的映像檔優化(例如分離建構環境與執行環境)預留了擴充性。python:3.12-slim-bookworm
,這是一個體積較小的映像檔版本,能有效減少最終映像檔的大小,加快部署速度。appuser
的專用使用者來執行應用程式,避免在容器內使用權限過高的 root
使用者,這是公認的安全方式。--chown
):在 COPY
指令中加上 --chown=appuser:appuser
,確保複製進容器的檔案所有權都屬於自己建立的 appuser
,增強了安全性。COPY
指令都集中在 RUN pip install
之前。這是因為 pip
在安裝時需要讀取 pyproject.toml
與 src/
目錄,確保這些檔案先被複製進來是成功建構的關鍵。CMD ["python", "app.py"]
明確指定了當容器啟動時,應執行的主要程式。.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
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),並讓 n8n
和 m2a-agent
兩個服務都加入這個網路。http://n8n:5678/...
來存取 n8n 的服務,非常方便。為了讓容器內的 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
容器。
我在實作時啟動容器後發現一個名為 VmmemWSL
的系統程式佔用了大量的記憶體,嚴重影響我電腦的運作效能,因此要在我們啟動容器之前為 WSL 2 (Windows Subsystem for Linux) 進行資源調校。
VmmemWSL
代表了執行我所有 Docker 容器的整個 Linux 虛擬機器,為了對其記憶體與 CPU 使用率進行有效的資源配置,因此我透過 .wslconfig
檔案來設定其運作資源的上限。
以下是操作步驟:
建立或編輯 .wslconfig
檔案:
%UserProfile%
並按下 Enter,直接進入使用者主目錄。.wslconfig
的新檔案。撰寫資源限制內容:
打開 .wslconfig
檔案,並寫入最適合目前電腦硬體與工作負載的資源配置,這是在主機系統的流暢度與Docker 容器的效能之間取得平衡:
[wsl2]
memory=8GB # 依據電腦的總記憶體調整,建議設定為總量的 40%
processors=4 # 依據 CPU 的總核心數調整,建議設定為總量的一半
重啟 WSL 以套用設定:
這個設定並不會馬上生效,因此必須徹底關閉 WSL 才能讓它在下次啟動時讀取新設定。
wsl --shutdown
做完以上這些準備後再重新啟動 Docker Desktop。現在 WSL 2 會在我設定的資源限制下運作。
所有設定都已完成後,現在就來測試看看我們剛剛設定的是否都正確。
docker-compose up --build -d
--build
:此旗標會強制 Docker Compose 在啟動前,根據我們的 Dockerfile
重新建構 m2a-agent
服務的映像檔。-d
:代表在背景(Detached mode)執行,終端機可以繼續做其他操作。m2aagent
專案底下,除了原有的 m2a-n8n
,還多了一個名為 m2a-agent-app
的容器,並且兩者都處於執行中(Running)的狀態。http://localhost:7860
,譨看到熟悉的 Gradio 前端介面。✅ 完成項目
Dockerfile
.dockerignore
檔案,優化了映像檔的建構流程docker-compose.yml
,實現了程式碼熱重載與資料持久化docker-compose up --build -d
即可「一鍵啟動」整個 AI 會議助理後端系統今天我完成了從在「本機執行」到在「容器中執行」的突破,這個過程雖然充滿了錯誤除錯的挑戰,但也讓我對 Docker 的運作機制有了更深刻的理解。從 COPY
的時機、volumes
的必要性,到 VmmemWSL
的資源管理,每一步都是邁向專業開發流程的踏實足跡。
現在我的專案擁有了一個標準化、可攜、且易於分享的開發環境,看著兩個服務的啟動,整個系統流暢地正常工作,這種將複雜化繁為簡的感覺,充滿了成就感。
🎯 明天計劃
最佳化 Docker 映像,多階段建構縮減體積、層快取加速建構,使 M2A Agent 更輕量。