上篇发出后,有朋友私信问这玩意自己能不能搭一套试试。我说能啊,而且从服务器到数据库到域名,这一整套基础设施其实都能白嫖——阿里云新人有免费 ECS,Supabase 免费额度够撑个人项目,Cloudflare Tunnel 和 HTTPS 入口也都是免费的。这篇就从头到尾走一遍,配关键截图。

1. 第一步:阿里云 ECS,白嫖 2 个月 2C2G

阿里云对新用户有一个”免费试用中心”,其中最实在的就是云服务器 ECS:2 vCPU、2 GB 内存、40 GB 云盘、3 Mbps 带宽,免费试用 2 个月。

操作路径:

  1. 打开阿里云免费试用页面(free.aliyun.com),搜索”云服务器 ECS”
  2. 香港节点——这个重要,因为 we-events 用 Playwright 抓公众号,网络连通性需要绕开一些限制,香港节点最稳
  3. 系统镜像选 Ubuntu 22.04,系统盘 40 GB 够用
  4. 安全组临时放行 22(SSH)、38001(后端 API)、30000(前端) 端口用于调试——Cloudflare Tunnel 跑通后,正式环境建议只保留 22,并限制来源 IP
  5. 领取后实例几分钟就就绪,记下公网 IP

进服务器第一件事:装 Docker,没有它后面什么也跑不了。

1
2
3
4
5
6
7
8
9
10
# 用你熟悉的 SSH 客户端连上去
ssh root@<你的公网IP>

# 安装 Docker(Ubuntu 一键脚本)
curl -fsSL https://get.docker.com | sh
systemctl enable --now docker

# 验证
docker --version
docker compose version

装好之后把这台机器当成你的”根据地”——代码、数据库客户端、反向代理全跑在上面。2C2G 对 we-events 绰绰有余,毕竟后端就一个 FastAPI 进程加一个 Playwright 浏览器实例。

2. 第二步:Supabase,白嫖一个 PostgreSQL

we-events 的数据层全在 PostgreSQL 上——公众号列表、文章内容、采集队列、活动记录,还有图片对象存储。Supabase 的免费额度给得很慷慨:

  • 500 MB PostgreSQL 数据库
  • 1 GB 文件存储(够存几万张文章配图)
  • 每月 50 GB 带宽
  • 内置 Auth、Row Level Security

2.1. 创建项目

  1. 注册/登录 Supabase(supabase.com),GitHub 账号直接进
  2. Dashboard → New project → 取个名字(比如 we-events
  3. 数据库密码:自动生成一个,存好(后面要用)
  4. Region 选靠近用户的区域——香港或新加坡
  5. 等几分钟初始化完成

2.2. 拿到连接信息

项目建好后,在 Project Settings → API 里找到三个关键值:

  • Project URLhttps://<project-ref>.supabase.co
  • anon public key
  • service_role key(管理级权限,存好别泄露)

这三个值要填到 we-events 的配置里。

2.3. 初始化数据库

项目 supabase/ 目录下有完整的迁移脚本。连到 Supabase 的 SQL Editor,按顺序执行:

  1. supabase/migrations/20241120_initial_schema.sql——建表
  2. supabase/migrations/20241120_rls_policies.sql——行级安全策略
  3. supabase/config_managements_seed.sql——运行时配置种子数据

也可以用 psql 客户端从服务器直连执行:

1
2
3
4
5
# 从 ECS 连 Supabase 的 PostgreSQL
psql "postgresql://postgres:<密码>@db.<project-ref>.supabase.co:5432/postgres" \
-f 20241120_initial_schema.sql
psql ... -f 20241120_rls_policies.sql
psql ... -f config_managements_seed.sql

SQL 跑完后还需要在 Storage 里创建两个存储桶:qr(微信登录二维码)和 article-images(文章配图)。Bucket 权限按项目 README 配:qr 私密、article-images 公开。

3. 第三步:Cloudflare,白嫖 HTTPS 入口 + 反向隧道

这个不是必须的。这里主要是不花钱搞定公网入口:ECS 上仍然用 Docker Compose 跑前端和后端,Cloudflare Tunnel 只负责把前端 nginx 暴露成 HTTPS 域名。

3.1. 前端入口:Docker nginx

we-events 的前端是一个 React + Vite 静态应用,但当前项目没有走 Cloudflare Pages,而是由 frontend/Dockerfile 构建静态资源,再交给 nginx 对外提供服务。这样有一个好处:前端和后端天然同域,浏览器请求 /api/ 时,nginx 会直接转发到 Docker 网络里的后端容器。

关键配置在 frontend/nginx.conf

1
2
3
4
5
6
7
8
9
10
11
server {
listen 30000;

location / {
try_files $uri $uri/ /index.html;
}

location /api/ {
proxy_pass http://backend:38001;
}
}

前端代码里 API baseURL 固定为 /api/v1,所以线上不需要再额外配置 VITE_API_BASE。只要用户访问的是前端域名,比如 https://event.phoenine.top,登录请求实际会走:

1
https://event.phoenine.top/api/v1/wx/auth/token

这也是我没有把前端单独丢到 Cloudflare Pages 的原因:Pages 上没有这个 nginx 反代,除非额外改前端请求地址或加 Functions/Worker 代理,否则 /api/ 会找不到后端。

3.2. 公网入口:Cloudflare Tunnel

ECS 的公网 IP 暴露端口不安全,而且 3 Mbps 带宽也吃力。Cloudflare Tunnel(cloudflared)可以把你的服务安全地暴露出去,不走公网 IP,还能白嫖 Cloudflare 的 CDN 和 SSL。

在 ECS 上装 cloudflared 并创建隧道:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 安装 cloudflared
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared
chmod +x /usr/local/bin/cloudflared

# 认证
cloudflared tunnel login

# 创建隧道
cloudflared tunnel create we-events

# 配置隧道(编辑 ~/.cloudflared/config.yml)
tunnel: we-events
credentials-file: /root/.cloudflared/<uuid>.json

ingress:
- hostname: event.phoenine.top
service: http://localhost:30000
- service: http_status:404

# DNS 记录
cloudflared tunnel route dns we-events event.phoenine.top

# 启动(建议做成 systemd 服务)
cloudflared tunnel run we-events

这样 event.phoenine.top 就指向 ECS 上的前端 nginx:页面访问走 localhost:30000,接口请求再由 nginx 转到 Docker 网络里的 backend:38001。公网只看到一个 HTTPS 域名,后端端口不需要直接暴露给用户。

3.3. 踩坑:被 Google 当成欺诈网站

全部配好之后,访问自定义域名 https://event.phoenine.top/login,Chrome 直接弹了一个大红屏——“Deceptive site ahead”,页面被整页遮蔽,点”详细信息”才能继续访问。这个不是 Cloudflare 本身的错误,也不是证书坏了,而是 Chrome 走 Google Safe Browsing 把这个登录页判成了危险网站。

先按基础链路排了一遍:

1
2
3
4
curl -I https://event.phoenine.top
curl -I http://event.phoenine.top
curl -L -I https://event.phoenine.top
curl -L https://event.phoenine.top | head -n 80

结果很正常:HTTP 能跳到 HTTPS,HTTPS 返回 200,没有异常重定向,也没有跳到奇怪的第三方域名。继续把首页引用的 JS 文件拉下来搜了一遍。注意文件名里的 hash 每次构建都可能变,先从首页 HTML 里找到实际的 /assets/index-xxx.js

1
2
3
4
ASSET=$(curl -L https://event.phoenine.top | grep -Eo '/assets/index-[^"]+\.js' | head -n 1)
curl -L "https://event.phoenine.top${ASSET}" -o /tmp/event-index.js
grep -Eo 'https?://[^"'\''`]+' /tmp/event-index.js | sort -u
grep -Ei 'location|href|redirect|login|auth|token|password|iframe|eval|document\.write' /tmp/event-index.js | head -n 80

这一步主要确认三件事:有没有陌生外链、有没有 iframe / eval 这种高风险脚本、登录接口有没有跨域跳转。结果也没发现恶意注入,登录接口就是正常的:

1
POST /api/v1/wx/auth/token

最后去 Google Search Console 看 Security issues,命中的 URL 很明确,就是:

1
https://event.phoenine.top/login

类型是 deceptive content / social engineering。也就是说,Google 不是说整套 Cloudflare 部署有问题,而是这个登录页的特征像钓鱼页。

我判断触发原因主要是这几个因素叠在一起:

  • 页面路径是 /login
  • 页面里有用户名、密码、登录按钮
  • 项目和文案里出现了”公众号”、”扫码登录”、”当前环境异常”、”完成验证后即可继续访问”这类词
  • 域名部署时间短,站点信誉还没积累起来

we-events 当然不是钓鱼站,但机器审核不会理解业务上下文。它看到”登录表单 + 微信/公众号/验证相关词 + 新站点”,就很容易往欺诈网站上靠。

处理上我做了几件事:

  1. 把登录页文案改得更中性,少出现”微信”、”公众号”、”扫码登录”这类容易误伤的词
  2. 确认页面 title、logo、favicon 没有仿冒微信公众平台的表达
  3. 给站点加 robots.txt,当前项目里是 Disallow: /,避免搜索引擎收录后台入口
  4. 补安全响应头,比如 X-Content-Type-OptionsX-Frame-OptionsReferrer-Policy,有条件的话再加 HSTS
  5. 如果后台只是自己用,最好再加 IP 白名单或 Cloudflare Access,不要把 /login 裸露在公网

Search Console 复审流程如下:

  1. 打开 search.google.com/search-console

  2. 添加资源,推荐直接用 Domain 资源:phoenine.top,这样 event.phoenine.top 也会覆盖进去

  3. Google 会给一条 TXT 记录,在 Cloudflare DNS 里添加:

    1
    2
    3
    Type: TXT
    Name: @
    Content: google-site-verification=xxxxxx
  4. 回 Search Console 点 Verify

  5. 进入 Security & Manual ActionsSecurity issues

  6. 如果看到 /login 被标记,先确认上面的文案和配置已经改完,再点 Request Review

复审说明可以这么写:

1
2
3
4
5
6
7
8
9
10
11
This website is an internal management system for our own operations.

The login page was incorrectly flagged as deceptive due to generic login form patterns and wording related to account authentication.

We have reviewed the website thoroughly:
- No phishing content exists
- No malware exists
- No credential harvesting exists
- Login page wording and metadata have been updated to avoid misleading signals

Please re-review the flagged URL.

我的结论是:这次不是 Cloudflare Tunnel、DNS 或 HTTPS 配置错误,而是 Google Safe Browsing 对登录页语义的误判。自定义域名也不能完全避免这个问题,只是老域名会比全新的免费子域名或新子域名更不容易被误伤。后台类项目上线后,最好第一时间把域名加到 Search Console,并且不要让登录页看起来像某个平台的仿冒入口。

4. 第四步:部署 we-events

服务器和数据库都有了,开始部署应用本身。

4.1. 拉代码

1
2
3
cd /opt
git clone https://github.com/phoenine/we-events.git
cd we-events

4.2. 配环境变量

从模板创建线上 Supabase 的配置:

1
cp .env.online.example .env.online

编辑 docker-compose 使用的环境文件,把前面从 Supabase 拿到的三个值填进去,SSH 登录密码也设好:

1
2
3
4
5
USERNAME=admin@your-domain.com
PASSWORD=换成你自己的密码
SUPABASE_URL=https://<project-ref>.supabase.co
SUPABASE_ANON_KEY=你的 anon key
SUPABASE_SERVICE_KEY=你的 service_role key

4.3. 启动

1
docker compose --env-file .env.online up --build -d

等几十秒,两个容器跑起来之后:

1
2
后端:http://localhost:38001
前端:http://localhost:30000

通过 Cloudflare Tunnel 的话,访问 https://event.phoenine.top 就能看到登录页了。

4.4. 验证

  1. 浏览器打开前端地址,用上面配置的 USERNAME / PASSWORD 登录
  2. 进”公众号管理”添加几个想跟踪的公众号
  3. 在”系统”页扫微信登录二维码,建立抓取会话
  4. 触发采集,等文章入库

测试账号不用我给了——你自己的部署,管理员就是你自己。

5. 给开发者的信息

如果你对技术细节感兴趣,这里列几个部署时值得注意的点:

  • Playwright 浏览器:后端 Docker 镜像里内置了 Chromium(playwright install chromium),首次启动可能多耗十几秒。镜像约 1.2 GB,ECS 的 40 GB 云盘够用
  • 采集串行:因为微信 MP 登录态是全局唯一的,文章采集必须串行执行。后端限定了 workers=1,数据库层面还有 advisory lock 兜底
  • 存储桶article-images bucket 可以设成 public,前端展示文章时不需要后端签名 URL,减少请求量。qr bucket 保持 private,后端返回 signed URL
  • 免费额度余量:Supabase 免费配额的 500 MB 数据库对个人使用绰绰有余——一张文章记录也就几 KB,几万篇文章才几十 MB。存储桶主要是图片占空间,公众号配图一般 100–300 KB/张,1 GB 能存几千张
  • 不存在完美的免费方案:ECS 只免 2 个月,到期后按量付费一个月大概 50 块出头。到时要么升级续费,要么迁移到其他便宜 VPS,或者本地起docker也行。Supabase 免费额度用完需要等月度重置或付费。

这篇文章写的每一步我自己都跑通了一遍——从阿里云抢免费 ECS、到 Supabase 建库跑 SQL、到 Cloudflare Tunnel 配置。中间踩的坑都注明了。如果你也想给某件事搭一个小工具,这些羊毛值得薅一薅。项目代码在 github.com/phoenine/we-events