VPS 部署 project-dashboard 的完整踩坑记录:8 个失败与 1 个根因,以及如何避免重演
TL;DR:八个失败,一个根因——多个 agent 共用一个 git 工作目录。解法是”单一所有者、多贡献者”:部署目标由服务用户独占,每个 agent 在自己的 clone 里工作。
摘要
一台 Ubuntu 22.04 VPS,三个编码 agent(OpenClaw、DSH、Codex)共用一个 git 仓库,同时向 GitHub 推送。一次本该简单的部署,连续撞上 8 个不同的错误:mysqldump 缺失、备份目录权限、SSH 别名失效、git 对象损坏、polkit 版本错配、npm 缓存权限……它们看似毫无关联,其实根因只有一个。本文记录每个错误的现场和修复,并给出防止复发的架构方案。
背景
- VPS:Ubuntu 22.04,同时运行 OpenClaw、DeepSeek DSH、Codex 三个 agent
- 仓库:git938/project-dashboard,部署在 /opt/project-dashboard
- 服务:project-dashboard.service,以 OS 用户 project-dashboard(uid 998)运行
- 目标:部署到 commit dbd627d,迁移到 010,不执行 seed
一次本该顺利的部署,前后遇到 8 个独立的错误。下面按发生顺序记录。
错误 1:mysqldump: command not found
部署脚本在备份阶段停下:
line 89: mysqldump: コマンドが見つかりません
MySQL 跑在 Docker 容器里(docker-proxy 监听 127.0.0.1:3306),但宿主机上没装客户端工具。mysqldump 来自 mysql-client 包,从未安装。
修复:
apt-get update
apt-get install -y mysql-client
which mysqldump # /usr/bin/mysqldump
避免复发:凡是 DB 跑在容器里的环境,宿主机上都要显式安装对应的客户端工具。容器提供的是服务,不是工具。
错误 2:备份目录不可写
部署脚本以 project-dashboard 身份运行,但 BACKUP_DIR 指向 /root/backups/…。该目录权限 700、属主 root,服务用户写不进去。
mkdir: ディレクトリ `/root/backups/project-dashboard-...' を作成できません: 許可がありません
修复:把备份目录挪到服务用户拥有的位置:
mkdir -p /var/backups/project-dashboard
chown project-dashboard:project-dashboard /var/backups/project-dashboard
chmod 750 /var/backups/project-dashboard
sed -i 's|^BACKUP_DIR="/root/backups/|BACKUP_DIR="/var/backups/|' /root/deploy-project-dashboard.sh
避免复发:如果部署脚本要以低权限用户运行,那么它需要写的每一个路径都必须是该用户可写的。备份目录、日志目录、缓存目录——一个都不能漏。
错误 3:SSH 别名 github-project-dashboard 解析失败
git 远程地址用的是 SSH 别名:
git@github-project-dashboard:git938/project-dashboard.git
github-project-dashboard 不是真实主机名,而是 ~/.ssh/config 里的别名。root 有这份配置,project-dashboard 没有:
ssh: Could not resolve hostname github-project-dashboard: Temporary failure in name resolution
修复:把配置和部署密钥复制给服务用户:
mkdir -p /home/project-dashboard/.ssh
chmod 700 /home/project-dashboard/.ssh
cp /root/.ssh/deploy_project_dashboard /home/project-dashboard/.ssh/
cp /root/.ssh/deploy_project_dashboard.pub /home/project-dashboard/.ssh/
cp /root/.ssh/known_hosts /home/project-dashboard/.ssh/
cat > /home/project-dashboard/.ssh/config << 'EOF'
Host github-project-dashboard
HostName github.com
User git
IdentityFile ~/.ssh/deploy_project_dashboard
IdentitiesOnly yes
EOF
chown -R project-dashboard:project-dashboard /home/project-dashboard/.ssh
chmod 600 /home/project-dashboard/.ssh/config
chmod 600 /home/project-dashboard/.ssh/deploy_project_dashboard
验证:
sudo -u project-dashboard ssh -T git@github-project-dashboard
# Hi git938/project-dashboard! You've successfully authenticated...
避免复发:SSH 配置是按用户生效的。任何以其他用户身份执行的 git 操作,都需要为该用户单独配置 .ssh/config 和密钥。别假设”root 能访问 = 服务用户也能访问”。
错误 4:git 对象损坏
fatal: loose object dbd627d6c0e11dc6157ceb64e5e2dd70717e021e
(stored in .git/objects/db/d627d...) is corrupt
不是网络问题,不是权限问题——本地对象文件在磁盘上真的坏了。常见诱因:多个身份并发写同一个 .git,或 git 操作被中断。
第一次尝试:删掉损坏对象再 fetch。结果引出错误 5。
错误 5:insufficient permission for adding an object
error: insufficient permission for adding an object to repository database .git/objects
fatal: failed to write object
fatal: unpack-objects failed
.git/objects/ 里混着 root 拥有的文件,来自之前以 root 身份跑的 git 操作。服务用户写不进新对象。
修复:
chown -R project-dashboard:project-dashboard /opt/project-dashboard/.git
find /opt/project-dashboard/.git ! -user project-dashboard | wc -l # 0
fetch 通了,但共享 .git 这个架构本身仍然脆弱。
避免复发:永远不要让多个 OS 用户共用一个 .git 目录。它平时能用,坏起来会让你怀疑人生——错误信息看起来跟病因毫无关系。
错误 6:polkit 认证弹窗(0.105 与 0.106 的差异)
部署脚本执行 systemctl stop project-dashboard,以服务用户身份触发了 polkit:
==== AUTHENTICATING FOR org.freedesktop.systemd1.manage-units ====
Authenticating as: openclaw
Password:
第一反应是写 polkit 规则。写了、重写了、放宽到”放行所有用户”——全都没用。
真正的线索藏在 polkit 日志里:
polkitd started daemon version 0.105 using authority implementation `local' version `0.105'
polkit 0.105 不支持 /etc/polkit-1/rules.d/*.rules。那套 JavaScript 规则机制是 0.106 才引入的。在 0.105 上,polkitd 根本不读 .rules 文件。我写进去的每一条规则都被静默忽略。
0.105 用的是更老的 .pkla 格式:
mkdir -p /etc/polkit-1/localauthority/50-local.d
cat > /etc/polkit-1/localauthority/50-local.d/50-project-dashboard.pkla << 'EOF'
[Allow project-dashboard to manage project-dashboard.service]
Identity=unix-user:project-dashboard
Action=org.freedesktop.systemd1.manage-units
ResultAny=yes
ResultInactive=yes
ResultActive=yes
EOF
systemctl restart polkit
之后:
sudo -u project-dashboard systemctl stop project-dashboard
# 不再弹密码
日志还澄清了一个假线索:polkit 实际看到的是 owned by unix-user:project-dashboard,不是 openclaw。Authenticating as: openclaw 是终端认证代理 pkttyagent 显示错了标签。
避免复发:写配置前先查版本。polkit 0.105 和 0.106 是两套完全不同的规则系统。类似的版本断层在很多工具里都存在——systemd、nginx、openssl、npm 都有类似陷阱。
错误 7:读 .env 时 dotenv 不存在
清空 node_modules 修权限后,部署提前崩了:
Error [ERR_MODULE_NOT_FOUND]: Cannot find module
'/opt/project-dashboard/api/node_modules/dotenv/lib/main.js'
部署脚本用 dotenv 读 .env,但 dotenv 由 npm ci 安装,而 npm ci 排在”读 .env”之后。正常情况下 node_modules 已存在,顺序问题从不暴露。删掉它就暴露了鸡生蛋问题。
修复:在调用部署脚本前先装依赖:
sudo -u project-dashboard npm ci --prefix /opt/project-dashboard/api
/root/final-deploy.sh
避免复发:部署脚本里每一处隐式依赖,都要显式化。如果脚本第一步就需要某个 node 模块,那”这个模块已存在”应该是一道明确的前置检查,而不是靠运气。
错误 8:/home/project-dashboard/.npm 权限不足
npm error code EACCES
npm error path /home/project-dashboard/.npm
npm error Your cache folder contains root-owned files...
npm 缓存里有 root 文件,来自之前以 root 跑的安装。同时 node_modules 里也有一堆 TAR_ENTRY_ERROR 和 ENOTEMPTY,同一个原因。
修复:整体删掉 node_modules,修缓存属主,让 npm 重建:
rm -rf /opt/project-dashboard/api/node_modules
chown -R project-dashboard:project-dashboard /home/project-dashboard/.npm
chown -R project-dashboard:project-dashboard /opt/project-dashboard
避免复发:npm 的缓存、node_modules、全局配置都存在”属主”概念。同一个项目,自始至终只用同一个 OS 用户跑 npm。中途换身份 = 埋雷。
成功部署的输出
八个问题全部解决后,部署脚本干净跑完:
=== Fetching shared code ===
=== Stopping service for consistent backup ===
=== Backing up database and uploads ===
=== Updating code ===
Already up to date.
=== Installing dependencies and migrating ===
> project-dashboard-api@1.0.0 db:migrate
> node migrate.js
Database migrations complete. No seed data was loaded.
=== Starting service ===
Deployment successful.
Commit: dbd627d6c0e11dc6157ceb64e5e2dd70717e021e
Backups: /var/backups/project-dashboard/project-dashboard-20261004-125313
{"status":"ready"}OK
active
dbd627d6c0e11dc6157ceb64e5e2dd70717e021e
ready=200
每一项验收条件都满足:
- ✅ 部署的 HEAD 是 dbd627d
- ✅ MySQL 和 uploads 已备份
- ✅ 迁移到 010
- ✅ 未执行 seed
- ✅ /api/ready 返回 HTTP 200
真正的根因
上面每一个错误,追根究底都是同一件事:三个编码 agent 共用一个工作目录。
OpenClaw、DSH、Codex 都在 /opt/project-dashboard 里工作,都往同一个 GitHub 远程推送。不同的 OS 用户、不同的 sudo 上下文、不同的会话身份。同一个 .git、同一个 node_modules、同一个服务。
症状五花八门——对象损坏、权限错误、polkit 身份错乱、缓存属主冲突——但病因始终是这一个。
根治方案:单一所有者,多贡献者
/opt/project-dashboard 只作为部署目标,任何 agent 不得直接写入。每个 agent 在自己的 home 下各自 clone:
/home/openclaw/repos/project-dashboard
/home/dsh/repos/project-dashboard
/home/codex/repos/project-dashboard
agent 在自己的 clone 里提交、推送。VPS 上的部署脚本以服务用户身份 pull、reset /opt/project-dashboard。部署目标由服务用户独占。
这样能一次性消除:
- 并发写 .git
- node_modules 里的 root 文件
- .npm 缓存的属主混乱
- polkit 身份困惑
给三个 agent 的指令里加入硬性规则:
- 不要写入 /opt/project-dashboard —— 那是部署目标,不是工作区
- 不要在那里运行任何 git 写操作(add / commit / pull / checkout / reset)
- 不要重启 project-dashboard.service
- 不要跑 migration
- 发现 /opt/project-dashboard 状态异常时,报告并停止,不要尝试修复
给未来的自己:可直接复用的检查
部署前跑一次属主检查:
# 有没有不是服务用户拥有的文件?
find /opt/project-dashboard ! -user project-dashboard | wc -l
# 有的话,统一修正(跳过 .git 和 node_modules 之外的东西按需处理)
find /opt/project-dashboard \
-path '*/node_modules' -prune -o \
-path '*/.git' -prune -o \
-user root -print0 | xargs -0 -r chown project-dashboard:project-dashboard
确认 polkit 版本,选对规则格式:
polkitd --version
# 0.105 -> /etc/polkit-1/localauthority/50-local.d/*.pkla
# 0.106+ -> /etc/polkit-1/rules.d/*.rules
本项目完整部署序列:
sudo -u project-dashboard npm ci --prefix /opt/project-dashboard/api
/root/final-deploy.sh
检查 git 健康度:
sudo -u project-dashboard git -C /opt/project-dashboard fsck --full
sudo -u project-dashboard git -C /opt/project-dashboard rev-parse HEAD
sudo -u project-dashboard git -C /opt/project-dashboard ls-remote origin main
经验总结
- 写配置前先查版本。polkit 0.105 与 0.106 是两套系统。半个小时的规则调试,全是在给一个根本不读那个目录的守护进程写文件。
- 容器化的 DB 需要宿主机装客户端。mysqldump 不来自 Docker,来自宿主机的 mysql-client。
- node_modules 不存在时,npm ci 的顺序会成为问题。部署脚本若用某个 node 模块读配置,那个模块必须先存在。
- 永远不要让多个 OS 用户共用一个 .git。平时能用,坏起来全是看不懂的错误。
- sudo -u 不改变 polkit 主体。polkit 看的是会话用户,不是目标用户。规则匹配一个、调用者是另一个,规则就会静默失效。
- find . -user root | wc -l 是廉价有效的体检。每次部署前跑一次,能提前发现属主漂移。
结语
八个独立的失败,一个共同的根因。VPS 本身没问题——有问题的是”三个 agent 共用一个工作目录”这个架构。换成”单一所有者、多贡献者”模式后,下游的一切都顺了:不再有损坏对象、不再有权限互掐、不再有 polkit 意外。
部署成功只是副产品。这套经验才是这次真正拿到的东西。
