告别 Docker:凌骁的基础设施大迁移实录
有些事情,做完才发现——本来就不需要 Docker。
2026年4月2日,OpenClaw 的基础设施经历了一次全面重构。不是那种「规划了六个月、PPT 画了一百页」的迁移,而是那种「今天就动手、今晚就验证」的实干式迁移。事后回头看,这一天发生的事情远不止迁移本身——它是 OpenClaw 从「能跑」到「跑得优雅」的分水岭。
事件一:凌骁失去了 exec 权限
早上,凌骁(OpenClaw 的 AI 助手)在 Discord 里报告了一个奇怪的问题:我只有 4 个工具。
正常情况下应该有 17 个工具,包括 exec、read、write 等核心能力。但那天早上,凌骁的工具箱里只剩下:memory_search、memory_get、web_search、web_fetch。
排查下来,根因是三层叠加的配置问题:
第一层:AGENTS.md 的错误描述
AGENTS.md 里写着「Discord sessions 没有 exec 权限」——这条本来是历史遗留的保守配置,结果被当成了事实。
第二层:tools.allow 白名单不完整
// 修复前(只有 web 和 automation)
{
"allow": ["group:web", "group:automation"]
}
// 修复后(加入 fs 和 runtime)
{
"allow": ["group:web", "group:automation", "group:fs", "group:runtime"]
}
第三层:profile 配置指向了空对象
profile: "full" 对应的是一个空配置对象,而正确的完整工具集在 profile: "coding" 里。把 profile 改过来之后,工具数从 4 → 17,恢复正常。
这个问题看起来小,但影响不小——凌骁在缺少 exec 的情况下,很多自动化任务都没法直接执行,只能绕行或依赖 task-queue。
事件二:OpenClaw 3.28 → 3.31 → 4.1
趁着修配置,顺手把 OpenClaw 从 3.28 升到了最新的 4.1。
3.31 的主要变化
- SQLite task registry:新增了任务队列的持久化存储
- exec approval 重构:审批流程更清晰,Discord 按钮更好用
- Discord 插件翻倍:从 ~10 个命令增长到 ~20 个
4.1 的主要修复
- SQLite 同步卡死:修复了并发写入时 WAL 模式下的死锁问题
- 新增
/tasks命令:可以在 Discord 里直接查看后台任务队列 - exec allow-always 持久化:之前每次重启都要重新授权,现在持久化了
一个 patch:task-store-pg.mjs
因为生产环境已经有 PostgreSQL,不想再引入 SQLite,所以写了一个替代 patch:
// task-store-pg.mjs — PostgreSQL 替代 SQLite task store
import pg from 'pg';
export class PgTaskStore {
constructor(connStr) {
this.pool = new pg.Pool({ connectionString: connStr });
}
async init() {
await this.pool.query(`
CREATE TABLE IF NOT EXISTS oc_tasks (
id TEXT PRIMARY KEY,
type TEXT NOT NULL,
status TEXT DEFAULT 'pending',
payload JSONB,
result JSONB,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
)
`);
}
async enqueue(id, type, payload) {
await this.pool.query(
'INSERT INTO oc_tasks (id, type, payload) VALUES ($1, $2, $3)',
[id, type, JSON.stringify(payload)]
);
}
async complete(id, result) {
await this.pool.query(
'UPDATE oc_tasks SET status=$1, result=$2, updated_at=NOW() WHERE id=$3',
['done', JSON.stringify(result), id]
);
}
}
这个 patch 避免了引入新的 SQLite 依赖,也更符合整体的「全 PostgreSQL」架构目标。
事件三:Docker → 原生 systemd(主角登场)
这才是今天的重头戏。
迁移前的状态
OpenClaw 的基础服务运行在 5 个 Docker 容器里:
| 容器名 | 服务 | 端口 |
|---|---|---|
| oc-db | PostgreSQL 14 | 5434 |
| oc-redis | Redis 7 | 6380 |
| oc-minio | MinIO | 9002 |
| oc-api | Flask/Gunicorn | 4001 |
| oc-monitor | Node.js monitor | — |
Docker 本身没什么问题,但在一台 AMD Strix Halo 128GB 的裸金属机器上,为了跑几个本地服务而维护 Docker 网络、volume 挂载、container lifecycle,总感觉是在搬大炮打蚊子。更直接的原因:PostgreSQL 14 的 pgvector 版本较旧,想升级到 PG17,顺手就把架构也理顺了。
迁移步骤
Step 1:添加 PGDG 官方源
sudo apt install -y postgresql-common
sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh
sudo apt update
sudo apt install -y postgresql-17 postgresql-17-pgvector
Step 2:pg_dump 迁移数据
# 从 Docker 容器导出
docker exec oc-db pg_dump -U postgres openclaw > /tmp/openclaw_backup.sql
# 文件大小:14MB,涵盖 17 张表
# 导入到原生 PG17
sudo -u postgres psql -c "CREATE DATABASE openclaw;"
sudo -u postgres psql openclaw < /tmp/openclaw_backup.sql
# 验证
sudo -u postgres psql openclaw -c "\dt" | wc -l # 应该是 17+
Step 3:安装原生 Redis 7
sudo apt install -y redis-server
# 默认监听 6379,不需要额外配置
sudo systemctl enable --now redis-server
Step 4:创建 systemd 服务单元
以 oc-api 为例:
# /etc/systemd/system/oc-api.service
[Unit]
Description=OpenClaw API (Flask/Gunicorn)
After=network.target postgresql.service redis.service
Requires=postgresql.service
[Service]
Type=notify
User=borui
WorkingDirectory=/home/borui/openclaw/api
EnvironmentFile=/home/borui/openclaw/api/.env
ExecStart=/home/borui/openclaw/api/venv/bin/gunicorn \
--workers 4 \
--bind 0.0.0.0:4001 \
--timeout 120 \
app:app
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
Step 5:更新配置文件
共有 6 处配置需要更新(数据库连接字符串从 5434 改到 5432,Redis 从 6380 改到 6379):
# .env 文件
DATABASE_URL=postgresql://postgres:password@localhost:5432/openclaw
REDIS_URL=redis://localhost:6379/0
# 之前是
# DATABASE_URL=postgresql://postgres:password@localhost:5434/openclaw
# REDIS_URL=redis://localhost:6380/0
Step 6:验证并收尾
# 检查所有服务状态
systemctl status postgresql redis-server oc-api oc-monitor
# 验证 API 健康
curl http://localhost:4001/api/health
# {"status":"ok"}
# 验证 pgvector
sudo -u postgres psql openclaw -c "SELECT extversion FROM pg_extension WHERE extname='vector';"
# 0.8.0
# 停止并删除 Docker 容器
docker stop oc-db oc-redis oc-minio oc-api oc-monitor
docker rm oc-db oc-redis oc-minio oc-api oc-monitor
docker system prune -af # 释放约 2GB 空间
迁移后的状态
| 服务 | 端口(前) | 端口(后) | 运行方式 |
|---|---|---|---|
| PostgreSQL 17 + pgvector 0.8 | 5434 | 5432 | systemd |
| Redis 7 | 6380 | 6379 | systemd |
| MinIO | 9002 | 9002 | systemd |
| Flask/Gunicorn oc-api | 4001 | 4001 | systemd |
| Node.js oc-monitor | — | — | systemd |
零 Docker,全 systemd,端口全部回归标准。docker ps 输出空白,清爽。
事件四:Blog 自动发布管道上线
作为今天的收尾,Blog 的自动发布管道也正式上线了。
架构很简单:fetcher.py 每天凌晨 6 点(Vancouver 时间)抓取 AI 新闻,auto_publisher.py 调用 Anthropic Claude API 生成中英双语文章,然后通过 Blog Bot API 发布。
# crontab
0 6 * * * /home/borui/openclaw/blog/run_pipeline.sh >> /var/log/blog_auto.log 2>&1
# auto_publisher.py 核心逻辑
import anthropic
client = anthropic.Anthropic(api_key=ANTHROPIC_TOKEN)
def generate_article(news_items):
prompt = build_prompt(news_items)
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=4096,
messages=[{"role": "user", "content": prompt}]
)
return parse_article(response.content[0].text)
这篇文章本身,就是通过这套管道发布的第一篇手动触发文章。
总结
今天做的事情,用一句话概括:把 OpenClaw 从「能跑的原型」变成了「跑得优雅的系统」。
Docker 没有错,但不是每个场景都需要它。当你的服务和宿主机之间没有隔离需求,当你的团队就是你一个人,当你想要 systemctl status 一条命令看清所有服务状态——原生 systemd 就是更好的选择。
端口回归标准,配置更清晰,少了一层抽象,少了 2GB 镜像,多了可读性和可维护性。值。