跳轉到

部署指南

本章節說明如何將 Taiwan Health MCP 伺服器部署至生產環境。本專案採 Container-first 策略,強烈建議使用 Docker 部署以確保環境一致性。

完整的變數清單見環境變數配置;本頁是按順序執行的部署流程

支援環境

  • 作業系統:Linux (Ubuntu/CentOS)、macOS、Windows (WSL2)
  • 容器平台:Docker Engine 24 以上 + Docker Compose v2(docker compose,非舊版 docker-compose
  • Node.js 版本:20 以上(裸機部署或本機開發時)。本專案已無 Python 執行期相依。

資源需求

項目 最低 建議 備註
CPU 2 core 4 core 以上 匯入與嵌入工作為主要負載
記憶體 8 GB 16 GB 以上 admin-worker 單一容器就設定了 --max-old-space-size=8192(FHIR IG 匯入需要)
磁碟 20 GB 100 GB 以上 PostgreSQL + MinIO 藥品資產(仿單 / 標籤 / 外觀圖)會持續成長

嵌入、OCR(MinerU)與分析 LLM 都是外部 HTTP 服務,不含在本 compose stack 內,需另行準備並在管理後台設定。

服務組成

docker compose up -d 會啟動下列服務:

服務 說明
nginx 對外入口(預設 :8080,由 WEB_PORT 設定)。把 /mcp/openapi.json/tools/*/admin/api/*/admin/ws/fhir-client/*/fhir-oauth/* 導向 app,其餘全部導向 web
web Next.js 前端:/admin 管理後台 SPA。
app Node MCP 伺服器 + 管理後台 REST API。只在 compose 內部網路上 expose 8000 埠,不對主機發佈。
admin-worker 背景工作執行器:所有匯入(含藥品三階段管線)與嵌入工作。
postgres PostgreSQL 16 + pgvector。
pgbouncer 連線池(transaction mode)。
redis 回應快取。
minio + minio-init 藥品資產物件儲存與 bucket 初始化。

資料匯入由管理後台觸發、在 admin-worker 內執行,已無獨立的 data-loader 容器。

應用流量只走 nginx

不要在文件或客戶端設定中使用 http://<host>:8000 —— app 沒有對主機開放該埠。 所有應用流量(含 MCP)都必須經由 http://<host>:8080

postgresminio 另有對主機發佈的埠,屬於維運用途,正式環境請見對外埠與強化。)


部署流程

步驟 1:取得程式碼

git clone https://github.com/healthymind-tech/Taiwan-Health-MCP.git
cd Taiwan-Health-MCP

步驟 2:建立 .env

cp .env.example .env

.env.example 的預設值不能直接上生產環境。以下是必須處理的項目:

2-1. 必填 / 必改變數

變數 為何必要 建議做法
POSTGRES_PASSWORD 未設定時 docker compose 會直接失敗(compose 以 :? 強制要求);沿用 .env.example 的預設值時 app 會拒絕啟動 openssl rand -hex 24
ADMIN_ENABLED 預設 true/admin唯一能匯入資料的途徑)。資料載入完成後可設 false 縮小公開部署的受攻擊面 依需求
ADMIN_USERNAME 管理後台帳號 預設 admin,可自訂
ADMIN_INITIAL_PASSWORD
ADMIN_PASSWORD_HASH
沒有憑證就無法登入 二擇一,見 2-2
ADMIN_SESSION_SECRET session cookie 簽章金鑰(知道它就能偽造任何使用者的登入,不需密碼);同時是 FHIR_SERVER_SECRET_KEY 的回退值。沿用 .env.example 的預設值時 app 會拒絕啟動 openssl rand -hex 32
MINIO_ACCESS_KEY / MINIO_SECRET_KEY 預設 minioadmin / minioadmin,等同無密碼 各自 openssl rand -hex 16
PUBLIC_BASE_URL 對外 origin,用於 OAuth2 redirect_uri 與 WebAuthn RP ID 推導 https://your-domain.example.com
PUBLIC_TOOLS_AUTH_MODE / PUBLIC_TOOLS_BEARER_TOKEN 預設 none,代表 /mcp/tools/* 完全不驗證 對外開放時設 bearer + openssl rand -hex 32
WEB_PORT 對外埠 預設 8080,依環境調整

一次產生所有隨機值:

echo "POSTGRES_PASSWORD=$(openssl rand -hex 24)"
echo "ADMIN_SESSION_SECRET=$(openssl rand -hex 32)"
echo "FHIR_SERVER_SECRET_KEY=$(openssl rand -hex 32)"
echo "MINIO_ACCESS_KEY=$(openssl rand -hex 16)"
echo "MINIO_SECRET_KEY=$(openssl rand -hex 16)"
echo "PUBLIC_TOOLS_BEARER_TOKEN=$(openssl rand -hex 32)"

2-2. 設定管理後台密碼

兩種方式擇一:

(a) 明文初始密碼(較簡單) —— 只在首次啟動、資料庫尚無憑證列時使用,之後永久忽略:

ADMIN_INITIAL_PASSWORD=your-strong-password

(b) 預先算好的雜湊

node -e "console.log('sha256\$' + require('crypto').createHash('sha256').update('change-me').digest('hex'))"
ADMIN_PASSWORD_HASH=sha256$$1a2b3c...

兩者同時存在時 ADMIN_INITIAL_PASSWORD 優先。密碼雜湊支援 sha256$<hex>pbkdf2_sha256$<iterations>$<salt>$<hex>

.env 中的每個 $ 都要寫成 $$

Docker Compose 會對 .env 做變數展開,單一 $ 會被當成變數參照。 當雜湊值以字母開頭時(例如 sha256$abc…),$abc… 會被當成未定義變數而靜默截斷成 sha256, 結果是登入永遠失敗且沒有明顯錯誤訊息。

請把每個 $ 都寫成 $$(Compose 會還原成單一 $)。pbkdf2_sha256$...$...$... 含多個 $,每個都要加倍。 密碼本身或 ADMIN_SESSION_SECRET$ 時同理。

2-3. 加密金鑰的一致性

FHIR_SERVER_SECRET_KEY 是外部 FHIR 伺服器 OAuth token / client secret 的 pgcrypto 對稱金鑰, 未設定時回退為 ADMIN_SESSION_SECRETcompose.yaml 已把它同時傳給 appadmin-worker兩者必須一致,否則 worker 解密會拋 Illegal argument to function(金鑰為空) 或 Wrong key or corrupt data(金鑰不符)。

2-4. 認清「只讀一次」的設定

seed-only 設定(MinIO、TFDA 爬蟲、FHIR package registry、worker 調校)只在首次啟動時 從 .env 讀取一次寫入 admin.app_settingsseedIfEmpty()ON CONFLICT DO NOTHING 逐鍵寫入),之後改 .env 一律無效。但它們事後的可改性並不相同

設定群組 首次啟動後如何修改
TFDA 爬蟲、FHIR package registry Admin → Settings 直接改(支援熱套用)
MinIO(Storage)Worker Tuning 後台唯讀,標示為 "Owned by the deployment (.env / compose)"。但因為值早已種子化進資料庫、程式也是讀資料庫,.env 同樣沒有效果 —— 只能直接 UPDATE admin.app_settings 再重啟對應服務。
模型端點(嵌入 / OCR / 分析 LLM) 完全不從 .env 讀取,只能在 Admin → Settings 設定

ADMIN_MAX_CONCURRENT_JOBS 是例外:它不進 admin.app_settings,worker 每次啟動都直接讀環境變數,改 .env 後重啟 worker 即生效。

MinIO 憑證務必在首次啟動前就設定好

MINIO_ACCESS_KEY / MINIO_SECRET_KEY 有兩個消費者,行為並不一致:

  • minio 容器每次啟動都會.env 讀取,作為 root 帳密(MINIO_ROOT_USER / MINIO_ROOT_PASSWORD);
  • app / admin-worker 用的是首次啟動時種子化進 admin.app_settings 的那份(seedIfEmpty()ON CONFLICT DO NOTHING 寫入,之後永不覆寫)。

首次啟動後才改 .env,會讓 MinIO 伺服器帳密與應用端儲存的設定不一致,導致物件儲存讀寫失敗。

而且 Settings 中的 Storage 群組是唯讀的(標示為 "Owned by the deployment (.env / compose)"), 無法從後台修正。若已經發生,只能直接改資料庫:

sql UPDATE admin.app_settings SET value = '<new-access-key>' WHERE group_key = 'minio' AND key = 'access_key'; UPDATE admin.app_settings SET value = '<new-secret-key>' WHERE group_key = 'minio' AND key = 'secret_key';

改完重啟 appadmin-worker

詳見環境變數配置

2-5. 最小可用 .env 範例

# --- 對外 ---
WEB_PORT=8080
PUBLIC_BASE_URL=https://taiwan-health-mcp.example.com

# --- 資料庫 ---
POSTGRES_DB=taiwan_health
POSTGRES_USER=mcp
POSTGRES_PASSWORD=<openssl rand -hex 24>

# --- MCP ---
MCP_PORT=8000
MCP_PATH=/mcp
PUBLIC_TOOLS_AUTH_MODE=bearer
PUBLIC_TOOLS_BEARER_TOKEN=<openssl rand -hex 32>

# --- 管理後台 ---
ADMIN_ENABLED=true
ADMIN_USERNAME=admin
ADMIN_INITIAL_PASSWORD=<強密碼;含 $ 請寫成 $$>
ADMIN_SESSION_SECRET=<openssl rand -hex 32>
ADMIN_COOKIE_SECURE=true          # TLS 在外部 proxy 終止時明確設定
FHIR_SERVER_SECRET_KEY=<openssl rand -hex 32>

# --- 物件儲存(seed-only:首次啟動才讀)---
MINIO_ENDPOINT=minio:9000
MINIO_ACCESS_KEY=<openssl rand -hex 16>
MINIO_SECRET_KEY=<openssl rand -hex 16>
MINIO_BUCKET=taiwan-health-drug-assets

步驟 3:建置與啟動

docker compose build          # 首次建置 app / admin-worker / web 三個映像
docker compose up -d

首次啟動時 PostgreSQL 容器會自動套用 db/schema.sqlminio-init 會建立 bucket 後結束(狀態顯示為 exited 屬正常)。

步驟 4:驗證

確認容器狀態,postgres / redis / minio / pgbouncer 應為 healthy

docker compose ps

確認服務與各模組狀態:

curl http://localhost:8080/openapi.json | head  # 目前已註冊的工具

啟動失敗時先看日誌:

docker compose logs -f app admin-worker

步驟 5:登入管理後台並設定模型端點

瀏覽 http://<host>:8080/admin,以步驟 2 的帳密登入。

登入後到 Settings 設定外部模型端點(這些不能.env 設定):

子頁 設定內容 未設定的後果
Embedding 嵌入端點 / 模型(預設情境為 Ollama qwen3-embedding 搜尋退回關鍵字模式;中文關鍵字搜尋幾乎查不到東西
Analysis LM 藥品仿單抽取用 LLM 藥品分析階段無法執行
OCR MinerU 服務位址 仿單 PDF 無法 OCR

模型設定可用 Settings → Backup & restore 的 JSON Export / Import 在不同環境間搬移。詳見管理後台

步驟 6:載入資料

在 Modules 頁籤依模組匯入;需授權的來源檔(ICD-10、LOINC、SNOMED CT、RxNorm、FHIR IG)須先上傳,藥品 / 健康補充品 / 食品營養則由 API 自動抓取。

手動排入的 drug_enrichment 不受批次上限保護

DRUG_AUTOCHAIN_BATCH_LIMIT(預設 200)只作用於自動串接產生的工作。 手動排入且未帶 limitdrug_enrichment 會爬完整個待辦佇列, 對 TFDA 網站發出數萬次請求。

步驟與排程說明見快速開始背景工作與排程


日常維運

更新與重新部署

只改了應用程式碼時,不需要重啟整個 stack:

git pull
docker compose build app web admin-worker
docker compose up -d --no-deps app web admin-worker

改了 nginx/nginx.conf

docker compose restart nginx

改了 .env 中的 bootstrap 變數(DB / Redis / MCP / ADMIN_*)才需要重建容器:

docker compose up -d

(改 seed-only 區塊無效,見 2-4。)

資料庫遷移

首次啟動時 db/schema.sql 會自動套用。既有環境升級後需自行套用 db/migrations/ 下的增量變更,依檔名日期順序:

docker compose exec -T postgres \
  sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -v ON_ERROR_STOP=1' \
  < db/migrations/20260802_llm_call_log.sql

依序套用全部(已套用過的檔案請自行剔除,本專案未內建遷移版本追蹤):

for f in $(ls db/migrations/*.sql | sort); do
  echo "applying $f"
  docker compose exec -T postgres \
    sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -v ON_ERROR_STOP=1' < "$f"
done

帳號密碼由容器內部環境變數展開

上面的 $POSTGRES_USER / $POSTGRES_DB在容器內展開的(所以整段包在單引號的 sh -c 裡), 不需要在主機端 export 或手動代入。

直連 postgres 而非 pgbouncer

遷移請走 postgres 容器。pgBouncer 為 transaction mode,不適合執行 DDL 批次。

備份

兩種做法:

(a) 管理後台(推薦) —— Settings → Backup & restore 可勾選設定與憑證、PostgreSQL database、MinIO object storage,排入 system_backup 背景工作,產出的 ZIP 寫回 MinIO 的 system-backups/ prefix,可從備份歷史下載。

(b) 手動

# PostgreSQL(custom format,供 pg_restore 使用)
docker compose exec -T postgres \
  sh -c 'pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Fc' > backup-$(date +%F).dump

# MinIO 物件(volume 層級)
docker run --rm -v taiwan-health-mcp_minio_data:/data -v "$PWD":/backup \
  alpine tar czf /backup/minio-$(date +%F).tar.gz -C /data .

volume 名稱前綴為 compose 專案名(預設是目錄名),實際名稱以 docker volume ls 為準。

備份內含 API keys、登入憑證與醫療資料,應視同 production secret 管理

對外埠與強化

compose.yaml 目前對主機發佈的埠:

服務 綁定 正式環境建議
nginx ${WEB_PORT} → 80 全介面 前面加 TLS 反向代理
postgres 5432 全介面 改綁 127.0.0.1:5432:5432 或移除
minio 9000 全介面 改綁 127.0.0.1:9000:9000 或移除
minio console 9001 127.0.0.1 保持現狀
redis 6379 127.0.0.1 保持現狀
app metrics ${METRICS_PORT} 127.0.0.1 保持現狀

上線前的檢查清單:

  • POSTGRES_PASSWORDADMIN_SESSION_SECRETFHIR_SERVER_SECRET_KEY 皆為隨機強值
  • MinIO 憑證已改掉 minioadmin 預設值
  • postgres:5432 / minio:9000 已改綁 127.0.0.1 或以防火牆阻擋
  • PUBLIC_TOOLS_AUTH_MODE=bearer 並設定高熵 token(若 /mcp 對公網開放)
  • TLS 已在前置 proxy 終止,且 ADMIN_COOKIE_SECURE=true
  • .env 權限收斂(chmod 600 .env)且未進版控

docker-compose.override.yml 覆寫埠綁定,不必改動 compose.yaml

services:
  postgres:
    ports:
      - "127.0.0.1:5432:5432"
  minio:
    ports:
      - "127.0.0.1:9000:9000"

同一份 override 也適合放資源限制

疑難排解

症狀 可能原因 處置
docker compose up 立刻失敗並提示 POSTGRES_PASSWORD is required .env 未設定該變數 補上(compose 以 :? 強制要求)
/admin 回 404 ADMIN_ENABLED 被設為 false 改回 truedocker compose up -d
/admin 回 503 已啟用但認證變數不齊 看啟動日誌的 missing 欄位,補齊後重啟
管理後台密碼正確卻登入失敗 .env 中的 $ 未寫成 $$,雜湊被截斷 見 2-2 的警告框
改了 .env 的密碼卻沒生效 憑證在首次啟動即寫入 admin.admin_credentials,seed 為 ON CONFLICT DO NOTHING 改用 Settings → Privacy 修改密碼
worker 拋 Illegal argument to function / Wrong key or corrupt data FHIR_SERVER_SECRET_KEY 為空或 app 與 worker 不一致 見 2-3
改了 .env 的 TFDA / registry 設定沒有效果 該群組為 seed-only,只在首次啟動讀取 改到 Admin → Settings
改了 .env 的 MinIO / worker 調校沒有效果 seed-only,且後台為唯讀群組 直接 UPDATE admin.app_settings 後重啟服務(見 2-4)
中文搜尋幾乎查不到結果 嵌入端點未設定,退回關鍵字模式 於 Settings → Embedding 設定端點
IG 匯入時 worker OOM NODE_OPTIONS 被調低,或容器記憶體上限不足 保留 --max-old-space-size=8192,容器上限給到 10G
http://<host>:8000 連不上 設計如此,app 不對主機開埠 一律使用 http://<host>:${WEB_PORT}

延伸閱讀

架構與容器部署

基礎設施拓樸、容器組成與啟動流程。

環境變數配置

各項系統參數的完整清單,含 bootstrap 變數(.env)與 seed-only 設定(首次啟動後改於 Admin → Settings 管理)。

效能與監控

高併發場景的優化建議、連線池與快取策略、Prometheus 監控。

公開頁面已移出本專案

對外的宣傳頁與法務頁(//status/privacy/dpa)已改由獨立的宣傳網站專案提供, 不再由 web 服務發送。/privacy 是登記於 Anthropic Connectors Directory 的網址, 移轉後務必在 nginx/nginx.conf 補上 301 導向(該檔已留有 TODO 區塊)。