兩套 .NET 10 治理平台 × 兩個 SQL Server 2025 容器,從拓樸、網路、啟動、資料到執行期流程的完整圖解。共 25 頁講義,拆成六課,跟著課程地圖一課一課看下去。
佈署Microservice架構於Docker。將【Jev 資料庫管理治理平台】與【MCP Server 治理平台】與SQL Server 2025伺服器佈署完成。加上原先的n8n已經佈署Docker,尚有兩個治理平台尚未完成與佈署,那就是【AI Agent skill 治理平台】與【Microserive API溶斷與治理平台】設計與準備中… 待所有平台完成之後,我在考量是將Openclaw或者採用Cowork或者chatgpt codex等進行一個Workflow Hub配置。(我有重新調配那一台mac mini m4了)


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

這台 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) 收工,不是常駐服務。

老師用 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 實機畫面,對照上面的容器清單看更直覺。

workspace、n8n、mcpplatform、dbgov15 四個 Compose 專案並存,CPU 用量 2.01%/2400%(24 顆 CPU 可用),記憶體 3.15GB/14.92GB。
dbgov15 堆疊明細:sqlserver、db-init、web、worker 四個容器,右側是 sqlserver 的即時日誌(載入 xplog70.dll 這類初始化訊息)。
mcpplatform 堆疊明細:web 對外映射 5800:5800、sqlserver 對外映射 14333:1433,右側日誌能看到 web 呼叫 http://localhost:5080/api/courses 拿到 200 的紀錄。127.0.0.1:5800、14333、5801、14334,區網其他電腦連不到。db-init。docker ps 的清單對照,驗證容器狀態一致。第 02 課・p.05–08
把第 01 課看到的全景拆開,分別看 5800 mcpplatform 與 5801 dbgov15 各自的容器、網段、Volume 與掛載細節。

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,已存在的檔案不覆蓋。

mcp_app。| 服務 | 映像/角色 | 相依條件 |
|---|---|---|
sqlserver | mssql/server:2025-latest Developer;平台唯一資料庫,主機 14333 供 SSMS 管理 | —(healthcheck:sqlcmd SELECT 1) |
db-init | 同上映像,只跑 sqlcmd;建庫 → 冪等 Migration → 建立 mcp_app | sqlserver: service_healthy |
web | mcpplatform-web(393 MB);MVC 後臺、OpenIddict、/mcp、Gateway | db-init: service_completed_successfully |
demoapis | mcpplatform-demoapis(340 MB);示範上游 API,network_mode: service:web | web: service_started |
平台內建一個「開發環境檢查」頁(/SetupCheck),直接把共用命名空間的設計證明給你看:頁面顯示它以容器內位址 sqlserver,1433 連上資料庫、執行環境是 Production、.NET 版本 10.0.12,而且經由 http://localhost:5080 呼叫 demoapis 拿到 200——這一頁就是「web 跟 demoapis 真的在同一個網路命名空間裡」的活證據。畫面擷取於 2026-09-27 21:11(臺灣時間),系統管理員登入。

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 備份紀錄。

| 服務 | 映像/角色 | 相依條件 |
|---|---|---|
sqlserver | 受管實驗庫 Dev/Test;網路命名空間擁有者 | —(healthcheck 10s×20) |
db-init | 同上映像,執行 init-db.sh,依 README「從零安裝」順序跑 sql/ | sqlserver: service_healthy |
web | dbgov15-platform(533 MB);Migration、Bootstrap、MVC/API | db-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。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 只是受管目標。第 03 課・p.09–12
兩個堆疊都用了「共用網路命名空間」這一招,但方向剛好相反;再看 depends_on 怎麼把啟動順序卡緊,以及 db-init 的腳本怎麼跑。

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,所以不會出現在清單裡,這也直接驗證了兩邊「誰是擁有者」的設計。

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 身上。

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。圖上用綠色箭頭表示「需要條件成立才啟動」,灰色箭頭表示「只要前一個容器行程已啟動」。

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 先在兩個庫都跑完。第 04 課・p.13–16
映像怎麼建出來、資料放在哪個 Volume、兩個 SQL Server 帳號權限怎麼切、祕密怎麼從主機流進容器——這四件事決定了系統重建之後還剩下什麼。

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 登入。

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 才會連資料庫與金鑰一起刪除——這是一個一旦下錯指令就回不了頭的動作,操作前一定要先確認要不要留資料。

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。

.env 與 secrets/,以環境變數或唯讀掛載在執行時注入;.dockerignore 排除它們,映像可以安全分享。| mcpplatform 祕密 | sqlserver | db-init | web |
|---|---|---|---|
MSSQL_SA_PASSWORD | ● | ● | — |
MCP_APP_PASSWORD | — | ● | ● |
PFX_PASSWORD | — | — | ● |
https.pfx/token-signing.pfx | — | — | /secrets:ro |
BOOTSTRAP_ADMIN_* | — | — | 已註解 |
| dbgov15 祕密 | sqlserver | db-init | web | worker |
|---|---|---|---|---|
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 直接中止,不會啟動一個設定殘缺的半殘網站。
mssql-data、dp-keys、platformdata、sqldata;資料庫與 Data Protection 金鑰要成對備份。docker compose down 保留 Volume,down -v 會連資料庫與金鑰一起刪除——動手前先確認。sa 只給初始化用,網站和 worker 都拿不到 sa。.env 與 secrets/,以環境變數或唯讀掛載注入,.dockerignore 確保它們不會進映像。第 05 課・p.17–19
前面幾課看的是「架構長什麼樣」,這一課看「一次呼叫、一次部署真的跑起來時,資料怎麼流動」。

/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 課共用命名空間設計要達成的效果。

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 模式),這是兩個行程之間唯一的交接點。

| 面向 | 5800 · mcpplatform | 5801 · 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_Prod | SQLite /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 埠 | 14333 | 14334 |
| 程式映像 | 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/教室展示設定,兩者定位不同不能直接類比。第 06 課・p.20–25
最後一課:實機畫面能不能對得上前面講的設計、安全分層怎麼落地、日常維運要記住哪些指令,以及老師自己列的已知限制。

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,唯讀瀏覽,沒有建立任何額外資料。

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(一般使用者)唯讀瀏覽。

| 層級 | 5800 · mcpplatform | 5801 · 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。

| 指令 | 容器 | 映像 | 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/readydb-init 每次 up 都會重跑,因為所有腳本皆冪等,重跑不會破壞既有資料。

| 範圍 | 現況 | 建議 |
|---|---|---|
| 5800 | web 沒有 healthcheck;demoapis 只等 started | 加入 https 健康檢查,demoapis 改用 service_healthy |
| 5800 | HTTPS 使用本機開發憑證(僅本機信任) | 換成網域正式憑證,同步修改 OAuth__Issuer 與埠對應 |
| 5801 | 以 Development 執行:http、TrustServerCertificate、Fake Jev | 依 deployment-runbook 改 Production:受信任憑證、HTTPS、Jev 金鑰 |
| 5801 | web/worker 生命週期綁在 sqlserver 命名空間 | 正式環境把目標改成可解析的主機名稱,讓服務各自獨立網路 |
| 兩者 | Data Protection 金鑰在 Linux 為明文 XML | ProtectKeysWithCertificate,或放入金鑰管理服務 |
| 兩者 | 映像標籤 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。

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 各拿剛好夠用的登入。
5800 別單獨重啟 web,5801 別單獨重啟 sqlserver,兩邊都改用整組 up -d。docker compose down -v 會把 Volume 一起刪掉,牽動資料庫與金鑰,動手前要先確認。