
上一篇刚把 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