OpenClaw 实战 07|部署指南:3 形态决策树 + OpenWebUI 接入 + FreshRSS 跨实例原创
# 部署指南:3 形态决策树 + OpenWebUI 接入 + FreshRSS 跨实例
这一篇不讲"docker run、k8s manifest"那一套通用部署话题,只讲 OpenClaw 真实部署中遇到的 3 形态选型 + OpenWebUI 接入 + FreshRSS 跨实例。
本文所有内容都来自
~/.hermes/workspace/blog-material/notes/下的真实部署会话记录:
<nas-notes>/<session-file>— 容器服务清单 + Docker Compose 探查<nas-notes>/<session-file>— Cloudflare Tunnel + OVS 桥接<nas-notes>/<session-file>— FreshRSS Skill 跨实例claude-code/2026-06-15-我要把当前服务器上的openwebui-对接openclaw— OpenWebUI 接入关键认知:OpenClaw 没有"标准部署形态"——同一个人会有 3 种形态并存,部署决策先于技术细节。
# §1 清点容器的那次
今年 4 月我想清点一下家里面跑的容器到底有哪些服务在监听(来自 <session-file>)。
真实场景:
我 SSH 进一台跑 CasaOS 的机器,想搞清楚里面到底跑了哪些服务:
- 哪些容器在运行?
- 哪些端口暴露?
- 哪些有 OpenClaw?
清点结果:
ssh <USER>@<IP> "docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}' "
# 看到 12 个容器,包括:
# - openclaw (本机 OpenClaw 实例)
# - lobe-chat (chat UI)
# - comfyui
# - flowise
# - jupyter
# - <DB_BOT>
# - multilingual-e5-large
# - 等等
2
3
4
5
6
7
8
9
10
清点暴露的真相:
很多容器监听了端口但没在用——开发完放那儿没人清。多网卡环境下端口冲突风险高。
这个故事揭示 OpenClaw 部署的 3 个真实挑战:
- 多形态并存:容器、裸机、源码,运维命令不通用(来自 03 架构)
- 多网卡 IP 冲突:容器监听
<PUBLIC_IP>,Tunnel 目标写<INTERNAL_IP>就会断(来自 05 安全) - 多服务混跑:OpenClaw + OpenWebUI + FreshRSS + ComfyUI 共存,端口和凭证要分清
下面按部署决策树 → 容器/裸机/源码各自模板 → 接入层(OpenWebUI/Tunnel) → 验证清单展开。
# §2 部署形态决策树(先选形态再谈技术)
部署 OpenClaw 前必须先回答 3 个问题:
Q1: 24x7 跑 vs 开发调试?
├─ 24x7 → Q2
└─ 开发调试 → Q3
Q2: 公网暴露 vs 局域网?
├─ 公网暴露 → 容器(隔离 + 可移植)
└─ 局域网 → 裸机 + systemd(开销小)
Q3: 单次调试 vs 反复改?
├─ 单次 → 源码(直接 node)
└─ 反复 → 源码 + nodemon
2
3
4
5
6
7
8
9
10
11
决策结果:
| 场景 | 形态 | 进程管理 | 端口 |
|---|---|---|---|
| 公网暴露 24x7 | Docker + Supervisor | supervisorctl | 8701/8702 |
| 局域网 24x7 | 裸机 + user systemd | systemctl --user | 18789(默认) |
| 开发调试 | 源码 | node openclaw.mjs | 18789 |
复用规则:同一个人往往 3 种形态并存(远程用容器、家用裸机、调试源码)。任何 Skill / 巡检脚本第一步必须是拓扑判定(参见 03 §2)。
# §3 容器化部署:真实配置
最常见的生产部署形态(来自 <session-file>)。
# 3.1 标准 Docker Compose 模板
# ~/.openclaw/deploy/docker-compose.yml
version: '3.8'
services:
openclaw:
image: <OPENCLAW_IMAGE>:<TAG>
container_name: openclaw
restart: unless-stopped
user: "<USERNAME>" # 关键:必须指定用户,避免 root 写 state.db
environment:
- OPENCLAW_GATEWAY_TOKEN=${OPENCLAW_GATEWAY_TOKEN}
- HOME=/home/<USERNAME>
- PATH=/home/<USERNAME>/.nvm/versions/node/v22.20.0/bin:/home/<USERNAME>/.local/share/pnpm/bin:/usr/local/bin:/usr/bin:/bin
volumes:
- /home/<USERNAME>/.openclaw:/home/<USERNAME>/.openclaw
- /home/<USERNAME>/.nvm:/home/<USERNAME>/.nvm
- /home/<USERNAME>/.local/share/pnpm:/home/<USERNAME>/.local/share/pnpm
- /var/log/openclaw:/var/log/openclaw
ports:
- "<BIND_ALL>:18789:18789" # Gateway
- "<BIND_ALL>:8701:8701" # Hermes Profile(如果启用)
network_mode: bridge
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:18789/health"]
interval: 30s
timeout: 5s
retries: 3
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
关键陷阱:
user:必须指定:root 默认跑会写~/.openclaw/state为 root 拥有,后续普通用户跑全报unable to open database file- PATH 必须显式:nvm/pnpm 路径不会自动加载
- bind IP 必须明确:
<BIND_ALL>让容器外能连;localhost只容器内能连
# 3.2 Supervisor 配置(在容器里用)
; /etc/supervisor/conf.d/openclaw.conf
[program:openclaw]
command=/home/<USERNAME>/.nvm/versions/node/v22.20.0/bin/node /home/<USERNAME>/openclaw-src/openclaw.mjs
directory=/home/<USERNAME>/openclaw-src
user=<USERNAME>
environment=OPENCLAW_GATEWAY_TOKEN=%(ENV_OPENCLAW_GATEWAY_TOKEN)s
autorestart=true
startretries=10
stdout_logfile=/var/log/openclaw/openclaw.stdout.log
stderr_logfile=/var/log/openclaw/openclaw.stderr.log
2
3
4
5
6
7
8
9
10
# 3.3 部署命令
# 1. 准备目录
mkdir -p /home/<USERNAME>/.openclaw /var/log/openclaw
chown -R <USERNAME>:<USERNAME> /home/<USERNAME>/.openclaw /var/log/openclaw
# 2. 配置 Supervisor
sudo tee /etc/supervisor/conf.d/openclaw.conf <<EOF
[program:openclaw]
command=...
EOF
# 3. 启动
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl status openclaw
# 4. 验证
curl -sf http://<BIND_ALL>:18789/health
# 期望:{"status":"ok"}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# §4 裸机部署:家用 NAS + user systemd
最适合家用 24x7(来自 <session-file> swappiness 调优素材隐含 + 03 架构)。
# 4.1 user systemd unit
# ~/.config/systemd/user/openclaw-gateway.service
[Unit]
Description=OpenClaw Gateway
After=network.target
[Service]
Type=simple
WorkingDirectory=/home/<USERNAME>/openclaw-src
Environment="OPENCLAW_GATEWAY_TOKEN=%E"
Environment="PATH=/home/<USERNAME>/.nvm/versions/node/v22.20.0/bin:/home/<USERNAME>/.local/share/pnpm/bin:/usr/local/bin:/usr/bin:/bin"
ExecStart=/home/<USERNAME>/.nvm/versions/node/v22.20.0/bin/node /home/<USERNAME>/openclaw-src/openclaw.mjs
Restart=on-failure
RestartSec=5
StandardOutput=append:/var/log/openclaw/openclaw.log
StandardError=append:/var/log/openclaw/openclaw.err
[Install]
WantedBy=default.target
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 4.2 启用 user systemd 持久化
# 1. 启用 user systemd 持久化(避免 SSH 退出后 unit 被杀)
sudo loginctl enable-linger <USERNAME>
# 2. 启用 service
systemctl --user daemon-reload
systemctl --user enable openclaw-gateway
systemctl --user start openclaw-gateway
# 3. 验证
systemctl --user status openclaw-gateway
# 期望:active (running)
# 4. 日志
journalctl --user -u openclaw-gateway -f
2
3
4
5
6
7
8
9
10
11
12
13
14
# 4.3 NAS 系统的 swappiness 调优(重要!)
OpenClaw 在 NAS 上跑经常触发 Swap 高占用——这是 swappiness=60 默认值的问题(来自 <session-file>)。
# 1. 查当前
cat /proc/sys/vm/swappiness
# 默认 60(内存可用 40% 时即开始 swap)
# 2. 临时调
sudo sysctl -w vm.swappiness=1
# 3. 持久化
echo "vm.swappiness=1" | sudo tee /etc/sysctl.d/99-swappiness.conf
2
3
4
5
6
7
8
9
为什么:OpenClaw 进程长时间运行会触发内核主动换出不活跃页面,但物理内存其实够用。swappiness=1 让内核极度保守,只在物理内存耗尽时才 swap。
# §5 源码部署:开发态
最快但不稳定(来自 <session-file> 容器内重装 + 03 架构)。
# 5.1 单次启动
cd /workspace/openclaw
pnpm install
node openclaw.mjs
2
3
# 5.2 反复改 + nodemon
pnpm add -D nodemon
nodemon --watch src --ext js,mjs,ts openclaw.mjs
2
# 5.3 完整卸载(防止残留)
# 4 个地方都要清
rm -rf /home/<USERNAME>/openclaw-sandboxes
rm -rf /workspace/openclaw
rm -rf /home/<USERNAME>/.openclaw
cd /workspace && pnpm store prune
2
3
4
5
踩坑:只删源码目录不够,pnpm store 缓存还在。
# §6 OpenWebUI 接入 + Kong 反向代理(来自 06-15 OpenWebUI 集成)
最常见的"OpenClaw 不是孤岛"场景:让用户用 OpenWebUI 跟 OpenClaw 聊。
# 6.1 OpenClaw 端:开启 chatCompletions
openclaw config patch --stdin <<'JSON5'
{
"gateway": {
"http": {
"endpoints": {
"chatCompletions": { "enabled": true }
}
},
"bind": "lan"
}
}
JSON5
supervisorctl restart openclaw
2
3
4
5
6
7
8
9
10
11
12
13
14
# 6.2 OpenWebUI 端:调用 /openai/config/update
{
"ENABLE_OPENAI_API": true,
"OPENAI_API_BASE_URLS": [
"http://<INTERNAL_IP>:8702/v1",
"https://openclaw.<INTERNAL_BRAND>.ai/v1"
],
"OPENAI_API_KEYS": ["<BEARER_TOKEN>"],
"OPENAI_API_CONFIGS": {
"1": {
"enable": true,
"tags": [],
"prefix_id": "openclaw",
"model_ids": ["openclaw/default"]
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 6.3 Kong 反向代理(公网入口)
Kong 必须调高 WebSocket 超时(来自 06-15 素材):
# kong.yml
services:
- name: openclaw
url: http://<INTERNAL_IP>:18789
read_timeout: 3600000 # 1 小时
write_timeout: 3600000
routes:
- name: openclaw
paths:
- /openclaw
strip_path: true
2
3
4
5
6
7
8
9
10
11
为什么需要:OpenClaw WebSocket 长连接流式输出,默认 60 秒超时把连接掐断。
# 6.4 端到端验证
# 1. 验证 /v1/models 返回 JSON
curl -H "Authorization: Bearer <BEARER_TOKEN>" \
http://<INTERNAL_IP>:18789/v1/models
# 期望:{"object":"list","data":[{"id":"openclaw/default",...}]}
# 2. 验证 /v1/chat/completions 流式
curl -N -H "Authorization: Bearer <BEARER_TOKEN>" \
-H "Content-Type: application/json" \
http://<INTERNAL_IP>:18789/v1/chat/completions \
-d '{"model":"openclaw/default","messages":[{"role":"user","content":"hi"}]}'
# 期望:流式返回 data: {...}\n\n...
2
3
4
5
6
7
8
9
10
11
12
13
# §7 FreshRSS 跨实例 + Tunnel(来自 <session-file>)
跨实例接自托管服务的典型场景。
# 7.1 网络层:Cloudflare Tunnel
# ~/.cloudflared/config.yml
tunnel: <TUNNEL_ID>
credentials-file: /home/<USERNAME>/.cloudflared/<TUNNEL_ID>.json
ingress:
- hostname: openclaw.<INTERNAL_BRAND>.ai
service: http://<INTERNAL_IP>:18789
- hostname: freshrss.<INTERNAL_BRAND>.ai
service: http://<INTERNAL_IP>:8749
- service: http_status:404
2
3
4
5
6
7
8
9
10
# 7.2 FreshRSS 端:开启 Google Reader API
# FreshRSS 后台 → 设置 → API → 启用 Google Reader API
# 生成 API 密码(不是登录密码)
2
# 7.3 OpenClaw 端:装 freshrss-reader Skill
npx clawhub install freshrss-reader
配置:
# 环境变量
export FRESHRSS_URL="http://<INTERNAL_IP>:8749"
export FRESHRSS_USER="<USERNAME>"
export FRESHRSS_API_PASSWORD="<API_PASSWORD>" # 文件 600 权限
2
3
4
# 7.4 验证
# 1. 认证:拿 Auth Token
curl -X POST "$FRESHRSS_URL/api/greader.php/accounts/ClientLogin" \
-d "Email=$FRESHRSS_USER&Passwd=$FRESHRSS_API_PASSWORD"
# 2. 数据:拿 headlines
curl "$FRESHRSS_URL/api/greader.php/reader/api/0/stream/contents" \
-H "Authorization: <AUTH_TOKEN>"
# 3. Skill 端到端
openclaw infer model "用 FreshRSS 总结今天未读新闻"
2
3
4
5
6
7
8
9
10
# §8 多网卡环境的反向代理(来自 <session-file>)
最隐蔽的部署故障:监听 IP 错位。
# 8.1 典型故障模式
| 服务 | 实际监听 | Tunnel 目标 | 故障? |
|---|---|---|---|
| lobe-chat 容器 | <PUBLIC_IP> | <INTERNAL_IP> (OVS 桥接) | ❌ Tunnel 不通 |
| OpenClaw 容器 | <BIND_ALL> | <PUBLIC_IP> | ✅ 通 |
| FreshRSS | localhost | <INTERNAL_IP> | ❌ 外网连不到 |
# 8.2 监听 IP 验证
ss -tlnp | grep -E '18789|8749|3210'
# 期望:每行 *:PORT 或 <BIND_ALL>:PORT
# 异常:<LOOPBACK>:PORT(仅本机)
2
3
# 8.3 多 IP 验证 Tunnel 可达
curl -m 3 http://<INTERNAL_IP>:3210
curl -m 3 http://<PUBLIC_IP>:3210
# 两个都通 → 监听正确
# 仅内网通 → 监听在 <PUBLIC_IP>,Tunnel 用 <INTERNAL_IP> 会断
# 都不通 → 后端服务没起
2
3
4
5
6
# 8.4 修复
把 Tunnel 目标从 <INTERNAL_IP> 改为 <PUBLIC_IP> 绕过 OVS:
ingress:
- hostname: lobe.<INTERNAL_BRAND>.ai
service: http://<PUBLIC_IP>:3210 # 从 INTERNAL_IP 改 PUBLIC_IP
2
3
复用规则:多网卡环境 Tunnel 目标必须用"实际监听可达的 IP",不一定是内网 IP。
# §9 端到端部署验证清单(10 步)
[1] 拓扑判定(容器/裸机/源码)
ssh <USER>@<IP> 'which openclaw; supervisorctl status openclaw'
[2] 服务运行
supervisorctl status openclaw # 容器
systemctl --user status openclaw-gateway # 裸机
[3] 端口监听
ss -tlnp | grep 18789
[4] Gateway 健康
curl -sf http://<INTERNAL_IP>:18789/health
[5] 配置合法
openclaw config validate
[6] Provider 在线
openclaw infer model providers
[7] Token 一致性
sha256 对比 config vs env
[8] Skill 端到端
openclaw skills list
[9] 多通道 ping(CLI / Control UI / Telegram / WebSocket)
[10] 反向代理 + Tunnel 可达
curl -m 3 http://<PUBLIC_IP>:<PORT>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
前 5 步必走,后 5 步可选。
# §10 下一篇
下一篇:08|故障排除——Token Mismatch、SSRF 死循环、"部分存活"假象的真实定位路径。
# §11 Agent 可直接解析的元数据块
{
"_meta": {
"doc_version": "2026-06-27",
"article_id": "openclaw-deployment",
"profile_context": "blog",
"estimated_read_time": "14min",
"real_material_count": 4,
"material_sources": [
"<nas-notes>/<session-file>.md",
"<nas-notes>/<session-file>.md",
"<nas-notes>/<session-file>.md",
"notes/claude-code/<session-file>.md"
]
},
"deployment_decision_tree": {
"q1_247_or_dev": {
"247": "q2",
"dev": "q3"
},
"q2_public_or_lan": {
"public": "docker-supervisor",
"lan": "baremetal-systemd"
},
"q3_once_or_repeat": {
"once": "source-direct",
"repeat": "source-nodemon"
}
},
"docker_compose_critical": {
"user_spec": "user: <USERNAME> (not root, prevent state.db permission error)",
"path_env": "explicit PATH with nvm/pnpm",
"bind_ip": "<BIND_ALL> for external access"
},
"swappiness_recommendation": {
"default": 60,
"recommended_for_nas": 1,
"persistence": "/etc/sysctl.d/99-swappiness.conf"
},
"openwebui_integration": {
"openclaw_side": "gateway.http.endpoints.chatCompletions.enabled=true + bind=lan",
"openwebui_side": "OPENAI_API_CONFIGS.<id>.model_ids whitelist",
"kong_config": "read_timeout=3600000 + write_timeout=3600000 (WebSocket)"
},
"freshrss_integration": {
"tunnel": "cloudflared",
"skill_install": "npx clawhub install freshrss-reader",
"credentials_env": ["FRESHRSS_URL", "FRESHRSS_USER", "FRESHRSS_API_PASSWORD"]
},
"multi_nic_trap": {
"symptom": "Tunnel connection refused even when backend healthy",
"root_cause": "Tunnel target <INTERNAL_IP> not reachable via OVS bridge",
"fix": "Change target to <PUBLIC_IP> or fix OVS bridge"
},
"safety_rules": [
"Docker user: 必须指定非 root 用户",
"PATH 必须显式 export(nvm/pnpm)",
"bind IP 永远用 <BIND_ALL>(容器外访问)",
"Tunnel 目标必须用可达 IP(多网卡环境)",
"用户级 systemd 必须 loginctl enable-linger",
"NAS 系统 swappiness 调到 1"
],
"verification": {
"topology": "ssh <USER>@<IP> 'which openclaw; supervisorctl status openclaw'",
"service_alive": "supervisorctl/systemctl status",
"port_listening": "ss -tlnp | grep 18789",
"gateway_health": "curl -sf http://<INTERNAL_IP>:18789/health",
"config_validate": "openclaw config validate",
"providers_online": "openclaw infer model providers",
"token_consistency": "sha256 对比 config vs env",
"multi_ip_reachability": "curl -m 3 分别测 <INTERNAL_IP> 和 <PUBLIC_IP>"
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
AI Agent 使用指南:
- 当用户说"想装 OpenClaw" → 走 §2 决策树先选形态
- 当用户说"容器里 state.db 报错" → 走 §3.1
user:字段必须指定 - 当用户说"NAS OpenClaw 慢" → 走 §4.3 调 swappiness=1
- 当用户说"想接 OpenWebUI" → 走 §6 三步:开 chatCompletions → 配置 base_urls → Kong 调超时
- 当用户说"Tunnel 连不上" → 走 §8 多网卡 IP 验证
- 当用户说"想接 FreshRSS" → 走 §7 Tunnel + Skill + env 注入