iT邦幫忙

2026 iThome 鐵人賽

DAY 2
0
AI Engineering

生活中的 AI 應用:我在家用 NAS 養了一隻 Agent,幫我看盤、顧家、盯備考——30 天自架實錄系列 第 2

Day 2:為什麼一份 image 要跑兩個容器?我裝完半年才懂官方的設計

  • 分享至 

  • xImage
  •  

昨天的架構圖裡,你看到了三個核心容器(Gateway、CLI、MCP),加上 md-server 等輔助服務。今天把核心三層拆開來說:它們分別叫什麼、做什麼、為什麼要這樣切。

先講清楚歸屬,免得整篇讀起來像我的設計功勞:

Gateway 與 CLI 的雙容器切分,是 OpenClaw 官方 compose 檔就長這樣的。我照著文件把它裝起來,沒有做任何架構決策。這篇要講的不是「我怎麼設計」,是「我跑了半年之後,才真正理解它為什麼這樣切」——以及我自己後來額外加上去的那些容器(那些才是我的)。

會想搞懂這件事,是因為裝的時候我心裡有個疑問:一台家用 NAS 而已,同一份 image 跑兩個容器,不是多此一舉嗎?

一份 image、兩個角色:我後來理解的理由

如果全部塞在一個容器裡,會有一個很煩的後果:任何一層出問題,你就得整個重啟

而這件事我是真的撞到才懂的:

Telegram Bot 的 webhook 連線有問題,需要重啟 Gateway。
如果它跟排程在同一個容器裡,重啟的同時,正在執行的 cron job 也一起中斷。

分開之後,Gateway 重啟不影響 CLI 容器裡正在跑的任務;MCP Server 更新工具介面也不會讓 Agent 停工。每一層獨立生死,不連坐。

有意思的是這個切法背後的判準——不是按功能切,是按「重啟頻率」切。對外通訊層要面對外部世界的不穩定(webhook 掉線、憑證更新、網路抖動),註定重啟得比較勤;而排程與記憶是有狀態的長時工作,最怕被打斷。把「常重啟的」和「怕被打斷的」分開,這條原則在任何有狀態的系統上都成立,不是 AI 專屬。

我把它記下來,是因為它幫我後來自己加容器時少走了彎路:md-server 我就刻意做成無狀態、可隨時重啟的;而唯一有狀態的東西(SQLite)掛在 volume 上,不隨容器生死。

https://ithelp.ithome.com.tw/upload/images/20260801/20182865KzW6nOb0Rl.png


Gateway 容器——對外通訊層

角色是「前台」,處理所有進出 NAS 的訊息。

  • Telegram webhook:手機發訊息給 Clawrion,走這裡進來
  • Bot API / Control UI:REST API 端點,讓其他服務或 Cowork 直接呼叫
  • WebSocket:維持與 CLI 容器的持久連線

Gateway 本身是 stateless 的——它不保存任何 Agent 狀態,只負責轉發。這很重要:你可以隨時重啟 Gateway 而不會丟失任何記憶或排程狀態。

外部流量不走 port forwarding,全部透過 Cloudflare Tunnel 進來(這個留到 Day 6 展開講)。

tg.<your-domain>  →  DS723+ : 8788  (Telegram webhook)
bot.<your-domain> →  DS723+ : 18789 (Bot API)

CLI 容器——大腦(Agent Runtime)

這是整個系統真正「思考」的地方。

  • Cron Scheduler:管理所有定時任務,從凌晨抓行情到每日晨報
  • Memory Core:三層記憶系統(短期 Session、長期 Markdown、Dreaming 整合)
  • Agent 執行環境:真正呼叫 LLM、讀取 Workspace、寫回記憶的地方

CLI 容器沒有對外 port——它不接受外部連線,只透過 Gateway 的 WebSocket 接收指令,透過 MCP 提供工具。

所有狀態都存在這裡:

  • SQLite DB:/home/node/.openclaw/state/openclaw.sqlite(Job 狀態、執行記錄)
  • Workspace:/home/node/.openclaw/workspace/(全部 Markdown 檔案)

踩坑:Gateway 重啟後 WebSocket 斷線

這是目前已知的系統問題。

Gateway 重啟之後,CLI 容器的 WebSocket 連線不會自動重連。具體現象是:你傳訊息給 Telegram Bot,它沒有回應——Gateway 收到了,但 CLI 聽不到。

解法:

docker restart openclaw-openclaw-cli-1

或者透過自建 MCP Server 的 openclaw_restart_containers 工具觸發,不需要 SSH。這個工具不是一開始就設計好的——它是在踩了這個坑之後,為了讓 AI 能自己修復而補上去的。MCP Server 裡的工具大多是這樣來的:系統出問題、人工介入、覺得「這步應該讓 AI 自己做」,然後把能力加進去。這個問題我目前沒有根治,但已經知道怎麼快速恢復。


MCP Server——工具層

MCP(Model Context Protocol)是 Anthropic 定義的工具協定,讓 AI 能呼叫外部工具。

這個容器不是官方給的,是後來補的——前兩層照著文件裝就有,這一層是我為了「讓桌面的 AI 能直接操作 NAS」而加的。它是一個 Node 服務,把一組操作包成 MCP 工具,暴露給桌面端的 AI 呼叫。

照 Day 1 講好的用字:**這層的規格、邊界、哪些能力不給,是我定的;程式碼是我跟 AI 助手一起寫出來的。**後面提到「自建」都是這個意思。

半年下來長到 26 個工具,分成六類:

類別 工具 典型用途
Workspace 讀寫 read / write / append / list_dir / search 讓桌面 AI 直接讀改 NAS 上的記憶與報告
排程管理 list / create / update / trigger / get_job_history 不用 SSH 就能改排程、補跑任務、查失敗紀錄
系統操作 exec_in_container / restart_containers / run_script / force_deploy 故障排除與部署
監控觀測 get_status / get_logs / read_log_file / get_memory_usage / usage_summary 查健康、查 log、查這個月花了多少錢
投資資料 get_market_prices / check_market_cache / get_portfolio_snapshot 讀快取而不是重打 API(Day 16 會講為什麼)
Loop 與通訊 loop_status / loop_abort / send_telegram / chat 長流程控制與推播

對外入口:mcp.<your-domain>(port 3001)

這 26 個工具沒有一個是一開始規劃的。每一個都對應到一次「我又手動做了同樣的事第三遍」的時刻——查 log 查到膩了就加 get_logs,被 $40 事故嚇到就加 usage_summary。所以這份清單與其說是設計,不如說是我的懶惰史。Day 27 會專門講怎麼設計工具的邊界(哪些能力刻意不給,比給了什麼更重要)。

MCP Server 本身是 stateless 的——它只是橋接,不存任何狀態。有個已知的預期行為:啟動時會嘗試建立 SSE 持久連線(GET /mcp),但 stateless server 回傳 404。這是正常行為,不影響工具呼叫。


路徑三層映射

這是很多人剛開始自架時最容易搞混的地方。

同一份 Workspace,在不同地方有不同路徑:

https://ithelp.ithome.com.tw/upload/images/20260801/201828651tep0yfyds.png

Syncthing 負責本機 Windows 與 NAS 的雙向同步。我在電腦上改一個 Markdown 檔,幾秒內同步到 NAS,容器裡的 Agent 下次讀取就看到最新版本。這就是為什麼整套系統不需要 git push、不需要重新部署——改 Markdown 就好。

這也是為什麼選 Markdown 而不是資料庫:任何工具都能讀,任何地方都能改,版本控制天然可用。


那還有哪些容器?

容器 用途
md-server Workspace 瀏覽器(port 9527),把 Markdown 檔案變成可瀏覽網頁。原本只是為了方便查看 Workspace 內容,後來變成「我提需求、AI 做開發」的介面——我在瀏覽器裡看到問題或想法,直接講給 AI 助手,它改完推回 Workspace,下次開網頁就是新版本。
cloudflared Cloudflare Tunnel 守護程序。原本用 ISP port forwarding(電信商 → Router → NAS),買網域之後改用 Cloudflare Tunnel,路由器一個 port 都不需要開。細節留到 Day 6。
watchtower 定期輪詢 registry,偵測到有新版 image 就自動 pull + 重建容器 + 清掉舊 image,不需要人介入。只會動「有貼標籤」的容器,其他服務不受影響。詳見下方「更新機制」。

這三個是輔助層,不需要每天打交道,但少了它們系統就不完整。Cloudflare Tunnel 的故事放在 Day 6。


更新機制:三條路,因為三種東西的性質不一樣

這是我覺得最值得分享的一段,因為它不是一開始想好的,是被三種不同的痛點逼出來的。

系統裡有三類程式碼,它們的擁有者和變動頻率完全不同,所以更新方式也各走各的:

https://ithelp.ithome.com.tw/upload/images/20260801/20182865xp3E2xzEA9.png

① 官方 image:交給 watchtower

Gateway 和 CLI 跑的是同一份 Docker image(ghcr.io/openclaw/openclaw),透過不同的啟動指令分成兩個角色。好處是維運只需要操作一個 image——拉一次,兩個容器同步更新。openclaw 用日期當版號(寫這篇時是 2026.6.6,發文時已經是 2026.7.1),有新功能或修復就推一個新版到 registry。

更新由 watchtower 處理,但它不是無差別更新所有容器——設定了 WATCHTOWER_LABEL_ENABLE=true,代表只更新有主動貼標籤的容器

$ docker inspect openclaw-openclaw-gateway-1     --format '{{index .Config.Labels "com.centurylinklabs.watchtower.enable"}}'
true

有這個標籤,watchtower 每天凌晨 04:00 偵測新版、自動 pull + 重建 + 清掉舊 image;沒貼的完全不受影響。這個「白名單而非黑名單」的設計是重點——同一台 NAS 上還跑著相簿、文件檢視器之類的家用服務,我可不希望半夜有人幫我把它們一起升級了。

② 我的腳本:push 就部署

md-server 和幾十支自動化腳本放在 Workspace 裡,是我這邊負責的部分。這些東西改動頻率遠高於官方 image——有時一天好幾次,因為跟 AI 助手配對開發的節奏就是這樣:想到一個檢查、十分鐘後它就在跑了。如果每次都要 SSH 上去手動搬檔,我大概兩週就放棄維護了。

**這條自動部署線其實是被實作速度逼出來的。**當寫程式的成本掉下來之後,瓶頸就從「寫」移到「怎麼安全地送上去」——這也是為什麼下一段的 PR 閘門與自動測試對我來說不是形式,是唯一能讓我敢一天改好幾次的東西。

現在的做法:推上 master 分支就自動部署。NAS 上跑了一個 GitHub Actions 的自架 runner,收到 push 事件後把變動檔案送進 md-server 容器、讀回驗證、再打一次健康檢查確認服務還活著。

這裡有個值得記下來的細節:部署要讀回驗證,不能只看「寫入成功」。而我為了這句話付出過代價——某天整天看著部署回報「內容不符」,以為檔案壞了,追下去才發現是驗證程式自己的 bug:它逐塊拼接 HTTP 回應,中文字剛好跨在兩塊之間就被拆成亂碼。**檔案從頭到尾都是對的,錯的是那個負責檢查的東西。**這個主題會在 Day 28 變成一個更大的問題。

③ MCP Server:旗標檔 + 定時 watcher

MCP Server 是 TypeScript 寫的,需要編譯,所以不能像 Markdown 那樣同步過去就生效——得複製原始碼、重新 build image、重啟容器。

而這裡有個環境限制:MCP Server 容器裡沒有 Docker socket(這是刻意的,Day 26 講安全時會解釋為什麼不給),所以它沒辦法自己重建自己。

我的解法很土但很好用:**要部署時,在 Workspace 放一個空的旗標檔。**NAS 上有支每小時跑的守衛腳本,看到旗標就去複製原始碼、build --no-cacheup -d,全部成功才刪掉旗標;看到沒旗標就立刻結束,零副作用。因為 build 可能跑很久,它跟其他維運腳本共用一把檔案鎖,搶不到就這輪跳過、旗標留著下輪再處理——寧可晚一小時,也不要兩邊同時 build 汙染同一個目錄。

兩個實作上的坑:

  • **必須整個目錄樹複製。**原始碼早期只有一個檔案,後來拆成多個模組,我還照舊只複製主檔——結果 build 出來的容器缺模組,或者留著舊工具的程式碼。
  • 一定要 build --no-cacheup -d --build 有機率吃到舊快取,改了程式碼但容器行為沒變。這種故障最難查,因為你會一直懷疑自己的邏輯。

為什麼不統一成一種

老實說我想過。但這三類東西的失敗代價差太多了:官方 image 升級壞了我只能等上游修;我的腳本壞了我自己五分鐘改回來;MCP Server 壞了會讓桌面 AI 失去所有工具、但排程完全不受影響。

**代價不同,就值得用不同的謹慎程度對待。**統一成一條流水線看起來乾淨,實際上是把最脆弱那層的風險,加到最穩定那層身上。


踩坑總結

問題 現象 解法
Gateway 重啟後 CLI WebSocket 斷 Bot 無回應 docker restart openclaw-openclaw-cli-1
MCP Server SSE 404 啟動 log 有 404 error 非致命,忽略即可
docker compose up --build 用舊快取 改了 src 但容器沒更新 分開執行 build --no-cacheup -d

小結

三個容器,三件事:Gateway 管通訊,CLI 管思考,MCP 管工具。前兩個是官方的設計,第三個是我這邊補的;分開跑的好處是可以獨立重啟、獨立更新,出問題時範圍可控。

如果只帶走一句話,我會選這個:**切分的判準不是功能,是「重啟頻率」與「失敗代價」。**官方把常重啟的通訊層和怕被打斷的排程層分開,是前者;我把三類程式碼配三條更新路徑,是後者。兩件事其實是同一個道理。

明天從「靈魂」開始:為什麼要給 Agent 寫一個人格檔案,以及 Clawrion 的行為紅線是怎麼設計的。


🔑 這篇的關鍵字
容器分工docker compose · stateless / stateful 服務切分 · Docker volume mount
更新機制watchtower + WATCHTOWER_LABEL_ENABLE(白名單式自動更新)· GitHub Actions self-hosted runnerruns-on: [self-hosted])· flock(非阻塞檔案鎖,避免兩邊同時 build)· docker compose build --no-cache
工具層:Model Context Protocol(MCP)· stateless MCP server


我是一名金融業資訊工程師,這是我半年來在家自架 AI Agent 系統的實錄。



上一篇
Day 1:我把 AI 管家裝進家裡的 NAS——30 天自架實錄
下一篇
Day 3:我的 AI 有名字、有性格、有紅線——人格檔案設計實錄
系列文
生活中的 AI 應用:我在家用 NAS 養了一隻 Agent,幫我看盤、顧家、盯備考——30 天自架實錄5
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言