用Cloudflare Tunnel继续继续搬paper-hot

上一篇刚把 FreshRSS 从阿里云搬回办公室 Fedora。

这件事做完以后,我突然意识到:既然 Cloudflare Tunnel 已经通了,那 PaperHot 也没必要继续依赖公网服务器。反正它就是一个 FastAPI 小服务,跑在办公室电脑上,再通过 Tunnel 暴露出去就行。

于是这篇算是上一篇的续集。

FreshRSS 是:

https://rss.seis-jun.xyz/

这次 PaperHot 是:

https://paper-hot.seis-jun.xyz/

结构也很像:

浏览器
    ↓
https://paper-hot.seis-jun.xyz/
    ↓
Cloudflare DNS / Tunnel
    ↓
办公室 Fedora 上的 cloudflared
    ↓
http://127.0.0.1:8000
    ↓
PaperHot FastAPI 服务

这样还是老优点:

  • 不需要阿里云 VPS。
  • 办公室电脑不需要公网 IP。
  • 不需要在路由器上开 80、443、8000。
  • 服务还是自己控制,代码也在自己仓库里。

前提

这次比 FreshRSS 简单一点,因为前面的基础已经搭好了。

默认已有:

seis-jun.xyz 已接入 Cloudflare
cloudflared 已安装
已有一个 Cloudflare Tunnel
/root/.cloudflared/config.yml 已存在
FreshRSS 已经通过 Tunnel 正常访问

也就是说,这次不是从零开始搭 Cloudflare Tunnel,而是在已有 Tunnel 里加一个新的 hostname:

paper-hot.seis-jun.xyz

克隆 PaperHot

PaperHot 仓库在这里:

https://github.com/junxie01/paper-hot

在 Fedora 上:

mkdir -p /home/junxie/work
cd /home/junxie/work

git clone https://github.com/junxie01/paper-hot.git
cd paper-hot

如果已经 clone 过了,就直接进去:

cd /home/junxie/work/paper-hot

Python 版本别太激进

这里有个坑。

一开始如果用 Python 3.14,安装 pandas==2.1.4 可能会触发 NumPy 源码编译,然后一路报错。这个事情很烦,尤其是你只是想把一个服务跑起来,不是想研究 Python 包编译生态。

所以这次直接用 Python 3.12。

sudo dnf install -y python3.12 python3.12-devel

创建虚拟环境:

cd /home/junxie/work/paper-hot

rm -rf venv
python3.12 -m venv venv
source venv/bin/activate

确认版本:

python -V

期望类似:

Python 3.12.13

安装依赖:

pip install --upgrade pip setuptools wheel
pip install -r requirements.txt

先手动跑起来

不要一上来就 systemd。先手动跑,确认项目本身没问题。

cd /home/junxie/work/paper-hot
source venv/bin/activate

uvicorn backend.main:app --host 127.0.0.1 --port 8000

另开一个终端测试:

curl -s -o /tmp/paper-hot-local.html -w "LOCAL HTTP %{http_code}\n" http://127.0.0.1:8000/

期望:

LOCAL HTTP 200

也可以测一下旧路径:

curl -s -o /tmp/paper-hot-local.html -w "LOCAL /paper-hot HTTP %{http_code}\n" http://127.0.0.1:8000/paper-hot

期望:

LOCAL /paper-hot HTTP 200

这里还有一个小坑:不要用这个判断服务是否正常:

curl -I http://127.0.0.1:8000/

因为 curl -I 发的是 HEAD 请求。PaperHot 可能只允许 GET,于是会返回:

405 Method Not Allowed
allow: GET

这不是服务坏了,只是 HEAD 不支持。验收时用普通 GET。

把 PaperHot 加进 Cloudflare Tunnel

FreshRSS 那篇里已经有一个 Tunnel 了,所以这次只需要改 ingress。

先备份配置:

sudo cp /root/.cloudflared/config.yml /root/.cloudflared/config.yml.bak.$(date +%Y%m%d_%H%M%S)

编辑:

sudo nano /root/.cloudflared/config.yml

配置类似这样:

tunnel: <TUNNEL_ID>
credentials-file: /root/.cloudflared/<TUNNEL_ID>.json

ingress:
  - hostname: rss.seis-jun.xyz
    service: http://127.0.0.1:8080
  - hostname: paper-hot.seis-jun.xyz
    service: http://127.0.0.1:8000
  - service: http_status:404

新增的是:

  - hostname: paper-hot.seis-jun.xyz
    service: http://127.0.0.1:8000

注意它必须放在:

  - service: http_status:404

之前。

这个 http_status:404 是兜底规则,必须放最后。不然前面的域名规则可能根本轮不到。

保存后看一眼:

sudo cat /root/.cloudflared/config.yml

添加 DNS 路由

执行:

sudo cloudflared tunnel route dns freshrss-office paper-hot.seis-jun.xyz

成功时会看到类似:

Added CNAME paper-hot.seis-jun.xyz which will route to this tunnel

然后重启 Tunnel:

sudo systemctl restart cloudflared-freshrss.service

检查:

sudo systemctl status cloudflared-freshrss.service --no-pager -l

期望:

Active: active (running)

虽然服务名字里还叫 freshrss,但其实它现在同时代理 FreshRSS 和 PaperHot。名字有点历史包袱,不过能跑就行。

公网测试

先看 DNS:

dig @1.1.1.1 paper-hot.seis-jun.xyz +short

正常会返回 Cloudflare IP。

再测公网:

curl -s -o /tmp/paper-hot-public.html -w "PUBLIC HTTP %{http_code}\n" https://paper-hot.seis-jun.xyz/

期望:

PUBLIC HTTP 200

如果本机 DNS 暂时解析不到,可以绕过本机 DNS 测:

curl -v --connect-timeout 15 \
  --resolve paper-hot.seis-jun.xyz:443:104.21.96.86 \
  https://paper-hot.seis-jun.xyz/ \
  -o /tmp/paper-hot-public-bypass.html

看到:

HTTP/2 200

就说明 Cloudflare、Tunnel、PaperHot 都是通的,只是当前机器 DNS 缓存或解析器有问题。

用用户级 systemd 管 PaperHot

一开始试过 system 级服务:

/etc/systemd/system/paper-hot.service

但 Fedora 上可能会遇到:

status=203/EXEC

这种错误很烦。PaperHot 本来就在用户目录:

/home/junxie/work/paper-hot

所以更自然的办法是用户级 systemd。

先清理旧的 system 级服务:

sudo systemctl disable --now paper-hot.service 2>/dev/null || true
sudo rm -f /etc/systemd/system/paper-hot.service
sudo systemctl daemon-reload
sudo systemctl reset-failed paper-hot.service 2>/dev/null || true

启用 linger,让用户服务开机后也能跑:

sudo loginctl enable-linger junxie

创建用户服务目录:

mkdir -p ~/.config/systemd/user

写服务文件:

cat > ~/.config/systemd/user/paper-hot.service <<'EOF'
[Unit]
Description=Paper Hot FastAPI Service

[Service]
Type=simple
WorkingDirectory=/home/junxie/work/paper-hot
Environment=PATH=/home/junxie/work/paper-hot/venv/bin:/usr/local/bin:/usr/bin:/bin
ExecStart=/home/junxie/work/paper-hot/venv/bin/python -m uvicorn backend.main:app --host 127.0.0.1 --port 8000 --proxy-headers --forwarded-allow-ips=*
Restart=always
RestartSec=10

[Install]
WantedBy=default.target
EOF

启动:

systemctl --user daemon-reload
systemctl --user enable --now paper-hot.service

检查:

systemctl --user status paper-hot.service --no-pager -l

期望看到:

Active: active (running)
Uvicorn running on http://127.0.0.1:8000

再看端口:

ss -ltnp | grep ':8000' || echo "No process listening on 8000"

期望:

LISTEN ... 127.0.0.1:8000 ... python

开机自启验证

PaperHot:

systemctl --user is-enabled paper-hot.service
systemctl --user is-active paper-hot.service

期望:

enabled
active

linger:

loginctl show-user junxie -p Linger

期望:

Linger=yes

Tunnel:

sudo systemctl is-enabled cloudflared-freshrss.service
sudo systemctl is-active cloudflared-freshrss.service

期望:

enabled
active

如果要狠一点,就重启机器:

sudo reboot

等 1-2 分钟后检查:

systemctl --user status paper-hot.service --no-pager -l
sudo systemctl status cloudflared-freshrss.service --no-pager -l

curl -s -o /tmp/paper-hot.html -w "PaperHot HTTP %{http_code}\n" https://paper-hot.seis-jun.xyz/

期望:

PaperHot HTTP 200

还是那句话,别用 curl -I 吓自己。它可能返回:

HTTP/2 405
allow: GET

这是 HEAD 请求不支持,不是服务坏了。

日常维护

看 PaperHot:

systemctl --user status paper-hot.service --no-pager -l

重启 PaperHot:

systemctl --user restart paper-hot.service

看日志:

journalctl --user -u paper-hot.service -n 100 --no-pager

看 Cloudflare Tunnel:

sudo systemctl status cloudflared-freshrss.service --no-pager -l

重启 Tunnel:

sudo systemctl restart cloudflared-freshrss.service

看 Tunnel 日志:

sudo journalctl -u cloudflared-freshrss.service -n 120 --no-pager

本地测试:

curl -s -o /tmp/paper-hot-local.html -w "LOCAL HTTP %{http_code}\n" http://127.0.0.1:8000/

公网测试:

curl -s -o /tmp/paper-hot-public.html -w "PUBLIC HTTP %{http_code}\n" https://paper-hot.seis-jun.xyz/

几个坑

pandas 装不上

如果看到:

Failed to build pandas
Cannot compile Python.h

并且 Python 是 3.14,大概率就是版本太新。直接换 Python 3.12:

sudo dnf install -y python3.12 python3.12-devel
python3.12 -m venv venv
source venv/bin/activate
pip install -r requirements.txt

systemd 203/EXEC

如果 system 级服务报:

status=203/EXEC

不要继续硬修 /etc/systemd/system/paper-hot.service。PaperHot 在用户 home 目录下,用用户级 systemd 更顺手:

systemctl --user enable --now paper-hot.service

公网 502

先查本地:

ss -ltnp | grep ':8000' || echo "No process listening on 8000"
curl -s -o /tmp/paper-hot-local.html -w "LOCAL HTTP %{http_code}\n" http://127.0.0.1:8000/

如果本地不是 200,说明 PaperHot 没起来。

systemctl --user restart paper-hot.service

如果本地是 200,但公网还是 502,再查 Tunnel:

sudo systemctl status cloudflared-freshrss.service --no-pager -l
sudo journalctl -u cloudflared-freshrss.service -n 120 --no-pager

DNS 解析不到

如果:

Could not resolve host: paper-hot.seis-jun.xyz

但公共 DNS 正常:

dig @1.1.1.1 paper-hot.seis-jun.xyz +short

那多半是本机 DNS 缓存问题:

sudo resolvectl flush-caches
resolvectl query paper-hot.seis-jun.xyz

也可以临时绕过 DNS:

curl -v --connect-timeout 15 \
  --resolve paper-hot.seis-jun.xyz:443:104.21.96.86 \
  https://paper-hot.seis-jun.xyz/ \
  -o /tmp/paper-hot-public-bypass.html

curl -I 返回 405

这个最容易误判。

curl -I https://paper-hot.seis-jun.xyz/

返回:

HTTP/2 405
allow: GET

不是坏了。正确测试:

curl -s -o /tmp/paper-hot.html -w "PaperHot HTTP %{http_code}\n" https://paper-hot.seis-jun.xyz/

最后

现在这台办公室 Fedora 上就有两个服务了:

FreshRSS:
https://rss.seis-jun.xyz/

PaperHot:
https://paper-hot.seis-jun.xyz/

关系大概是:

paper-hot.seis-jun.xyz
    ↓
Cloudflare
    ↓
Cloudflare Tunnel
    ↓
办公室 Fedora: 127.0.0.1:8000
    ↓
PaperHot FastAPI / Uvicorn

当前状态应该是:

cloudflared-freshrss.service:system 级服务,enabled + active
paper-hot.service:用户级服务,enabled + active
Linger=yes:允许用户服务开机后自动运行

验收命令:

curl -s -o /tmp/paper-hot.html -w "PaperHot HTTP %{http_code}\n" https://paper-hot.seis-jun.xyz/

期望:

PaperHot HTTP 200

上一篇是把 RSS 搬回来,这一篇是把 PaperHot 接上。两个服务都不需要公网 IP,不需要开路由器端口,也不用再惦记阿里云到期。

未来的我如果又忘了怎么整,别翻聊天记录了,看这篇就行。

Comments

← 用Cloudflare Tunnel把FreshRSS搬回办公室电脑 Blog Paper Reading (60) →