pages.chenos.dev / workstation / 共享文件夹与 pages

手把手:虚拟机共享文件夹,以及用 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 凭证不要留在模板里。

第一部分:建共享文件夹(virtiofs)

virtiofs 把 PVE 宿主机上的一个目录直接挂进虚拟机。PVE 9 的网页界面就能配置,不走网络,速度接近读写本地硬盘。

1.1在宿主机上建目录

PVE 网页 → 点节点名 → Shell:

mkdir -p /srv/shared/pages /srv/shared/ref /srv/shared/inbox
chown -R 1000:1000 /srv/shared     # 1000 是虚拟机里 dev 用户的编号
所有虚拟机都是从同一个模板克隆出来的,里面的 dev 用户编号都是 1000,所以每台虚拟机里文件的所有者和权限都是一致的。

1.2在 PVE 里登记这个目录

数据中心 → 目录映射(Directory Mappings)→ 添加:

1.3给虚拟机加上 virtiofs 设备

选中虚拟机 → 硬件 → 添加 → Virtiofs → 目录 ID 选 shared → 添加。

加完之后,虚拟机要完全关机,再开机才会生效。直接重启不行。
在做模板之前加到 agent-01 上:做模板时会一起带过去,以后克隆出来的虚拟机都自动有这个设备(见 模板教程第四部分)。已经克隆出来的虚拟机,要一台一台单独加。
菜单名称和“加了 virtiofs 的虚拟机能否拍包含内存的快照”需要实机确认,见 待实机确认。

1.4在虚拟机里挂载

sudo mkdir -p /mnt/shared
sudo mount -t virtiofs shared /mnt/shared

# 设置开机自动挂载
echo 'shared /mnt/shared virtiofs defaults,nofail 0 0' | sudo tee -a /etc/fstab

# 测试
touch /mnt/shared/inbox/hello-from-$(hostname)
ls -l /mnt/shared/inbox

在另一台虚拟机里执行 ls /mnt/shared/inbox,如果能看到这个文件,就说明共享成功了。

1.5建软链接,保留原来的路径

每台虚拟机里执行一次:

mkdir -p ~/NocoBase
ln -s /mnt/shared/pages ~/NocoBase/pages
ln -s /mnt/shared/ref   ~/NocoBase/ref

agent 看到的还是 ~/NocoBase/pages 这些熟悉的路径,CLAUDE.md 里写的路径基本不用改。想让参考仓库的路径和以前完全一样,也可以单独建链接,比如 ln -s /mnt/shared/ref/ash ~/NocoBase/ash。

这一步也可以在模板里做好,克隆出来的虚拟机就都有了。

第二部分:把 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
原来那台机器如果是 Linux 并且装了 Tailscale,要先执行一次 sudo tailscale up --accept-routes,才能访问家里的网段。

第三部分:web 容器里装 Caddy

3.1创建 LXC 容器

和创建 proxy 容器一样(见 搭建教程第一部分),在 PVE 网页上点 创建 CT,只有下面几项不同:

项目填什么
CT ID / 主机名102 / web
磁盘2 GiB
内存512 MiB
IPv4192.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 等文件会被直接下载。
能读到文件的原因:无特权容器里的 caddy 用户,在宿主机看来是一个“其他用户”。第二部分已经给所有文件加了“其他用户可读”权限,agent 新建的文件默认是 644 或 664,“其他用户”同样可以读。

第四部分:同一个容器里装 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:

  1. 在 web 容器的 config.yml 里,在 http_status:404 那一行之前加一条规则,然后执行 systemctl restart cloudflared
  2. 在 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   # 把这个文件恢复到那个版本
想再多一层保险,可以给这个仓库加一个 GitHub 私有仓库作为远程地址,在定时任务里顺便 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 生效。

在虚拟机里设置的只读,有 sudo 权限的人可以自己改回去。agent 一般都有 sudo 权限,所以这只能防止“不小心改错”,防不了“故意改”。要彻底禁止修改,就要在宿主机这一侧限制,也就是第 3 档。

第 3 档:每台虚拟机只能写自己的子目录(最严格)

在 PVE 里为每台虚拟机单独登记一个目录映射,只映射它自己的那部分:

目录映射名称宿主机上的路径给哪台虚拟机
pages-agent01/srv/shared/pages/agent-01agent-01
pages-agent02/srv/shared/pages/agent-02agent-02

每台虚拟机只能看到、也只能修改自己的子目录。即使在虚拟机里有 root 权限,也碰不到别人的文件,因为这个限制是在宿主机这一侧做的。

代价是 agent 不能再改总目录 toc.html 了。这时可以改成在宿主机上写一个小脚本,每隔几分钟扫描各个子目录,自动重新生成 toc.html。

建议:起步用第 1 档,加上第七部分的写作规则和第五部分的自动历史记录,一个人用已经够了。以后 agent 多了、出现互相覆盖,或者要给别人开虚拟机用,再升级到第 3 档。
容易忽略的一点:在共享文件夹里,虚拟机里的 root,就相当于宿主机上这个目录的 root。virtiofs 会把操作范围限制在共享的目录里,影响不到宿主机的其他地方,但在这个目录里面,它想改什么都可以。这也是第 2 档防不住故意修改、必须用第 3 档的原因。

第七部分:多个 agent 一起写文档的规则

建议写进模板里的 ~/.claude/CLAUDE.md,这样每台克隆机都会带上:

## pages 文档
- 文档写到 ~/NocoBase/pages/<项目或主题>/ 下面,按主题建子目录,不要直接放在根目录
- 修改 toc.html 之前先重新读一遍,只追加自己的条目,不要整个文件覆盖
- pages 会公开发布到 https://pages.chenos.dev,不要写入任何密码、token、内部地址
所有放进 pages 的内容都会公开到互联网上。现在 agent 是自动写文档的,更需要注意这一点。如果有些内容只想自己看,可以在 Cloudflare 后台用 Zero Trust → Access 给 pages.chenos.dev 加一层登录验证,比如只允许你自己的邮箱访问。

连不上时逐项排查

现象原因解决
虚拟机里 mount -t virtiofs 报错virtiofs 设备没生效确认已在 硬件 里加了 Virtiofs,并且虚拟机是关机后再开机的
虚拟机里写共享文件夹提示没有权限目录所有者不对在宿主机执行 chown -R 1000:1000 /srv/shared;在虚拟机里用 id 确认 dev 的编号是 1000
日志里出现 expected at least 2 Cloudflare RegionsDNS 返回的 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 容器的流量也经过它

注意事项