Polar 预览环境架构全解exe.dev 克隆 VM Tailscale 私有网络的 PR 级全栈部署方案【免费下载链接】polarPolar — A billing platform for the intelligence era项目地址: https://gitcode.com/GitHub_Trending/po/polar本文以 Polar 仓库的 预览环境说明文档 为核心深入剖析这套每个 Pull Request 一台独立虚拟机、整套服务跑在 Tailscale 私有网络中的预览环境设计与实现。Polar 的预览环境没有使用常见的容器或 Serverless 方案而是采用 exe.dev 的写时复制 VM 克隆加 MagicDNS 证书、tailnet-only 访问为每一个 PR 提供包含 APIuvicorn、任务队列dramatiq、Redis、Next.js 前端、Postgres 与 Tinybird 分支的完整独立环境。读完本文你将理解这套系统从黄金镜像构建、工作流触发、远程部署、增量构建到调试日志的完整链路并能直接复现其中的关键命令。一、架构总览每 PR 一台 VM整套服务跑在私有网络上预览环境的核心设计是一个 PR 对应一个 exe.dev VM该 VM 从一台名为polar-base的黄金镜像克隆而来。每台 VM 上运行完整的 Polar 全栈组件端口说明uvicornAPI 服务:10000FastAPI 应用入口见 run-preview-backend.shdramatiq任务队列 worker—消费high_priority、medium_priority、low_priority三个队列Redis:6379系统级 redis-server一台预览 VM 独占一个实例Next.js 前端next dev:3000使用 Turbopack 的开发服务器Caddy 反向代理:8000/:443统一入口443 上通过 tailscaled 获取 MagicDNS 证书终结 TLS日志查看器:9990通过 Caddy 的/_logs/*路径对外暴露 journalctl 日志整体访问方式为tailnet-only预览地址是https://pr-N.tailnet.ts.net只有加入该 Tailscale 网络的成员才能访问。Caddy 同时监听:8000exe.dev 的 share 端口与:443其中 443 的 TLS 证书通过 Caddy 的get_certificate tailscale指令直接从 tailscaled 获取 MagicDNS 证书deploy.sh 中会执行tailscale set --operatorcaddy来赋予 Caddy 调用证书接口的权限。两个重量级共享服务——Postgres每个预览环境一份 template 复制的独立数据库与Tinybird每个预览环境一个独立 branch——由 GitHub Workflow 调用 server/scripts/preview.py 统一供给不随 VM 克隆。二、前置条件账号、凭证与网络策略在部署任何预览环境之前需要准备四类资源exe.dev 账号与 CI SSH 密钥CI 通过 SSH 操作 VM需要将密钥注册到 exe.dev 账号ssh exe.dev ssh-key add。Tailscale trust credential信任凭证要求具备 Auth Keys 写入权限且仅携带tag:preview标签。这里有一个容易踩坑的细节铸造出的 key 必须携带与凭证完全一致的标签集合编辑凭证的标签不会重新作用于已经签发的 secret——修改标签后必须重新生成 secret否则旧 key 依然沿用旧标签。GitHub Secrets 清单Workflow 依赖以下 Secrets 完成远程部署POLAR_PREVIEW_SSH_KEYexe.dev 账号密钥POLAR_PREVIEW_TAILSCALE_OAUTH_SECRET信任凭证的 secretPOLAR_PREVIEW_STRIPE_SECRET_KEY、POLAR_PREVIEW_STRIPE_WEBHOOK_SECRETStripe 测试密钥POLAR_PREVIEW_PYDANTIC_AI_GATEWAY_API_KEYPydantic AI 网关密钥已有的POLAR_PREVIEW_POSTGRES_ADMIN_DSN与POLAR_PREVIEW_TINYBIRD_ADMIN_TOKEN用于供给共享 Postgres 与 Tinybird 资源Tailnet 网络策略ACL需要满足三条规则——普通成员可以访问tag:preview的 443 端口tag:preview节点只能访问预览 Postgres 的 5432 端口、不能访问其他任何资源Tailscale 管理后台需开启 MagicDNS 与 HTTPS 证书功能。在 preview.yml 中可以看到这些变量的实际使用Workflow 通过tailscale/github-action以 OAuth client 身份加入 tailnetCI 自身使用tag:ci标签供给资源时读取POLAR_PREVIEW_POSTGRES_ADMIN_DSN、POLAR_PREVIEW_TINYBIRD_ADMIN_TOKEN等 Secrets并通过vars.POLAR_PREVIEW_BASE_DOMAIN默认preview.polar.sh见 preview.py 顶部的DEFAULT_PREVIEW_BASE_DOMAIN常量等变量控制域名与端口。三、黄金镜像 polar-base一切预览 VM 的模板3.1 首次构建由于 exe.dev 的 setup 脚本以非特权用户运行构建镜像必须通过 SSH 以 sudo 执行ssh exe.dev new --namepolar-base ssh polar-base.exe.xyz sudo git clone --depth 50 https://github.com/polarsource/polar.git /srv/polar sudo bash /srv/polar/infra/preview/setup-base-vm.shsetup-base-vm.sh 是一个可重复执行幂等的 8 步构建脚本完整步骤为配置 swap创建 1G 的/swapfile并写入 fstab同时设置 journald 的SystemMaxUse100M避免日志撑爆小磁盘。安装系统包redis-server、git、curl、jq、rsync、util-linux、build-essential、libpq-dev并启用系统 redis一台预览 VM 一份 redis监听127.0.0.1:6379。安装 Caddy通过 Cloudsmith 仓库安装 stable 版。安装 uv使用 Astral 官方安装脚本并将uv/uvx复制到/usr/local/bin。安装 Node.js 24 与 pnpm通过 nodesource 安装 Node 24再用corepack enablecorepack prepare pnpmlatest --activate启用 pnpm。安装 Tailscale只安装、不加入网络——如果黄金镜像就加入了 tailnet克隆出来的每台预览 VM 都会重复节点身份。每台预览 VM 会在首次部署时才加入 tailnet见 deploy.sh。克隆仓库并预热依赖执行uv sync --frozen、uv run task emails邮件渲染器、uv run task backoffice后台静态资源以及带POLAR_SKIP_DTS1的pnpm install --frozen-lockfile。脚本还会把当前 commit 写入/srv/polar/.deployed_sha作为后续增量构建的基线。安装预览工具与 systemd 单元把deploy.sh、run-preview-backend.sh、log-viewer.py安装到/srv/preview-tools把 Caddyfile 复制到/etc/caddy/Caddyfile并把四个 systemd 单元复制到/etc/systemd/system/。构建完成后脚本会输出下一步操作提示用ssh exe.dev share port polar-base 8000验证 Caddy 的 share 端口用ssh exe.dev share add polar-base email授权团队成员访问用ssh exe.dev cp polar-base pr-N克隆出预览 VM。关键设计镜像里不烘焙任何 Secret。polar-backend与polar-frontend两个 systemd 单元都通过ConditionPathExists门控各自的 env 文件详见下文因此刚从黄金镜像克隆出来的 VM 会处于空转idle状态直到第一次部署写入 env 文件后才真正启动服务。3.2 基线的夜间刷新Preview Base Refresh 工作流依赖与前端包会持续演进因此需要定期刷新黄金镜像。Preview Base Refresh工作流每天02:17 UTC自动刷新一次也可以通过workflow_dispatch在main分支上手动触发。它复用现有的POLAR_PREVIEW_SSH_KEYSecret 与POLAR_PREVIEW_BASE_VM变量默认值polar-base此时要求该 SSH 密钥在基线 VM 上拥有带免密 sudo 的 shell 访问权限。每次运行会执行检出当前origin/main并运行setup-base-vm.sh更新依赖、预构建资产、本地 Turbo 缓存、预览工具与.deployed_sha。刷新任务被串行化并对 VM 持有排他锁远程命令有75 分钟超时失败会由工作流上报。刷新是**原地in-place**进行的——刷新期间克隆出来的预览 VM 可能仍需自行完成依赖安装但已存在的预览 VM 不受影响。也可以手动刷新ssh polar-base.exe.xyz cd /srv/polar sudo git fetch origin main sudo git checkout -f origin/main sudo bash infra/preview/setup-base-vm.sh或者干脆推倒重建ssh exe.dev rm polar-base后重新执行构建命令。四、部署与销毁GitHub Workflow 驱动的完整生命周期4.1 触发条件preview.yml 在以下事件触发pull_request的opened/reopened/synchronize即 PR 打开、重新打开、推送新 commitpull_request_target的closedPR 关闭触发销毁workflow_dispatch手动触发可选择deploy或destroy动作并指定 PR 号。出于安全考虑Dependabot 的 PR 会被跳过注释说明 Dependabot PR 读取的是 Dependabot 自己的 Secret 存储预览 Secret 为空同时只对仓库内的 fork/分支生效。每个 PR 的部署使用concurrency串行化preview-pr-N分组且cancel-in-progress: false避免同一 PR 的多个 commit 并发部署互相干扰。4.2 部署的四步流程以 PR 打开或推送到新 commit 为例Workflow 依次执行供给预览资源运行uv run python -m scripts.preview create-or-update --pr-number ... --branch ... --sha ... --github-output ...见 preview.py创建模板复制的 Postgres 数据库与 Tinybird 分支并输出数据库名、用户名、密码、Tinybird token 等供后续步骤使用。从 preview.py 的常量可以看到资源命名约束预览 ID 最长 63 字符与 Postgres 标识符上限一致、模板复制失败最多重试 30 次每次间隔 2 秒、数据库删除重试 5 次间隔 1 秒。克隆 VM若缺失ssh exe.dev cp polar-base pr-N。得益于 exe.dev 的**写时复制copy-on-write**特性这一步约 1 秒即可完成克隆几乎不占额外磁盘。铸造一次性 Tailscale keyWorkflow 先用 OAuth secret 换取 access token再通过 Tailscale API 铸造一枚 **10 分钟 TTL、不可复用、临时ephemeral、预授权preauthorized**的 key并携带tag:preview标签。这样 OAuth secret 本身永远不会出现在 VM 上VM 被删除后临时节点会自动从 tailnet 消失。SSH 管道部署Workflow 把{pr_num, branch, sha, env_b64, ts_authkey, ts_tags}序列化为 JSON通过 stdin 管道给远端执行sudo /srv/preview-tools/deploy.sh随后从部署日志中解析Deployed at ...行作为preview_url输出。部署结束后Workflow 还会在 VM 本地轮询http://127.0.0.1:8000/v1/最多 60 次、每次间隔 5 秒直到返回{detail:Not Found}才认为 API 已就绪——由于预览 URL 只在 tailnet 内可达健康检查必须在 VM 内部进行。成功部署后preview_commentjob 会在 PR 上创建/更新一条标记为!-- __PREVIEW_ENV__ --的评论包含预览 URL、API 地址url/v1/、后端/前端日志链接与 SHA。4.3 销毁PR 关闭pull_request_target的closed时Workflow 执行两件事ssh exe.dev rm pr-N直接删除整台 VM这同时会带走 VM 上所有本地状态uv run python -m scripts.preview destroy --pr-number ... --branch ... --sha ...清理该 PR 的 Postgres 数据库与 Tinybird 分支。销毁步骤即使 VM 已不存在也会继续continue-on-error与if: always()确保共享资源一定被清理。五、deploy.sh 源码级解析一次部署在 VM 上做了什么deploy.sh 是整套系统的核心执行体。它从stdin 读取 JSON而非命令行参数原因在脚本注释里写得很明确避免分支名等字段造成 shell 注入。脚本开头会校验pr_num必须为纯数字、sha必须为十六进制非法输入直接退出。5.1 首次部署加入 tailnetsystemctl start tailscaled tailscale up --auth-key... --advertise-tagstag:preview --accept-routes --hostnamepr-N tailscale set --operatorcaddy对于tskey-client-*形式的 auth key脚本会自动补上?ephemeraltruepreauthorizedtrue参数确保临时节点生命周期与 VM 一致。随后tailscale set --operatorcaddy让 Caddy 能以非 root 身份从 tailscaled 获取证书。5.2 增量构建只重建发生变化的部分脚本以/srv/polar/.deployed_sha记录上次成功部署的 commit通过git diff --name-only prev sha计算变更文件集合实现按需构建变更范围执行动作^server/后端uv sync --frozen重装后端依赖^server/emails/或产物缺失uv run task emails构建邮件渲染器^server/polar/backoffice/或产物缺失uv run task backoffice构建后台静态资源clients/下的 lockfile / workspace / package.json 变化或node_modules缺失pnpm install --frozen-lockfile前端包总是执行pnpm exec turbo run build --filter./packages/*且设置POLAR_SKIP_DTS1值得注意的两个优化细节由于next dev只解析包的源码或 dist JS、从不解析类型声明deploy 跳过 tsup 的 DTS 生成步骤它占据构建时间的大头构建产物是 gitignore 的因此即使源码没变只要产物文件缺失也会触发重建。若分支已被合并或 commit 已被强推remote 上已不存在脚本会打印提示并安全退出跳过这次过期部署。5.3 写入 env 文件迁移前必须完成脚本向/srv/polar/server/.env写入后端配置核心变量包括POLAR_ENVdevelopment、POLAR_BASE_URL、POLAR_FRONTEND_BASE_URL指向预览 URLPOLAR_ALLOWED_HOSTS[preview-host]、POLAR_CORS_ORIGINS[https://...]POLAR_CHECKOUT_BASE_URL、两个会话 Cookie 域名POLAR_USER_SESSION_COOKIE_DOMAIN/POLAR_AUTHENTICATION_SESSION_COOKIE_DOMAINPOLAR_PREVIEW_API_PORT10000、Redis 指向本机127.0.0.1:6379POLAR_CURRENT_JWK_KIDpolar_preview、测试用POLAR_TURNSTILE_SECRET、Tinybird 读写开关。Workflow 通过env_b64base64 编码的 JSON传入的 Stripe 密钥、Postgres 连接信息、Tinybird token 等会被追加到该文件末尾。前端 env 写入clients/apps/web/.env.localNEXT_PUBLIC_API_URL、POLAR_API_URLhttp://127.0.0.1:10000等以及clients/.env.previewPORT3000。5.4 JWKS、迁移与种子数据JWKS 按 VM 生成克隆出来的 VM 绝不能共享黄金镜像的签名密钥。脚本检查.jwks.json是否存在、或.jwks.host记录的主机名是否与当前主机一致不匹配则执行uv run python -m polar.kit.jwk polar_preview重新生成并记录主机名。迁移uv run alembic upgrade head把数据库 schema 升到最新。两阶段种子数据先同步执行uv run task seeds_load --phase simple加载就绪关键数据否则服务无法正常提供核心功能再通过systemctl restart --no-block polar-seed-simple-complement异步启动simple-complement补充种子的后台任务加载剩余演示数据与分析数据。重启服务systemctl restart polar-backend polar-frontend。5.5 种子任务的并发锁deploy.sh在部署开头会对/run/lock/polar-seed-simple-complement.lock执行flock --timeout 900防止同一 VM 上并发部署时两个补充种子任务互相踩踏获取锁失败15 分钟未完成则部署失败退出。补充种子单元 polar-seed-simple-complement.service 自身也通过flock --exclusive --no-fork使用同一把锁并设置了TimeoutStartSec15min、Restarton-failure间隔 15 秒、StartLimitBurst4的重试上限。六、反向代理与 TLSCaddyfile 的路由设计Caddyfile 用命名 site block(polar)定义了一套路由规则同时被:8000与:443两个监听端口导入路径后端说明/v1/*127.0.0.1:10000API 路由/backoffice、/backoffice/*127.0.0.1:10000后台管理界面/healthz、/openapi.json127.0.0.1:10000健康检查与 OpenAPI 文档/_logs/*127.0.0.1:9990日志查看器其余所有路径127.0.0.1:3000Next.js 前端:443上通过tls { get_certificate tailscale }从 tailscaled 获取 MagicDNS 证书。这套路由设计保证了即使next dev的 3000 端口只监听在127.0.0.1所有外部流量依然通过 Caddy 统一入口进入且日志、健康检查等基础设施路径与业务路径互不干扰。七、systemd 服务单元与进程模型四个 systemd 单元全部由 setup-base-vm.sh 安装并各自设定了资源上限polar-backend.service以ConditionPathExists/srv/polar/server/.env门控——env 文件不存在即尚未首次部署时不启动。ExecStart指向 run-preview-backend.sh该脚本在一个进程内同时拉起 dramatiq worker-p 1 -t 1消费三个队列并加载polar.worker.scheduler:start调度器与uvicorn polar.app:app --host 127.0.0.1 --port 10000 --workers 1任一进程退出即整体重启。资源上限MemoryMax2G、CPUQuota200%Restarton-failure、RestartSec5。polar-frontend.service以ConditionPathExists/srv/polar/clients/.env.preview门控ExecStart为npx next dev --turbopack --hostname 127.0.0.1 --port ${PORT}端口来自clients/.env.preview的PORT3000资源上限MemoryMax5G、CPUQuota200%。polar-logs.service运行 log-viewer.py在127.0.0.1:9990起一个极简 HTTP 服务把/_logs/backend、/_logs/frontend、/_logs/seed分别映射到对应 systemd 单元用journalctl -u unit -n 500 --no-pager -o short-iso拉取最近 500 行日志返回。polar-seed-simple-complement.serviceoneshot 单元执行uv run task seeds_load --phase simple-complement。八、访问与调试给开发者的实操手册预览环境只对 tailnet 成员开放日常使用包括访问预览https://pr-N.tailnet.ts.net。对应的pr-N.exe.xyzURL 保持账号私有不会对外暴露。查看日志https://pr-N.tailnet.ts.net/_logs/backend以及/_logs/frontend、/_logs/seed。登录验证码OTP code也会出现在后端日志里以LOGIN CODE标记——测试登录流程时可直接从这里取码。进入 Shellssh pr-N.exe.xyz。无论传入什么用户名都会以exedev用户登录且拥有免密 sudo。端口约定9999被 exe.dev 虚拟机上的 Shelley 服务占用因此日志查看器使用9990端口。九、小结这套方案的工程取舍回看整个设计Polar 预览环境有几个值得借鉴的工程决策克隆代替构建cp polar-base pr-N秒级完成 VM 复制写时复制让数十个预览共存几乎零额外成本依赖与构建产物在黄金镜像中预热部署只做增量。安全边界清晰tailnet-only 的访问模型 每 VM 一次性的临时 Tailscale key 不烘焙 Secret 的镜像 JWKS 按主机生成让预览环境既可用又不泄漏。就绪分层迁移 → 就绪关键种子 → 服务重启 → 异步补充种子配合锁文件串行化保证了能访问时核心功能一定可用。可观测性内建日志查看器、健康检查、CI 轮询就绪让部署结果可验证、可诊断。如果你需要为类似的全栈仓库设计 PR 级预览环境本文涉及的文件——setup-base-vm.sh、deploy.sh、Caddyfile、preview.yml 与 preview.py——本身就是一套完整、可直接参考的参考实现。【免费下载链接】polarPolar — A billing platform for the intelligence era项目地址: https://gitcode.com/GitHub_Trending/po/polar创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考