手把手:虚拟机共享文件夹,以及用 Caddy + Cloudflare Tunnel 发布 pages
宿主机上建一个共享目录,用 virtiofs 挂进所有 agent 虚拟机;pages 文档放在里面,由单独的 LXC 容器只读挂载,用 Caddy 提供网页、cloudflared 发布到公网;宿主机自动保存历史版本。
整体结构
宿主机 /srv/shared ├── pages/ ──(virtiofs,可读写)──▶ agent-01、agent-02 … 的 /mnt/shared/pages ← agent 在这里写文档 │ └─(挂载点,只读)────▶ LXC web 的 /srv/pages │ ├── Caddy 127.0.0.1:8787 │ └── cloudflared → https://pages.chenos.dev ├── ref/ 参考仓库(ash、statesman……) └── inbox/ 虚拟机之间临时交换的文件 宿主机每 10 分钟自动做一次 git 提交 → 保留 pages 的所有历史版本
| 好处 | 说明 |
|---|---|
| 网站更稳定 | 哪台 agent 虚拟机坏了、回滚了、正在重装,网站都照常运行 |
| 配置和现在一模一样 | LXC 里仍然是 Caddy 监听 127.0.0.1:8787,隧道转发到本机 |
| 更安全 | 网站容器只能读文档;agent 虚拟机里也没有隧道的密钥 |
| 有历史记录 | agent 误删或者改坏了文档,可以恢复到任意一个历史版本 |
| 所有 agent 都能发布 | 不论在哪台虚拟机里写,文件出现在共享文件夹里就自动上线 |
后文用到的地址:PVE 宿主机 192.168.50.205,proxy 容器 192.168.50.101,web 容器 192.168.50.102。换成你自己的网段。
哪些放共享,哪些不放
| 内容 | 例子 | 放在哪里 |
|---|---|---|
| 文档 | pages | ✅ 共享文件夹,所有 agent 都能写 |
| 参考用的仓库(主要拿来读) | ash、statesman、symfony-workflow、prisma-orm 等 | ✅ 可以共享,放一份就够,最好以只读方式使用 |
| 正在开发的项目 | nocobase3、nocobase3-pro 及其 worktree | ❌ 每台虚拟机各自 clone |
为什么开发中的源码不能共享
| 问题 | 会发生什么 |
|---|---|
| Git 状态是所有人共用的 | 只有一个 .git。agent-01 切换分支,agent-02 手上的代码就跟着变了;同时 git commit 会因为 index.lock 冲突而报错 |
| 改同一份文件 | 两个 agent 改同一个文件,会直接互相覆盖,事先没有任何提示 |
| node_modules 和构建产物 | 一台执行 pnpm install 或构建,会改掉另一台正在用的依赖;大量小文件在 virtiofs 上读写也更慢 |
| 监听文件变化失效 | 别的虚拟机改了文件,这台收不到通知,热更新不会触发 |
| 数据库和端口 | 每个 agent 跑测试都要用自己的数据库和端口,共享代码目录会让配置互相干扰 |
正确的做法:每个 agent 在自己的虚拟机里 clone 一份,各自在自己的分支上工作,改完推到 GitHub,再通过 PR 合并。
让每台虚拟机都很快有源码:在模板机里(转换成模板之前)预先 clone 并装好依赖:
cd ~/NocoBase
git clone https://github.com/nocobase/nocobase.git nocobase3 # 换成你实际用的仓库地址和分支
cd nocobase3 && pnpm install
克隆出来的虚拟机只需要 git pull && pnpm install,只装有变化的部分,很快。私有仓库的 Git 凭证不要留在模板里。
第二部分:把 pages 放进共享文件夹
在原来那台机器上执行,把现有文档传到宿主机:
rsync -av ~/NocoBase/pages/ root@192.168.50.205:/srv/shared/pages/
传完后,回到宿主机修正文件所有者和权限,确保 agent 能写、网站能读:
chown -R 1000:1000 /srv/shared/pages chmod -R u+rwX,go+rX /srv/shared/pages
sudo tailscale up --accept-routes,才能访问家里的网段。第三部分:web 容器里装 Caddy
3.1创建 LXC 容器
和创建 proxy 容器一样(见 搭建教程第一部分),在 PVE 网页上点 创建 CT,只有下面几项不同:
| 项目 | 填什么 |
|---|---|
| CT ID / 主机名 | 102 / web |
| 磁盘 | 2 GiB |
| 内存 | 512 MiB |
| IPv4 | 192.168.50.102/24,网关 192.168.50.1 |
创建好后,在 选项 里打开 开机自动启动。进 控制台,按搭建教程 1.3 换镜像源,并安装 curl。
3.2把 pages 以只读方式挂进容器
在宿主机 Shell 里执行:
pct set 102 -mp0 /srv/shared/pages,mp=/srv/pages,ro=1 pct reboot 102
3.3安装和配置 Caddy
进入 web 容器的控制台:
apt install -y caddy
cat > /etc/caddy/Caddyfile <<'EOF'
{
auto_https off
}
:8787 {
bind 127.0.0.1
root * /srv/pages
encode gzip
file_server {
hide .git .gitignore
}
header /*.md Content-Type "text/markdown; charset=utf-8"
}
EOF
systemctl restart caddy
curl -sI http://127.0.0.1:8787/ | head -1 # 应该输出 HTTP/1.1 200 OK
:8787 再加 bind 127.0.0.1,不要写成 http://127.0.0.1:8787。后一种写法会让 Caddy 只响应 Host 是 127.0.0.1 的请求,而隧道转过来的请求 Host 是 pages.chenos.dev,结果就是返回空白页面。browse:文档只通过隐藏的 toc.html 进入,开了目录列表,任何人都能从 /nocobase/ 这类路径看到全部文件名。hide .git .gitignore 是因为 pages 本身是一个 git 仓库(chenos/pages),不隐藏的话 /.git/config 等文件会被直接下载。第四部分:同一个容器里装 cloudflared,搬迁现有隧道
4.1安装 cloudflared
在 web 容器的控制台里执行:
# 临时通过 mihomo 下载
export http_proxy=http://192.168.50.101:7890 https_proxy=http://192.168.50.101:7890
mkdir -p --mode=0755 /usr/share/keyrings
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg -o /usr/share/keyrings/cloudflare-main.gpg
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared any main' \
> /etc/apt/sources.list.d/cloudflared.list
apt update && apt install -y cloudflared
unset http_proxy https_proxy
cloudflared --version
4.2复制隧道密钥文件
直接沿用原来的隧道,Cloudflare 那边的 DNS 一点都不用改,只需要把隧道的密钥文件搬过来。下面用 <隧道ID> 代表你的隧道编号,在原来那台机器上执行 ls ~/.cloudflared/*.json 就能看到,文件名就是隧道 ID。
Debian 容器默认不允许 root 用密码登录 SSH,所以先把文件传到宿主机,再推进容器。在原来那台机器上:
scp ~/.cloudflared/<隧道ID>.json root@192.168.50.205:/root/
在宿主机 Shell 里:
pct exec 102 -- mkdir -p /etc/cloudflared
pct push 102 /root/<隧道ID>.json /etc/cloudflared/<隧道ID>.json
rm /root/<隧道ID>.json # 用完马上从宿主机上删掉
.json 文件就是隧道的密码,谁拿到它,谁就能接管你的隧道。不要发给别人,也不要提交到 Git 仓库里。4.3写隧道配置
回到 web 容器的控制台:
cat > /etc/cloudflared/config.yml <<'EOF'
tunnel: <隧道ID>
credentials-file: /etc/cloudflared/<隧道ID>.json
ingress:
- hostname: pages.chenos.dev
service: http://127.0.0.1:8787
- service: http_status:404
EOF
chmod 600 /etc/cloudflared/*.json
cloudflared tunnel ingress validate # 检查转发规则有没有写错
4.4切换:先停旧的,再启新的
同一条隧道如果在两台机器上同时运行,Cloudflare 会把访问者随机分配到两边,可能一会儿看到新内容、一会儿看到旧内容。所以先停掉旧的。
在原来那台机器上:
sudo systemctl disable --now cloudflared
sudo systemctl disable --now caddy # 不再需要了,也可以保留
在 web 容器里:
cloudflared service install # 安装成系统服务,自动读取 /etc/cloudflared/config.yml systemctl enable --now cloudflared journalctl -u cloudflared -n 30 # 看到几行 "Registered tunnel connection" 就说明成功了
4.5验证
用任意一台设备的浏览器打开 pages.chenos.dev,能看到搬过来的文档就成功了。中间网站中断的时间只有几十秒。在任意一台 agent 虚拟机里往 ~/NocoBase/pages/ 写个文件,刷新网页应该马上就能看到。
4.6以后想发布新的服务
比如 docs.chenos.dev:
- 在 web 容器的
config.yml里,在http_status:404那一行之前加一条规则,然后执行systemctl restart cloudflared - 在 Cloudflare 后台的 DNS 页面添加一条 CNAME 记录:名称填
docs,目标填<隧道ID>.cfargotunnel.com,并打开代理(橙色云朵)
第五部分:自动保存历史版本
在宿主机上用 Git 每 10 分钟自动提交一次。Git 的数据放在 pages 目录外面:agent 改不了历史记录,网站也不会把 .git 目录暴露出去。
apt install -y git
git init --bare /srv/pages-history.git
G="git --git-dir=/srv/pages-history.git --work-tree=/srv/shared/pages"
$G config user.name pages-bot && $G config user.email pages-bot@localhost
$G add -A && $G commit -qm init
# 每 10 分钟自动提交一次(没有改动时什么都不做)
cat > /etc/cron.d/pages-history <<'EOF'
*/10 * * * * root git --git-dir=/srv/pages-history.git --work-tree=/srv/shared/pages add -A && git --git-dir=/srv/pages-history.git --work-tree=/srv/shared/pages commit -qm "auto $(date +\%F_\%T)" >/dev/null 2>&1
EOF
找回误删或改坏的文件
G="git --git-dir=/srv/pages-history.git --work-tree=/srv/shared/pages" $G log --stat -- workstation/ # 查看某个目录的改动历史 $G checkout <提交编号> -- workstation/setup-guide.html # 把这个文件恢复到那个版本
push,这样就有了异地备份。第六部分:谁能改 pages,怎么限制
按上面的配置,所有挂载了共享文件夹的虚拟机都能读写 pages:
| 机器 | 对 pages 的权限 | 原因 |
|---|---|---|
| agent-01、agent-02…… | ✅ 可读写 | 通过 virtiofs 以读写方式挂载;每台虚拟机里 dev 用户的编号都是 1000,和目录的所有者一致 |
| web 容器(Caddy) | 👁 只读 | 挂载时加了 ro=1 |
| PVE 宿主机 | ✅ 可读写 | 文件本来就存在宿主机上 |
任何一台虚拟机里新建或修改的文件,其他虚拟机马上就能看到,网站上也会马上更新。
想限制的话,有三档可以选
第 1 档:全部可写(默认)
最简单,适合只有你一个人在用、agent 都是你自己的情况。误改、误删靠第五部分的自动历史记录找回来。
第 2 档:部分虚拟机只读
在那台虚拟机的 /etc/fstab 里,把挂载选项从 defaults 改成 ro:
shared /mnt/shared virtiofs ro,nofail 0 0
然后执行 sudo umount /mnt/shared && sudo mount /mnt/shared 生效。
第 3 档:每台虚拟机只能写自己的子目录(最严格)
在 PVE 里为每台虚拟机单独登记一个目录映射,只映射它自己的那部分:
| 目录映射名称 | 宿主机上的路径 | 给哪台虚拟机 |
|---|---|---|
pages-agent01 | /srv/shared/pages/agent-01 | agent-01 |
pages-agent02 | /srv/shared/pages/agent-02 | agent-02 |
每台虚拟机只能看到、也只能修改自己的子目录。即使在虚拟机里有 root 权限,也碰不到别人的文件,因为这个限制是在宿主机这一侧做的。
代价是 agent 不能再改总目录 toc.html 了。这时可以改成在宿主机上写一个小脚本,每隔几分钟扫描各个子目录,自动重新生成 toc.html。
第七部分:多个 agent 一起写文档的规则
建议写进模板里的 ~/.claude/CLAUDE.md,这样每台克隆机都会带上:
## pages 文档 - 文档写到 ~/NocoBase/pages/<项目或主题>/ 下面,按主题建子目录,不要直接放在根目录 - 修改 toc.html 之前先重新读一遍,只追加自己的条目,不要整个文件覆盖 - pages 会公开发布到 https://pages.chenos.dev,不要写入任何密码、token、内部地址
pages.chenos.dev 加一层登录验证,比如只允许你自己的邮箱访问。连不上时逐项排查
| 现象 | 原因 | 解决 |
|---|---|---|
虚拟机里 mount -t virtiofs 报错 | virtiofs 设备没生效 | 确认已在 硬件 里加了 Virtiofs,并且虚拟机是关机后再开机的 |
| 虚拟机里写共享文件夹提示没有权限 | 目录所有者不对 | 在宿主机执行 chown -R 1000:1000 /srv/shared;在虚拟机里用 id 确认 dev 的编号是 1000 |
日志里出现 expected at least 2 Cloudflare Regions | DNS 返回的 SRV 记录不完整 | PVE → web 容器 → DNS,把 DNS 服务器改成 1.1.1.1,然后重启容器 |
网站报 Error 1033,cloudflared 日志里连接的边缘节点 IP 是 198.18.x.x | 同一台机器上开着 Clash 的 TUN 模式,fake-ip 接管了 DNS,QUIC(UDP)走不通(实际在旧机器上遇到过) | 在 config.yml 第一行加 protocol: http2,重启 cloudflared;或者在 Clash 里让 argotunnel.com、cftunnel.com 走直连 |
| 日志里反复连接失败,提示 QUIC 或 UDP 相关错误 | 网络屏蔽了 UDP 7844 端口 | 在 config.yml 最上面加一行 protocol: http2,然后重启 cloudflared |
| 网站显示 502 Bad Gateway | 隧道连不上 Caddy | 在容器里执行 systemctl status caddy 和 curl -I http://127.0.0.1:8787 |
| 页面能打开,但一片空白 | Caddy 站点地址写错了 | 检查 Caddyfile 里写的是不是 :8787 加 bind 127.0.0.1 |
| 某个新文档打开是 403 或 404 | 文件权限不允许“其他用户”读 | 在宿主机执行 chmod -R go+rX /srv/shared/pages |
| 隧道经常断开 | 国内连接 Cloudflare 的线路不稳定;cloudflared 的隧道连接不走 HTTP 代理 | 先试 protocol: http2;还不行就要把 mihomo 改成透明网关(TUN 模式),让 web 容器的流量也经过它 |
注意事项
- 快照回滚不会影响共享文件夹:共享文件夹在宿主机上,不在虚拟机硬盘里。回滚不会丢共享数据;但反过来,agent 在共享文件夹里误删的文件,回滚也救不回来(pages 可以靠第五部分的历史记录找回)。
- PVE 的虚拟机备份不包括共享文件夹,需要单独备份。比如在宿主机上设一个每天运行的任务,用
rsync把/srv/shared复制到 4T 数据盘或者 NAS 上。 - 别让多个 agent 同时改同一个文件:共享文件夹没有锁,同时修改会互相覆盖。按用途分好子目录。
- 安全方面:任何一台虚拟机都能改共享文件夹里的内容。重要的东西只给需要的虚拟机挂载,或者按第六部分的第 3 档限制。
- LXC 容器挂共享目录要用“挂载点”(mount point),无特权容器里的用户编号会被平移(比如 1000 变成 101000)。只读使用(像 web 容器这样)没问题;要写入就比较麻烦,有需要再单独处理。
- 笔记本也想直接访问:可以在某台虚拟机里装 Samba,把
/mnt/shared共享出去;或者继续用 VS Code Remote-SSH 和scp。