Wasp 应用自托管部署到 Caprover 完整指南:GitHub Actions + GHCR 全自动 CI/CD
【免费下载链接】waspThe batteries-included full-stack framework for the AI era. Develop JS/TS web apps (React, Node.js, and Prisma) using declarative code that abstracts away complex full-stack features like auth, background jobs, RPC, email sending, end-to-end type safety, single-command deployment, and more.项目地址: https://gitcode.com/GitHub_Trending/wa/wasp
本指南基于 Wasp 开源仓库官方文档 web/docs/guides/deployment/self-hosted/caprover.md,详细讲解如何将一个 Wasp 全栈应用(React 客户端 + Node.js 服务端 + PostgreSQL 数据库)部署到自托管的 Caprover PaaS 平台。读完本文,你将掌握:在 Caprover 中创建数据库、服务端、客户端三个应用,配置域名与 HTTPS,以及通过 GitHub Actions 在推送main分支时自动构建 Docker 镜像、推送到 GitHub Container Registry(GHCR)并触发 Caprover 滚动部署的完整实战流程。
版本说明:本指南经 Wasp 0.24 与 Caprover 1.14.1 验证(见文档头部
<LastCheckedWithVersionsNotice>标记),文中的命令与配置适用于这两个版本及其相邻版本。
为什么 Wasp 应用要拆成三部分部署
Wasp 是"batteries-included"的全栈框架,一个main.wasp.ts声明文件同时描述客户端(React + Vite)、服务端(Node.js + Express)和数据模型(Prisma + PostgreSQL)。当你在本地运行wasp build时,生成器会把这三部分分别输出到.wasp/out目录下的web-app/、server/和db/等子目录中(可参考 CLI 文档 对wasp build的描述:"generates the complete web app code, which is ready for deployment. The generated code is stored in the.wasp/outfolder")。
正因为构建产物天然分离,生产部署也遵循同样的"三件套"模式:
- 客户端:
wasp build输出的web-app经过 Vite 构建后是一堆纯静态文件(HTML/JS/CSS),需要一个静态文件服务器来托管; - 服务端:
.wasp/out/server是一个可独立运行的 Node.js 服务,对外提供 REST API、WebSocket 和认证逻辑; - 数据库:Wasp 服务端依赖 PostgreSQL(通过
DATABASE_URL连接),由 Prisma 管理 schema 与迁移。
Caprover 恰好以"应用 + 容器"为基本单位,三者可以各自成为一个 Caprover 应用,互不干扰、独立伸缩,这正是本文方案的架构基础。
部署总览
将 Wasp 应用部署到 Caprover 的核心链路如下:
- 在 Caprover 中创建三个应用:数据库(
myapp-db)、服务端(myapp-server)、客户端(myapp-client); - 使用 GitHub Actions 在 CI 中执行
wasp build,分别构建服务端与客户端的 Docker 镜像; - 将两个镜像推送到 GitHub Container Registry(GHCR);
- 通过 Caprover 官方 Action 触发 Caprover 拉取并部署新镜像。
下面按照原文档的步骤顺序逐一展开。
前置条件
开始之前,请确认你已具备以下资源:
- 一台安装了 Caprover 的服务器:Caprover 是一个自托管的 PaaS(Platform as a Service),负责管理你的应用部署。安装方式参考 Caprover 官方快速开始文档;
- 一个域名:用于对外提供服务的入口;
- 一个托管着 Wasp 应用代码的 GitHub 仓库:CI/CD 的触发源与镜像仓库载体。
:::tip 快速测试路径 如果你在安装 Caprover 时配置了*.apps通配子域名,可以直接使用https://myapp-client.apps.mydomain.com和https://myapp-server.apps.mydomain.com这类地址做快速联调,不必先解析两条 DNS A 记录。 :::
Step 1:设置域名解析
在域名服务商处添加两条 DNS A 记录,将流量指向你的服务器 IP:
| 记录 | 指向 | 用途 |
|---|---|---|
@(根域名) | 服务器 IP | 客户端入口,如myapp.com |
api(子域名) | 服务器 IP | 服务端入口,如api.myapp.com |
注意保持与后续 Caprover 应用 HTTP 设置中的域名一致,DNS 生效后才能顺利签发 HTTPS 证书(Caprover 使用 Let's Encrypt 自动颁发)。
Step 2:在 Caprover 中创建三个应用
2.1 创建数据库(PostgreSQL)
- 进入 Caprover 面板,打开One-Click Apps,选择PostgreSQL;
- 将应用命名为
myapp-db; - 版本选择
18(或当前最新版本); - 点击部署,等待数据库容器就绪;
- 记下数据库连接串,格式形如:
postgresql://postgres:<password>@srv-captain--myapp-db:5432/postgres其中
srv-captain--myapp-db是 Caprover 内部网络中该应用的固定主机名,服务端应用必须通过这个内部地址访问数据库,而不是公网域名——这样既快又安全(数据库不对外暴露端口)。
2.2 创建服务端应用myapp-server
- 新建应用,命名为
myapp-server; - 进入HTTP Settings:
- Connect domain:
https://api.<your-domain> - 点击Enable HTTPS自动签发证书
- Container HTTP Port:
3001 - 开启Force HTTPS与Websocket Support
- Connect domain:
- 点击Save & Restart。
端口3001是 Wasp 服务端的默认监听端口:开发环境下 Wasp 默认在 3001 启动服务器,生产构建的服务端镜像也通过PORT环境变量读取监听端口(见下文 Step 3 的环境变量表)。开启Websocket Support是因为 Wasp 的实时功能(如 WebSocket 订阅、部分认证流程)依赖 WebSocket 长连接。
2.3 创建客户端应用myapp-client
- 新建应用,命名为
myapp-client; - 进入HTTP Settings:
- Connect domain:
https://<your-domain> - 点击Enable HTTPS
- Container HTTP Port:
8043 - 开启Force HTTPS与Websocket Support
- Connect domain:
- 点击Save & Restart。
客户端镜像基于静态文件服务器pierrezemb/gostatic构建,该镜像默认监听 8043 端口,因此容器 HTTP 端口要填8043。WebSocket Support 同样建议开启,避免反向代理中断客户端与服务器之间的实时连接。
Step 3:配置服务端环境变量
在 Caprover 中打开myapp-server应用,进入App Configs > Environment Variables,添加以下变量:
| 变量 | 值 | 作用 |
|---|---|---|
DATABASE_URL | postgresql://postgres:<password>@srv-captain--myapp-db:5432/postgres | PostgreSQL 连接串,Wasp 服务端与 Prisma 迁移都依赖它 |
JWT_SECRET | 至少 32 字符的随机字符串 | 用于生成安全的 JWT token;开发环境默认值为DEVJWTSECRET,生产环境必须显式设置 |
PORT | 3001 | 服务端监听端口,必须与 Caprover 中设置的 Container HTTP Port 一致 |
WASP_WEB_CLIENT_URL | https://<your-domain> | 客户端对外 URL;服务端用它生成邮件中的应用链接等 |
WASP_SERVER_URL | https://api.<your-domain> | 服务端对外 URL;OAuth 登录(如 Google、GitHub)回调时依赖它做重定向 |
以上变量的具体语义可查阅 环境变量文档 中"Server General Configuration"一节:DATABASE_URL是必填的数据库地址;WASP_WEB_CLIENT_URL与WASP_SERVER_URL是生产环境必填的(开发环境由 Wasp 自动填充,生产环境缺失会导致服务端启动失败);JWT_SECRET需要至少 32 个字符的随机串。
另外,把.env.server中应用需要的其他环境变量一并添加进来,例如:
- 邮件服务商相关:
SMTP_HOST、SMTP_PORT、SMTP_USERNAME、SMTP_PASSWORD(SMTP 发送器)、SENDGRID_API_KEY、MAILGUN_API_KEY、RESEND_API_KEY等; - OAuth 相关:
GOOGLE_CLIENT_ID、GOOGLE_CLIENT_SECRET、GITHUB_CLIENT_ID、GITHUB_CLIENT_SECRET等(命名规则为<PROVIDER_NAME>_CLIENT_ID/<PROVIDER_NAME>_CLIENT_SECRET); - 你自定义的任意服务端变量(如 Stripe 密钥)。
:::caution 客户端环境变量的区别 与服务端不同,客户端环境变量(REACT_APP_*前缀)是在构建期注入到静态文件中的(Vite 构建时会替换import.meta.env.REACT_APP_XXX),因此在 Caprover 面板里给客户端应用设置环境变量是无效的——它们必须在 GitHub Actions 构建时通过命令行提供,详见 部署期环境变量文档。客户端变量不能存放任何密钥,因为它们对浏览器公开可见。 :::
Step 4:配置 GitHub Container Registry 访问
为了让 Caprover 能拉取 GHCR 上的私有镜像,需要在 Caprover 中登记远端镜像仓库:
- 进入 Caprover 的Cluster页面;
- 添加新的Remote Registry:
- Username:你的 GitHub 用户名
- Password:你的 GitHub personal access token(需具备
write:packages权限) - Domain:
ghcr.io - Image Prefix:你的 GitHub 用户名
这样 Caprover 在部署ghcr.io/<username>/myapp-server这类镜像时就能通过认证拉取。
Step 5:编写 GitHub Actions 工作流
在你的仓库中创建.github/workflows/deploy.yml,内容如下(完整保留原文档配置):
name: "Deploy" on: push: branches: - "main" concurrency: group: deployment cancel-in-progress: true env: WASP_VERSION: "{pinnedLatestWaspVersion}" SERVER_APP_NAME: "myapp-server" SERVER_APP_URL: "https://api.myapp.com" CLIENT_APP_NAME: "myapp-client" DOCKER_REGISTRY: "ghcr.io" DOCKER_REGISTRY_USERNAME: ${{ github.repository_owner }} DOCKER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} jobs: build-and-push-images: permissions: contents: read packages: write runs-on: ubuntu-latest # Remove this block if your app is NOT in an 'app' folder defaults: run: working-directory: ./app steps: - name: Checkout repository uses: actions/checkout@v4 - name: Log in to Container registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ env.DOCKER_REGISTRY_USERNAME }} password: ${{ env.DOCKER_REGISTRY_PASSWORD }} - name: (server) Extract metadata for Docker id: meta-server uses: docker/metadata-action@v5 with: images: ${{ env.DOCKER_REGISTRY }}/${{ env.DOCKER_REGISTRY_USERNAME }}/${{ env.SERVER_APP_NAME }} - name: (client) Extract metadata for Docker id: meta-client uses: docker/metadata-action@v5 with: images: ${{ env.DOCKER_REGISTRY }}/${{ env.DOCKER_REGISTRY_USERNAME }}/${{ env.CLIENT_APP_NAME }} - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: "{minimumNodeJsVersion}" - name: Install Wasp shell: bash run: npm i -g @wasp.sh/wasp-cli@${{ env.WASP_VERSION }} - name: Install Wasp app dependencies run: wasp install - name: Build Wasp app run: wasp build - name: (client) Build run: REACT_APP_API_URL=${{ env.SERVER_APP_URL }} npx vite build - name: (client) Prepare Dockerfile run: | cd ./.wasp/out/web-app echo "FROM pierrezemb/gostatic" > Dockerfile echo "CMD [\"-fallback\", \"200.html\", \"-enable-logging\"]" >> Dockerfile echo "COPY ./build /srv/http" >> Dockerfile - name: (server) Build and push Docker image uses: docker/build-push-action@v6 with: # Remove 'app/' if your app is at the repo root context: ./app/.wasp/out file: ./app/.wasp/out/Dockerfile push: true tags: ${{ steps.meta-server.outputs.tags }} labels: ${{ steps.meta-server.outputs.labels }} - name: (client) Build and push Docker image uses: docker/build-push-action@v6 with: # Remove 'app/' if your app is at the repo root context: ./app/.wasp/out/web-app file: ./app/.wasp/out/web-app/Dockerfile push: true tags: ${{ steps.meta-client.outputs.tags }} labels: ${{ steps.meta-client.outputs.labels }} - name: (server) Deploy to Caprover uses: caprover/deploy-from-github@v1.1.2 with: server: ${{ secrets.CAPROVER_SERVER }} app: ${{ env.SERVER_APP_NAME }} token: ${{ secrets.SERVER_APP_TOKEN }} image: ${{ steps.meta-server.outputs.tags }} - name: (client) Deploy to Caprover uses: caprover/deploy-from-github@v1.1.2 with: server: ${{ secrets.CAPROVER_SERVER }} app: ${{ env.CLIENT_APP_NAME }} token: ${{ secrets.CLIENT_APP_TOKEN }} image: ${{ steps.meta-client.outputs.tags }}工作流逐段拆解
WASP_VERSION:请替换为当前固定的 Wasp CLI 版本号(如0.24.x)。CI 中执行npm i -g @wasp.sh/wasp-cli@${{ env.WASP_VERSION }}全局安装指定版本的 CLI,避免构建结果因 CLI 版本漂移而不可复现;node-version:替换为 Wasp 要求的最低 Node.js 版本;concurrency段:group: deployment保证同一时间只有一个部署任务在跑,cancel-in-progress: true允许新推送中断旧的进行中任务;- 登录 GHCR:
docker/login-action@v3使用 GitHub 自动提供的GITHUB_TOKEN完成认证(工作流顶层已声明permissions: packages: write,这是推包所必需的权限); wasp install/wasp build:分别安装依赖并生成.wasp/out完整构建产物。wasp build的产物结构见 CLI 文档:生成代码存放在.wasp/out目录,其中web-app是 Vite 客户端工程、server是服务端工程;- 客户端构建:
REACT_APP_API_URL=${{ env.SERVER_APP_URL }} npx vite build在构建时把服务端地址注入客户端代码(REACT_APP_API_URL是 Wasp 客户端连接服务端所用的必填变量,见 环境变量文档)。这一步必须在wasp build之后、位于web-app目录内执行; - 客户端 Dockerfile:
wasp build不会为客户端生成 Dockerfile,所以工作流用三条echo命令现场拼装一个基于pierrezemb/gostatic的极简静态托管镜像,通过-fallback 200.html实现 SPA 前端路由回退(任何未知路径都返回200.html,交给 React Router 处理),并把build目录复制到镜像的/srv/http; - 服务端镜像:直接使用
wasp build在.wasp/out/Dockerfile中生成的官方 Dockerfile 构建,docker/build-push-action@v6负责构建并推送到 GHCR; - 部署触发:
caprover/deploy-from-github@v1.1.2分别以各自的 app token 通知 Caprover 拉取服务端与客户端的新镜像并滚动更新。
Wasp 生成的服务端 Dockerfile 原理
服务端镜像的质量取决于 Wasp 生成器产出的 Dockerfile。在仓库中可找到该模板 waspc/data/Generator/templates/Dockerfile,它采用标准的多阶段构建:
node/base阶段:固定使用node:{版本}-alpine3.23基础镜像(模板注释说明固定 alpine 小版本是为了避免本地与 GitHub CI 构建出不同的环境),并安装openssl——Prisma 原生引擎在构建和运行时都需要 libssl;server-builder阶段:安装编译工具链(python3 build-base libtool autoconf automake,用于 Apple Silicon 上编译原生依赖),将.wasp/out/server、.wasp/out/sdk等拷贝进镜像并执行npm install、prisma generate与npm run bundle,产出打包后的服务端 bundle;server-production阶段:从 builder 阶段仅拷贝运行所需的node_modules、bundle 与 Prisma 文件,设置NODE_ENV=production,EXPOSE ${PORT},最终以npm run start-production作为入口启动。
也就是说,你在 CI 里对.wasp/out目录执行的docker build,其实是在构建一个已经过 Prisma 生成、依赖精简、生产就绪的 Node 服务镜像。这一环节无需手工编写 Dockerfile,wasp build已经替你做好了。
Step 6:配置 GitHub Secrets
在 GitHub 仓库的Settings > Secrets and variables > Actions中新增以下三个 Secret:
CAPROVER_SERVER
你的 Caprover 控制面板地址,例如https://captain.apps.mydomain.com。它是caprover/deploy-from-githubAction 连接 Caprover 的目标服务器。
SERVER_APP_TOKEN与CLIENT_APP_TOKEN
两个应用的部署令牌获取方式相同(分别对myapp-server和myapp-client操作一次):
- 在 Caprover 中打开对应应用;
- 进入Deployment标签,找到Method 1: Official CLI;
- 点击Enable App Token;
- 复制生成的 token,分别保存为
SERVER_APP_TOKEN与CLIENT_APP_TOKEN。
这两个令牌是独立的,分别控制服务端与客户端应用的部署权限,即使泄露也只会影响单个应用,便于按应用隔离与回收。
Step 7:部署与验证
完成以上全部配置后,向main分支推送代码,GitHub Actions 将自动执行:
wasp install+wasp build构建 Wasp 应用;- 分别构建服务端与客户端的 Docker 镜像;
- 将两个镜像推送到 GitHub Container Registry;
- 通过 Caprover 部署 Action 将新镜像滚动部署到
myapp-server与myapp-client。
首次部署成功后,建议按以下清单验证:
- 打开
https://<your-domain>,确认客户端页面正常加载,且浏览器 Network 面板中的 API 请求指向https://api.<your-domain>; - 在
myapp-server的App Logs中检查服务端是否成功连接数据库、是否打印启动日志; - 测试一个涉及认证的流程(注册/登录),验证
JWT_SECRET与WASP_SERVER_URL是否正确——OAuth 回调失败通常是WASP_SERVER_URL配置错误所致; - 若页面路由刷新后 404,说明客户端镜像的
-fallback 200.html参数缺失或 Dockerfile 未生效; - 若浏览器报 CORS 错误,确认
WASP_WEB_CLIENT_URL与客户端实际访问域名完全一致(含https://前缀),Wasp 默认只允许来自该域的跨域请求,多域场景可参考 CORS 多域名配置指南。
常见问题排查
| 现象 | 可能原因与对策 |
|---|---|
| 服务端启动失败 | 检查DATABASE_URL、JWT_SECRET、PORT、WASP_WEB_CLIENT_URL、WASP_SERVER_URL五个必填变量是否都已设置(详见 部署环境变量文档) |
| CI 构建报错 | 确认WASP_VERSION与node-version两个占位符已替换为实际版本;确认应用在app/子目录时保留了working-directory: ./app与路径前缀,否则应移除 |
| 镜像推不上去 | 检查工作流permissions是否包含packages: write;GHCR 要求镜像名小写 |
| Caprover 拉不到镜像 | 回到 Step 4 检查 Remote Registry 的凭据与 Image Prefix 是否正确 |
| 客户端白屏/接口 404 | 确认构建时REACT_APP_API_URL已注入(可通过查看浏览器加载的 JS 中是否包含服务器地址验证) |
| 部署成功但服务未更新 | 检查两个应用的 App Token 是否都已启用并正确填入 Secrets |
与其他自托管方案的对比
Caprover 并不是唯一选择:Wasp 官方还提供了 Coolify 部署指南 与 VPS 手动部署指南。三者共享相同的 Wasp 构建产物与"客户端静态托管 + 服务端容器"模型,区别主要在于触发方式:本文的 Caprover 方案使用caprover/deploy-from-githubAction 直连部署;Coolify 方案通过 Deploy Webhook 触发;而纯 VPS 方案则需要自行处理 Nginx 反代、进程守护与镜像更新。如果你的团队已经熟悉 Caprover 的界面与 API token 体系,本文方案是维护成本较低的一条路径。
小结
至此,你已经拥有了一条完整的 Wasp → Caprover 生产部署流水线:DNS 与 HTTPS 就绪、PostgreSQL 数据库就绪、服务端与客户端应用就绪、环境变量就绪、CI/CD 就绪。此后每次推送main分支,代码都会经过构建、打包、推送、部署四步自动上线。配合 Wasp 的单命令本地开发体验,这套方案可以让小团队以极低的运维成本长期运行自托管应用。文中涉及的所有仓库文档与源码均可在当前仓库中继续深入查阅:构建命令细节见 CLI 文档,环境变量全集见 环境变量文档,服务端镜像模板见 waspc/data/Generator/templates/Dockerfile。
【免费下载链接】waspThe batteries-included full-stack framework for the AI era. Develop JS/TS web apps (React, Node.js, and Prisma) using declarative code that abstracts away complex full-stack features like auth, background jobs, RPC, email sending, end-to-end type safety, single-command deployment, and more.项目地址: https://gitcode.com/GitHub_Trending/wa/wasp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考