老師的話

佈署Microservice架構於Docker。將【Jev 資料庫管理治理平台】與【MCP Server 治理平台】與SQL Server 2025伺服器佈署完成。加上原先的n8n已經佈署Docker,尚有兩個治理平台尚未完成與佈署,那就是【AI Agent skill 治理平台】與【Microserive API溶斷與治理平台】設計與準備中… 待所有平台完成之後,我在考量是將Openclaw或者採用Cowork或者chatgpt codex等進行一個Workflow Hub配置。(我有重新調配那一台mac mini m4了)

封面:Docker 微服務治理平台架構
封面(p.01):兩套 .NET 10 治理平台 × 兩個 SQL Server 2025 容器,資料來源為兩套 compose.yaml/Dockerfile/初始化腳本,與 2026-09-27 實機 docker ps、network inspect、網站畫面。
簡報大綱:六個段落
大綱(p.02):六個段落,由外而內——先看整體,再看每個堆疊,最後落到執行期流程與維運。
課程地圖

第 01 課・p.03–04

全景與現況

先看整台主機長什麼樣子:一台 Windows 11 主機、兩個各自獨立的 Docker Compose 專案,再對照 2026-09-27 當天的實機運行狀態。

一台主機,兩個完全獨立的堆疊

全景總覽:一台主機、兩個獨立堆疊
圖 p.03 全景總覽——兩套 Compose 專案各自擁有 SQL Server 2025 容器與 bridge 網路,彼此不共用任何資源;對外埠一律綁在 127.0.0.1。

這台 Windows 11 主機用 Docker Desktop(WSL2 後端)跑了兩個完全不相干的專案。一個是 mcpplatform(MCP Server API 平台,對外埠 5800,https),一個是 dbgov15(Jev SQL Server 結構治理平台,對外埠 5801,http)。兩邊各帶一台 SQL Server 2025(17.0.5005.3 Developer 版),主機用 14333 和 14334 這兩個埠直連容器內的 SQL Server 給 SSMS/sqlcmd 用。埠號對應是:5800→5800、14333→1433、5801→5801、14334→1433——左邊是主機埠,右邊是容器裡的埠。

圖上有個小細節:本機另外還有一台不在 Docker 裡的 SQL Server 佔用著 1433,這正是為什麼兩個容器化的 SQL Server 要改用 14333/14334 對外,不能直接用 1433。而且所有對外埠都只綁 127.0.0.1,區網裡的其他電腦連不進來,只有這台主機自己看得到。

mcpplatform 裡面藏了一個 demoapis 容器,它「共用網路命名空間」掛在 web 底下,只聽 localhost:5080,不對主機開放——這個設計是第 03 課的重點,這裡先記住它存在。db-init 是兩邊都有的一次性容器,跑完初始化就 Exited (0) 收工,不是常駐服務。

2026-09-27 實機運行狀態

實機運行狀態 2026-09-27
圖 p.04 六個常駐容器全部運作中,兩個 db-init 以 Exited (0) 表示初始化成功;兩個 SQL Server 與 dbgov15 web 通過健康檢查。
2Compose 專案
8容器(6 常駐+2 一次性)
2SQL Server 2025 執行個體
4具名 Volume
3自建映像
4對外埠(皆 127.0.0.1)

老師用 docker ps -a --filter label=com.docker.compose.project=… 分別查了兩個專案,那一刻量到的狀態是:

$ docker ps -a --filter label=com.docker.compose.project=mcpplatform
NAMES                     STATUS                   PORTS
mcpplatform-demoapis-1    Up 6 hours
mcpplatform-web-1         Up 6 hours               127.0.0.1:5800->5800/tcp
mcpplatform-db-init-1     Exited (0)
mcpplatform-sqlserver-1   Up 6 hours (healthy)     127.0.0.1:14333->1433/tcp

$ docker ps -a --filter label=com.docker.compose.project=dbgov15
NAMES                 STATUS                    PORTS
dbgov15-worker-1      Up 42 minutes
dbgov15-web-1         Up 42 minutes (healthy)
dbgov15-db-init-1     Exited (0)
dbgov15-sqlserver-1   Up 46 minutes (healthy)   127.0.0.1:5801->5801/tcp,
                                                127.0.0.1:14334->1433/tcp

用 docker stats 看記憶體用量:mcp sqlserver 1.15G、dbgov sqlserver 1018M、dbgov web 212M、mcp web 179M、dbgov worker 77M、mcp demoapis 29M。兩個 SQL Server 合計佔了主機總記憶體約 81%(2.2 GB/2.65 GB),兩個 .NET 網站加起來反而不到 400 MB——SQL Server 才是這套架構真正吃記憶體的地方。當時主機記憶體 15.27 GiB,CPU 全部低於 11%,機器很閒。

實機運行狀態:Docker Desktop 截圖

這三張是老師自己貼的 Docker Desktop 實機畫面,對照上面的容器清單看更直覺。

Docker Desktop 容器總覽
實機截圖 容器總覽:workspace、n8n、mcpplatform、dbgov15 四個 Compose 專案並存,CPU 用量 2.01%/2400%(24 顆 CPU 可用),記憶體 3.15GB/14.92GB。
Docker Desktop dbgov15 堆疊明細
實機截圖 dbgov15 堆疊明細:sqlserver、db-init、web、worker 四個容器,右側是 sqlserver 的即時日誌(載入 xplog70.dll 這類初始化訊息)。
Docker Desktop mcpplatform 堆疊明細
實機截圖 mcpplatform 堆疊明細:web 對外映射 5800:5800、sqlserver 對外映射 14333:1433,右側日誌能看到 web 呼叫 http://localhost:5080/api/courses 拿到 200 的紀錄。

本課重點

  • 一台主機上兩個完全獨立的 Compose 專案,各自的 SQL Server、網路與 Volume 互不共用。
  • 對外埠一律只綁 127.0.0.1:5800、14333、5801、14334,區網其他電腦連不到。
  • 8 個容器裡有 6 個常駐、2 個是跑完就收工的一次性 db-init。
  • 兩個 SQL Server 合計佔主機記憶體約 81%,是這套架構的用量大頭。
  • Docker Desktop 截圖可以直接跟 docker ps 的清單對照,驗證容器狀態一致。
01 / 06

第 02 課・p.05–08

兩個堆疊的拓樸

把第 01 課看到的全景拆開,分別看 5800 mcpplatform 與 5801 dbgov15 各自的容器、網段、Volume 與掛載細節。

Stack A · 127.0.0.1:5800 MCP Server API 平台拓樸

mcpplatform 拓樸圖
圖 p.05 四個服務、兩個具名 Volume、一個 bridge 網路;demoapis 掛在 web 的網路命名空間,因此網段裡只看得到兩個容器。

這個堆疊的 bridge 網路叫 mcpplatform_default,網段 172.24.0.0/16,靠 Docker 內建 DNS 用服務名稱找到彼此。web(mcpplatform-web:latest)是這個網路命名空間的擁有者,佔 172.24.0.3;demoapis 用 network_mode: service:web 加入,所以它自己沒有獨立 IP。sqlserver 獨立佔 172.24.0.2。

web 身兼三個角色:ASP.NET Core MVC 後臺與會員中心、OAuth 授權伺服器(OpenIddict)、/mcp 端點與 Gateway(帶 SsrfGuard)。它以 Production 模式跑 https://+:5800,用非 root 的 $APP_UID 執行。demoapis 是示範上游 API,提供 GET /api/products、GET /api/courses、POST /api/tickets 三支端點,只聽 localhost:5080,主機與其他容器都連不到,只有同命名空間裡的 web 能經 Gateway 呼叫它。

設定與祕密走兩條路進來:.env 用變數替換帶入 MSSQL_SA_PASSWORD、MCP_APP_PASSWORD、PFX_PASSWORD(BOOTSTRAP_* 已註解,代表管理者已經建立過);secrets/ 目錄下的 https.pfx(給 Kestrel)與 token-signing.pfx(給 OpenIddict)唯讀掛進 web:/secrets:ro。sql/deploy/ 底下的三支腳本唯讀掛進 db-init:/sql:ro。init-secrets.ps1 負責產生 .env 與兩張 PFX,已存在的檔案不覆蓋。

Stack A · 元件職責與相依

mcpplatform 元件職責與相依
圖 p.06 一次建置產生兩個執行映像;平台自身資料全部放在 SQL Server,網站只拿到最小權限帳號 mcp_app。
服務映像/角色相依條件
sqlservermssql/server:2025-latest Developer;平台唯一資料庫,主機 14333 供 SSMS 管理—(healthcheck:sqlcmd SELECT 1)
db-init同上映像,只跑 sqlcmd;建庫 → 冪等 Migration → 建立 mcp_appsqlserver: service_healthy
webmcpplatform-web(393 MB);MVC 後臺、OpenIddict、/mcp、Gatewaydb-init: service_completed_successfully
demoapismcpplatform-demoapis(340 MB);示範上游 API,network_mode: service:webweb: service_started

平台內建一個「開發環境檢查」頁(/SetupCheck),直接把共用命名空間的設計證明給你看:頁面顯示它以容器內位址 sqlserver,1433 連上資料庫、執行環境是 Production、.NET 版本 10.0.12,而且經由 http://localhost:5080 呼叫 demoapis 拿到 200——這一頁就是「web 跟 demoapis 真的在同一個網路命名空間裡」的活證據。畫面擷取於 2026-09-27 21:11(臺灣時間),系統管理員登入。

Stack B · 127.0.0.1:5801 Jev SQL Server 結構治理平台拓樸

dbgov15 拓樸圖
圖 p.07 web、worker、db-init 全部加入 sqlserver 的網路命名空間,所以 5801 是發布在 sqlserver 容器上;bridge 網段裡只有 sqlserver 一個成員。

這個堆疊反過來:sqlserver 才是網路命名空間 B 的擁有者,佔 172.25.0.2,也是 172.25.0.0/16 網段裡唯一有獨立 IP 的成員。web(MVC+Web API,負責提案與審核)與 worker(同一個映像、換一個 entrypoint,跑 DeploymentWorker,每 2 秒輪詢一次)都掛進這個命名空間,用 http :5801(Development 模式)對外。因為共用命名空間的服務不能各自宣告埠,所以 5801 這個埠號是宣告在 sqlserver 服務上的。

db-init 依 sql/00、01、04、05、06、02、07、09、10、08 這個順序(第 03 課會細講為什麼要照這個順序)跑 init-db.sh,唯讀掛入 docker/.env 裡的 SA_PASSWORD、READER/REHEARSAL/DEPLOYER_PASSWORD、ADMIN_INITIAL、USER01 這些值。docker/Dockerfile 是一個映像同時含 web 與 worker,建置 context 是範例根目錄。

sqlserver 容器裡放了兩個受管目標資料庫:DbGovernanceLab_Dev(開發,部署對象是 dbgov_deployer)與 DbGovernanceLab_Test(測試,演練對象是 dbgov_rehearsal),另外還有 master/msdb 放三個 SQL 登入與 backupset 備份紀錄。

Stack B · 元件職責:平台資料與受管資料分離

dbgov15 元件職責
圖 p.08 平台自己的帳號、提案、審核、部署與稽核存在 SQLite;SQL Server 2025 是被探索、演練與部署的「受管目標」。
服務映像/角色相依條件
sqlserver受管實驗庫 Dev/Test;網路命名空間擁有者—(healthcheck 10s×20)
db-init同上映像,執行 init-db.sh,依 README「從零安裝」順序跑 sql/sqlserver: service_healthy
webdbgov15-platform(533 MB);Migration、Bootstrap、MVC/APIdb-init: service_completed_successfully
worker同一映像,覆寫 entrypoint;背景部署、驗證、漂移web: service_healthy

worker 要等 web 健康才啟動,理由很直白:建資料表(Migration)是 web 的工作,worker 不能在資料表建立前就開始輪詢 DeploymentJobs。這個堆疊把「平台自己的東西」跟「被治理的東西」徹底分開:平台資料(16 張表、11 個 Migration 的 governance.db,加上 Data Protection 金鑰)放 SQLite;DbGovernanceLab_Dev/_Test 是被探索、演練、部署的對象,平台不會把自己的資料寫進去。首頁畫面直接標示了這個分工。

本課重點

  • 5800 網路命名空間擁有者是 web,demoapis 加入;5801 反過來,擁有者是 sqlserver。
  • 兩個堆疊各自的 bridge 網段:172.24.0.0/16(mcpplatform)與 172.25.0.0/16(dbgov15)。
  • mcpplatform 一次建置產生 web(393 MB)與 demoapis(340 MB)兩個映像;dbgov15 一個映像(533 MB)同時裝著 web 與 worker。
  • dbgov15 的 worker 依賴 web: service_healthy,因為資料表要先由 web 的 Migration 建好。
  • 平台資料與受管資料徹底分離:5800 的 SQL Server 是平台系統庫,5801 的平台資料在 SQLite、SQL Server 只是受管目標。
02 / 06

第 03 課・p.09–12

網路與啟動

兩個堆疊都用了「共用網路命名空間」這一招,但方向剛好相反;再看 depends_on 怎麼把啟動順序卡緊,以及 db-init 的腳本怎麼跑。

共用網路命名空間:方向相反的兩種設計

共用網路命名空間對照
圖 p.09 network_mode: "service:X" 讓容器直接使用 X 的網路介面與回送位址——兩個堆疊用同一招,但「誰當擁有者」剛好相反。

5800 這邊,擁有者是 web(172.24.0.3),demoapis 用 localhost:5080 讓 web 從自己的回送介面 lo 呼叫得到;連資料庫則是走 bridge 網路的 DNS,寫 Server=sqlserver,1433。埠宣告在 web 上,網段成員是 web 與 sqlserver 兩個。這樣設計的目的,是讓「上游呼叫」在程式眼中一定是明寫的 localhost,才能通過後面會講到的 SsrfGuard 檢查;副作用是只重啟 web 就會讓 demoapis 跟著斷網。

5801 這邊反過來,擁有者是 sqlserver(172.25.0.2,網段裡唯一成員),web、worker、db-init 全部加入,所以它們連 SQL Server 都寫 localhost,1433,走的是同一個回送介面。埠宣告在 sqlserver 上,網段成員只有 sqlserver 自己。目的是讓平台種子資料裡寫死的 localhost,1433 目標,以及目標登錄政策,都不必為了容器化而修改;代價是只重啟 sqlserver 就會讓 web/worker 跟著斷網。

老師在 2026-09-27 實測 docker network inspect:mcpplatform_default(172.24.0.0/16)看得到兩個成員——mcpplatform-web-1=172.24.0.3、mcpplatform-sqlserver-1=172.24.0.2;dbgov15_default(172.25.0.0/16)只看得到一個——dbgov15-sqlserver-1=172.25.0.2。共用命名空間的那些容器(demoapis、web、worker、db-init)沒有自己的 IP,所以不會出現在清單裡,這也直接驗證了兩邊「誰是擁有者」的設計。

為什麼要共用命名空間:不改程式碼,也不放寬安全規則

共用命名空間的理由
圖 p.10 兩個堆疊都是為了保留應用程式原本對「localhost」的假設,讓容器化只動部署設定。

5800 那邊的關鍵是 SsrfGuard 的判斷路徑:Gateway 要呼叫上游網址時,先看是不是在 AllowedEndpoints 白名單裡,不在就直接拒絕;在的話,再看主機名稱是不是明寫 localhost 或回送 IP 字面值,是就允許;不是的話再看解析結果是不是落在私有網段(含 172.16.0.0/12),是就拒絕,不是才允許公開位址。如果把 demoapis 獨立成自己的容器,它會落在 172.24.x.x 這種私有網段裡,反而會被這道規則擋下——所以要讓它跟 web 共用命名空間,變成貨真價實的 localhost。這段判斷寫在 McpPlatform.Infrastructure/Gateway/SsrfGuard.cs,在建立 TCP 連線的當下檢查。

5801 那邊的理由不一樣:平台的種子資料裡,受管目標伺服器本來就寫死是 localhost,1433,目標登錄政策也只允許 localhost 與 *.lab。讓 web/worker 加入 sqlserver 的命名空間之後,容器裡的 localhost 就真的變成 SQL Server 2025 了——不必改種子資料、不必改登錄政策、也不必改連線字串,跟「本機直接安裝」教材原本的步驟完全一致。代價寫在圖上:web/worker 的生命週期被綁在 sqlserver 身上。

啟動順序與相依條件

啟動順序與相依條件
圖 p.11 depends_on 的三種條件把「行程啟動」、「真的可連線」、「初始化成功」分開;任何一步失敗,後面的服務就不會起來。

mcpplatform(5800)的順序是:① sqlserver 啟動,healthcheck 用 sqlcmd -Q "SELECT 1",每 10 秒探一次、最多 12 次、start_period 20 秒,過了才算 healthy;② db-init 依序跑 00 建庫、01 Migration(-x 關閉變數替換)、02 建立 mcp_app,sqlcmd -b 讓任何錯誤都變成非 0 結束,成功則 Exited (0)、算 completed_successfully;③ web 用 mcp_app 連線、植入 Scope(Bootstrap 已停用),對外 https://+:5800,本身沒有 healthcheck,只要求 started;④ demoapis 加入 web 的命名空間,聽 localhost:5080。

dbgov15(5801)的順序是:① sqlserver 建立命名空間與埠 5801,同樣用 sqlcmd -Q "SELECT 1",每 10 秒探一次、最多 20 次;② db-init 依序建兩個實驗庫、三個登入、測試庫備份,set -euo pipefail 搭配 -b,成功 Exited (0);③ web 套用 11 個 Migration(SQLite)、建立 admin/user01/示範目標,靠 GET /health/ready → 200 判斷健康,每 10 秒探一次、最多 12 次;④ worker 要等資料表已經存在才開始輪詢 DeploymentJobs,working_dir 是 /app/worker。圖上用綠色箭頭表示「需要條件成立才啟動」,灰色箭頭表示「只要前一個容器行程已啟動」。

db-init:用同一組 SQL 腳本完成可重複的初始化

db-init 腳本順序
圖 p.12 兩邊都沿用「非 Docker 安裝」的同一組腳本,全部冪等;sa 密碼以環境變數 SQLCMDPASSWORD 傳入,不出現在命令列。

mcpplatform 只有三支腳本:00_create_database.sql(-v DbName=MCPServerManag_Prod)、01_migrate_idempotent.sql(EF 產生的冪等腳本,31.6 KB,-x 關閉 $(…) 變數替換)、02_create_app_login.sql(建立 mcp_app,給 db_datareader 加 db_datawriter)。三支都內嵌在 compose 的 entrypoint(bash -ec)裡跑。

dbgov15 的 init-db.sh 要依相依順序跑 13 次:00 建立實驗資料庫(master、Dev+Test);01 示範領域結構(Dev、Test 各跑一次);04、05、06 補充物件(Dev、Test 各一次)——這裡的順序是關鍵,因為 04 的跨庫 View 要參照 Test.dbo.Customers,所以兩個庫的 01 必須都先跑完;再來是 02 建立 dbgov_reader(唯讀探索,Dev+Test)、07 建立 dbgov_rehearsal(Test 演練,ddladmin)、09 建立 dbgov_deployer(Dev 部署,ddladmin);08 備份測試庫(COPY_ONLY),因為演練頁的 Backup 步驟要查得到 msdb.dbo.backupset 的備份證據。如果登入已經存在,重跑會改成 .env 裡的密碼並解除鎖定——換句話說,重跑 db-init 就等於輪替一次密碼。

兩邊的 sqlcmd 都帶 -C(信任自簽憑證)與 -b(錯誤即非 0 結束)這兩個旗標。

本課重點

  • network_mode: "service:X" 兩邊都用,但擁有者相反:5800 是 web,5801 是 sqlserver。
  • 5800 的設計是為了滿足 SsrfGuard 對「明寫 localhost」的要求;5801 是為了讓寫死的 localhost,1433 種子資料不必改。
  • depends_on 有三種條件:行程已啟動(started)、健康檢查通過(healthy)、一次性容器成功完成(completed_successfully)。
  • 兩邊的 db-init 都沿用「非 Docker 安裝」原有的 SQL 腳本,全部設計成冪等,可以安全重跑。
  • dbgov15 的初始化順序不能亂——04 的跨庫 View 依賴 01 先在兩個庫都跑完。
03 / 06

第 04 課・p.13–16

建置資料與祕密

映像怎麼建出來、資料放在哪個 Volume、兩個 SQL Server 帳號權限怎麼切、祕密怎麼從主機流進容器——這四件事決定了系統重建之後還剩下什麼。

多階段 Dockerfile:SDK 只在建置時存在

多階段 Dockerfile
圖 p.13 兩邊都是 sdk:10.0 建置 → aspnet:10.0 執行、非 root 帳號;差別在「一次建置出兩個映像」與「一個映像放兩支程式」。

mcpplatform 只有一個 build 階段,但輸出兩個 target:build 用 sdk:10.0,COPY . .、restore --locked-mode,然後 publish Web 到 /out/web、publish DemoApis 到 /out/demoapis;target web 用 aspnet:10.0,建 /app/keys 並 chown $APP_UID,以 USER $APP_UID 執行、EXPOSE 5800;target demoapis 同樣以 aspnet:10.0、USER $APP_UID 執行。

dbgov15 走的是「先還原套件當快取層」的路線,最後一個映像同時裝 web 與 worker:build 階段先 COPY 五個 *.csproj、restore Web、Worker(可被快取),再 COPY src/、publish 出 /app/web 與 /app/worker(用 publish 而不是 build,是因為 MapStaticAssets 需要清單檔);runtime 階段也是 aspnet:10.0,建 /data/keys 並 chown app,USER app,ASPNETCORE_HTTP_PORTS=5801,預設 ENTRYPOINT 是 DbGovernance.Web,worker 是覆寫這個 entrypoint 跑出來的。

用 docker images 看實際大小:mssql/server:2025 是 2.42G;dotnet/sdk:10.0(僅建置階段用,1.3G)不會進入任何執行映像;dotnet/aspnet:10.0 基底 340M;dbgov15-platform 533M;mcpplatform-web 393M;mcpplatform-demoapis 340M——demoapis 幾乎就等於 aspnet 基底大小,因為它本來就很小。全部基底映像都來自 mcr.microsoft.com,不需要 Docker Hub 登入。

資料持久化:4 個具名 Volume,資料庫與金鑰必須成對

資料持久化 Volume
圖 p.14 容器可以隨時重建;真正要保護的是 Volume。兩邊都把 Data Protection 金鑰放進 Volume,否則重建容器就會讓所有人被登出。

mcpplatform 有兩個 Volume:mssql-data(/var/opt/mssql)放 MCPServerManag_Prod——會員、API、授權、呼叫紀錄、OpenIddict Token/Client、03_backup.sql 的備份檔;dp-keys(/app/keys)放登入 Cookie、防偽權杖、已加密的上游 API 金鑰,這個 Volume 一旦遺失,上游金鑰就永久無法解密,所以要跟資料庫成對備份。網站本身無狀態,資料全部在 SQL Server。

dbgov15 也有兩個:platformdata(web+worker 共用,/data)放 governance.db(SQLite WAL 模式)——帳號、提案、決策、審核、部署工作、時間線、稽核,以及 keys/ 目錄下的 Data Protection 金鑰;sqldata(/var/opt/mssql)放 DbGovernanceLab_Dev/_Test 這兩個受管目標與三個 SQL 登入。平台資料在 SQLite,SQL Server 只放受管目標,跟第 02 課看到的分工一致。

唯讀繫結掛載方面:5800 是 ./secrets → /secrets 與 sql/deploy → /sql;5801 是 ../sql → /sql 與 init-db.sh → /init。這些只影響初始化與憑證,不存放執行期資料。有個 Linux 特有的坑:Linux 沒有 Windows 的 DPAPI,所以 Data Protection 金鑰的 XML 是以明文存在 Volume 裡的,需要限制主機上誰能讀到這個 Docker Volume。

老師在 2026-09-27 實測 docker volume ls,看到四個:mcpplatform_mssql-data、mcpplatform_dp-keys、dbgov15_sqldata、dbgov15_platformdata。備份原則是:5800 用 03_backup.sql 備份資料庫並同時保存 dp-keys;5801 用 tools/backup-platform.cs 取得 SQLite 一致備份,連同 keys/ 一起保存。docker compose down 會保留 Volume,down -v 才會連資料庫與金鑰一起刪除——這是一個一旦下錯指令就回不了頭的動作,操作前一定要先確認要不要留資料。

兩個 SQL Server 2025 執行個體與最小權限登入

SQL Server 登入權限
圖 p.15 同一個映像(17.0.5005.3 Enterprise Developer Edition),不同用途:一個是平台的系統資料庫,一個是被治理的目標伺服器。sa 只給初始化使用。

mcpplatform-sqlserver 主機 127.0.0.1,14333,容器內 sqlserver,1433:sa 只給 sqlserver、db-init 用,是系統管理員,web 拿不到;mcp_app 給 web 用,權限是 db_datareader+db_datawriter,不能建表、改表、刪表。MCPServerManag_Prod 這個資料庫的 Migration 全部由 db-init 以 sa 套用,網站啟動時不會去改結構。

dbgov15-sqlserver 主機 127.0.0.1,14334,容器內 localhost,1433:sa 只給 db-init 與健康檢查用,只用來執行 sql/ 腳本,平台本身不用 sa;dbgov_reader 給 web、worker 用,CONNECT+VIEW DEFINITION,讀相依性,對 DbGovernanceLab_Dev 和 DbGovernanceLab_Test 都能用;dbgov_rehearsal 給 web 用,只在 DbGovernanceLab_Test 上有 db_ddladmin+SELECT dbo,還能唯讀查 msdb.backupset;dbgov_deployer 給 web、worker 用,只在 DbGovernanceLab_Dev 上有 db_ddladmin+SELECT dbo+VIEW DEFINITION。每個登入只拿到它那一步需要的權限:探索讀不到資料列、演練只能碰 Test、部署只能碰 Dev,三者都不是 db_owner。

兩邊用的都是 mcr.microsoft.com/mssql/server:2025-latest(2.42 GB),環境變數 ACCEPT_EULA=Y、MSSQL_PID=Developer、TZ=Asia/Taipei,健康檢查工具是 /opt/mssql-tools18/bin/sqlcmd -C(信任自簽憑證)。權限的來源是 dbgov15 的 sql/02、07、09 與 mcpplatform 的 sql/deploy/02。

祕密與設定的流向

祕密與設定流向
圖 p.16 祕密只存在主機的 .env 與 secrets/,以環境變數或唯讀掛載在執行時注入;.dockerignore 排除它們,映像可以安全分享。
mcpplatform 祕密sqlserverdb-initweb
MSSQL_SA_PASSWORD●●—
MCP_APP_PASSWORD—●●
PFX_PASSWORD——●
https.pfx/token-signing.pfx——/secrets:ro
BOOTSTRAP_ADMIN_*——已註解
dbgov15 祕密sqlserverdb-initwebworker
SA_PASSWORD●●——
READER_PASSWORD—●●●
DEPLOYER_PASSWORD—●●●
REHEARSAL_PASSWORD—●●—
ADMIN_INITIAL/USER01——●—

表格裡 ● 代表這個容器會拿到該值作為環境變數注入,— 代表拿不到。mcpplatform 的祕密由 init-secrets.ps1 隨機產生,已存在的檔案不覆蓋;token-signing.pfx 固定從檔案載入,容器重建後舊 Token 仍然可以驗證。dbgov15 用 docker/.env(靠 x-app-env 錨點共用給多個服務),admin 帳號一旦已經存在,ADMIN_INITIAL 的初始密碼就不再生效,admin 首次登入必須改密碼。

環境變數到 .NET 設定的轉換規則是雙底線等於冒號,例如 ConnectionStrings__DefaultConnection 對應 ConnectionStrings:DefaultConnection、TargetSecrets__lab-deployer__Password 對應 TargetSecrets:lab-deployer:Password。${VAR:?訊息} 這種寫法在缺值時會讓 docker compose 直接中止,不會啟動一個設定殘缺的半殘網站。

本課重點

  • 兩邊都是多階段建置、非 root 執行;差別在「一次建置兩個映像」vs.「一個映像裝兩支程式」。
  • 4 個具名 Volume:mssql-data、dp-keys、platformdata、sqldata;資料庫與 Data Protection 金鑰要成對備份。
  • docker compose down 保留 Volume,down -v 會連資料庫與金鑰一起刪除——動手前先確認。
  • 每個 SQL 登入只拿它那一步需要的最小權限,sa 只給初始化用,網站和 worker 都拿不到 sa。
  • 祕密只活在主機的 .env 與 secrets/,以環境變數或唯讀掛載注入,.dockerignore 確保它們不會進映像。
04 / 06

第 05 課・p.17–19

執行期流程

前面幾課看的是「架構長什麼樣」,這一課看「一次呼叫、一次部署真的跑起來時,資料怎麼流動」。

5800 執行期:OAuth → MCP 工具 → 上游 API

5800 執行期流程
圖 p.17 MCP Client 以授權碼+PKCE 取得 Token,再呼叫 /mcp;web 在同一個命名空間裡以 localhost:5080 呼叫 demoapis。

授權階段一共七步:MCP Client 先 ① GET /mcp(沒帶 Token),拿到 ② 401 加上 oauth-protected-resource 中繼資料,接著走 ③ /connect/authorize(授權碼+PKCE);web ④ 驗證會員登入、確認 Connector 登錄(mcp_app),⑤ 把授權碼導回 Redirect URI;Client 再 ⑥ POST /connect/token(帶 code_verifier),換到 ⑦ 一個 10 分鐘效期的 Access Token,用 token-signing.pfx 簽章。

呼叫階段:Client ⑧ POST /mcp(帶 Bearer)呼叫工具,web ⑨ 檢查工具權限與額度,⑩ 驗 Token、確認每分鐘 60 次的限制、跑一次 SsrfGuard,⑪ 呼叫 http://localhost:5080/api/…(5 秒逾時、回應限制 ≤1 MB),拿到 ⑫ JSON 結果後 ⑬ 寫入 ApiCallLogs,最後 ⑭ 把 MCP 工具結果回給 Client。圖上用實線代表請求、虛線代表回應。

三個必要條件撐住這整條路:Production 下 /connect/* 只接受 https;OAuth__Issuer 必須等於 https://127.0.0.1:5800/;第 ⑪ 步呼叫 demoapis 不經過 bridge 網路,也不經過主機——這正是第 03 課共用命名空間設計要達成的效果。

5801 執行期:提案 → 演練 → 核准 → Worker 部署

5801 執行期流程
圖 p.18 web 與 worker 透過共用的 governance.db 交接工作;對 SQL Server 的每一步都換成權限剛好夠用的登入。

web 這邊:① 提案編輯,草稿送審凍結成版本;② Jev 評估(目前是 Fake 模式,沒有金鑰);③ 流程判定,走硬規則決定是一般審查還是加強審查;⑤ 凍結計畫,記下 ScriptHash/PlanHash 寫進 governance.db;⑥ 管理者核准,綁定雜湊,效期 72 小時。

核准之後 ⑦ 在 governance.db 建一筆 DeploymentJobs,IdempotencyKey 保證唯一;worker ⑧ 每 2 秒輪詢一次、租約 60 秒,領到工作後對 SQL Server 執行:先用唯讀的 dbgov_reader 探索結構、拍快照,④ 用 dbgov_rehearsal 在 Test 上演練、最後 ROLLBACK,⑨ 用 dbgov_deployer 在 Dev 上部署、整個包在一個交易裡,⑨' 部署完再驗證、比對漂移,⑩ 把結果寫回 governance.db 的時間線與稽核紀錄。

兩條安全網值得記住:同一個目標同時只允許一筆「執行中」或「結果不明」的工作;worker 如果在 COMMIT 之後、寫回結果之前中斷,會標記成 NeedsReconciliation,交給管理者對帳,系統不會自動補跑。而且執行前還會再檢查一次:核准是否還有效、綁定雜湊是否一致、用唯讀帳號重讀結構跟基準比對——一旦偵測到漂移就直接中止。worker 與 web 共用 platformdata Volume 上的 governance.db(WAL 模式),這是兩個行程之間唯一的交接點。

兩套部署的差異一覽

兩套部署差異對照表
圖 p.19 同樣是「.NET 10 網站+SQL Server 2025+一次性 db-init」,但定位不同:5800 以正式環境設定運行,5801 是教室展示設定。
面向5800 · mcpplatform5801 · dbgov15
對外網址/傳輸https://127.0.0.1:5800(開發憑證 PFX)http://127.0.0.1:5801
執行環境Production:OpenIddict 強制 https、無示範資料Development:允許 TrustServerCertificate、http Cookie、示範目標與 user01
應用程式資料SQL Server MCPServerManag_ProdSQLite /data/governance.db
SQL Server 的角色平台的系統資料庫被治理的受管目標(Dev/Test)
命名空間擁有者web(demoapis 加入)sqlserver(web、worker、db-init 加入)
容器網段172.24.0.0/16・2 個成員172.25.0.0/16・1 個成員
主機 SQL 埠1433314334
程式映像2 個(web 393 MB、demoapis 340 MB)1 個(web+worker,533 MB)
網站健康檢查無(demoapis 只等 started)/health/ready(worker 等 healthy)
祕密產生init-secrets.ps1(隨機+兩張 PFX)手動維護 docker/.env(6 個值)
外部依賴無(上游 API 就在同一命名空間)Jev AI 決策:無金鑰時 Fake 模式

兩者共同遵守的規矩:只綁 127.0.0.1、非 root 執行、sa 不給網站、restart: unless-stopped、TZ=Asia/Taipei。

本課重點

  • 5800 的執行期是標準 OAuth 授權碼+PKCE 流程,Access Token 效期 10 分鐘,每位會員每分鐘限 60 次呼叫。
  • 5801 的執行期是「提案 → 演練 → 核准 → 部署」,web 與 worker 靠 governance.db 交接,不是直接呼叫。
  • 部署權限精確到每一步:探索用 dbgov_reader、演練用 dbgov_rehearsal(只碰 Test)、部署用 dbgov_deployer(只碰 Dev)。
  • 中斷後的部署狀態不會自動補跑,會標成 NeedsReconciliation 交給人工對帳。
  • 5800 是 Production 設定,5801 是 Development/教室展示設定,兩者定位不同不能直接類比。
05 / 06

第 06 課・p.20–25

實證與維運

最後一課:實機畫面能不能對得上前面講的設計、安全分層怎麼落地、日常維運要記住哪些指令,以及老師自己列的已知限制。

5800 實機畫面:後臺儀表板與資料庫概況

5800 實機畫面
圖 p.20 以系統管理員登入 https://127.0.0.1:5800;資料庫概況頁讀出容器內 SQL Server 的資料表與已套用的 Migration。

儀表板頁面(/Admin/Dashboard)以臺灣時間計算統計區間,靠的是映像內建的 tzdata/ICU 加上 TZ=Asia/Taipei 環境變數;因為是新部署的環境,MCPServerManag_Prod 目前還沒有呼叫紀錄。資料庫概況頁(/Admin/Database)顯示已套用 5 個 Migration(包含 AddOpenIddict),AspNetUsers 只有 1 筆,就是 Bootstrap 建立的第一位管理者。畫面擷取於 2026-09-27,唯讀瀏覽,沒有建立任何額外資料。

5801 實機畫面與健康檢查

5801 實機畫面與健康檢查
圖 p.21 以示範使用者 user01 登入;健康檢查端點同時證明「平台資料」與「受管資料庫」兩條連線都正常。

/Deployments 頁面說明部署是在背景由 Worker 執行,頁面每 2 秒重新整理一次。GET /health/ready → 200 回傳的 JSON 裡有兩項檢查:platform-db(已套用 11 個 Migration)和 data-protection-keys(1 把金鑰),兩項都是 Healthy;這是 compose 的 healthcheck 目標,worker 要等它 healthy 才會啟動。/ChangeRequests 頁面對照 GET /health/lab → 200,回傳的 lab-target 檢查顯示 DbGovernanceLab_Dev:109 ms,這條走 localhost,1433 連到 SQL Server 2025,直接證明第 03 課講的共用命名空間確實接通。畫面擷取於 2026-09-27,user01(一般使用者)唯讀瀏覽。

安全設計:由外而內四層

安全設計四層
圖 p.22 兩個堆疊採用相同的分層思路;表格右兩欄是各自的具體做法。
層級5800 · mcpplatform5801 · dbgov15
① 對外暴露面5800、14333 只綁 127.0.0.1;demoapis 的 5080 完全不對主機開放;全程 https(Production)5801、14334 只綁 127.0.0.1;平台只連 localhost 受管目標;http 僅限教室(Development)
② 身分與權限sa 只在 sqlserver、db-init;web 用 mcp_app(讀寫,不能改結構);容器以非 root $APP_UID 執行reader/rehearsal/deployer 分工;演練只碰 Test、部署只碰 Dev;容器以非 root app 執行
③ 祕密保護.dockerignore 排除 .env、secrets/;PFX 以唯讀掛載 /secrets;SQLCMDPASSWORD 傳遞 sa 密碼.dockerignore 排除 docker/.env;${VAR:?} 缺值即中止;平台以 secret:// 參照取得帳密
④ 應用層防護OAuth 授權碼+PKCE、驗 iss/aud;每位會員每分鐘 60 次限制;AllowedEndpoints+SsrfGuard禁止自我核准、核准綁定雜湊;ScriptDom AST 檢查、硬規則攔截;稽核遮罩、部署冪等鍵與租約

最小權限的關鍵,兩邊共通:網站與 worker 都拿不到 sa。

生命週期與日常維運

生命週期與日常維運
圖 p.23 Volume 決定資料的生死;共用命名空間決定「不能單獨重啟誰」。
指令容器映像Volume注意事項
docker compose stop / start停止/啟動保留保留5800:單獨 restart web 會讓 demoapis 斷網,改用 up -d
docker compose up -d依設定差異重建沿用保留首位管理者建立後把 .env 的 BOOTSTRAP 兩行註解掉;換網域或埠時三個 OAuth__* 要一起改
up -d --build重建程式容器,db-init 重跑重建保留—
up -d --force-recreate全部重建沿用保留—
docker compose down移除(含網路)保留保留5801:不要只重啟 sqlserver,改整組 down 再 up -d
docker compose down -v移除保留刪除改 .env 密碼後 up -d --force-recreate db-init web worker 輪替;示範部署後以 sql/10 重設開發庫
# 5800:在 McpPlatform/deploy/docker
pwsh ./init-secrets.ps1 ; docker compose up -d --build

# 5801:在 範例/15_…(Git Bash 需 MSYS_NO_PATHCONV=1)
docker compose -f docker/docker-compose.yml up -d --build
curl http://127.0.0.1:5801/health/ready

db-init 每次 up 都會重跑,因為所有腳本皆冪等,重跑不會破壞既有資料。

已知限制與改善建議

已知限制與改善建議
圖 p.24 目前的設定適合本機展示與教學;走向正式環境前,下列項目需要調整。
範圍現況建議
5800web 沒有 healthcheck;demoapis 只等 started加入 https 健康檢查,demoapis 改用 service_healthy
5800HTTPS 使用本機開發憑證(僅本機信任)換成網域正式憑證,同步修改 OAuth__Issuer 與埠對應
5801以 Development 執行:http、TrustServerCertificate、Fake Jev依 deployment-runbook 改 Production:受信任憑證、HTTPS、Jev 金鑰
5801web/worker 生命週期綁在 sqlserver 命名空間正式環境把目標改成可解析的主機名稱,讓服務各自獨立網路
兩者Data Protection 金鑰在 Linux 為明文 XMLProtectKeysWithCertificate,或放入金鑰管理服務
兩者映像標籤 latest(含 mssql/server:2025-latest)改用版本號或 digest 固定,便於回復
兩者SQL Server 為 Developer 版正式環境改授權版本(MSSQL_PID)或外部 SQL Server
兩者日誌只在容器 stdout,容器移除即消失設定日誌驅動或集中式日誌收集

參考文件:McpPlatform/docs/MCP Server治理平台Docker佈署架構.md、範例/15 docs/deployment-runbook.md。

總結:同一套容器化手法,兩種取捨

總結
圖 p.25 https://127.0.0.1:5800 | http://127.0.0.1:5801 | SQL Server 2025 · 14333 / 14334

整份講義收在四點:一是兩個堆疊完全隔離——各自的 SQL Server 2025、bridge 網路與 Volume,埠只綁 127.0.0.1,並避開主機原本就佔用的 1433;二是命名空間共用,方向相反——5800 讓上游 API 加入 web(滿足 SSRF 規則),5801 讓應用加入 sqlserver(保留 localhost,1433);三是資料角色不同——5800 的 SQL Server 是平台系統庫,5801 的平台資料在 SQLite、SQL Server 是被治理的目標;四是失敗即停、最小權限——healthy → completed → started/healthy 的相依鏈環環相扣,sa 只給初始化用,網站與 worker 各拿剛好夠用的登入。

本課重點

  • 兩邊實機畫面都能跟第 02、03 課的設計互相印證:命名空間共用是真的接通了,不是紙上談兵。
  • 安全設計分四層,由外而內:對外暴露面、身分與權限、祕密保護、應用層防護,兩個堆疊各自有具體做法。
  • 共用命名空間的代價要記牢:5800 別單獨重啟 web,5801 別單獨重啟 sqlserver,兩邊都改用整組 up -d。
  • docker compose down -v 會把 Volume 一起刪掉,牽動資料庫與金鑰,動手前要先確認。
  • 目前是本機展示設定:開發憑證、Developer 版 SQL Server、容器內日誌不落地,這些都是走向正式環境前要處理的已知限制。
06 / 06・完課
想看原始講義的完整版本? 下載原始講義 PDF(25 頁)