01目标与前置条件
目标是让服务器统一保存订阅、文章和阅读状态,浏览器与客户端访问同一个 FreshRSS 实例。网页入口为 https://rss.example.com,客户端入口为 https://rss.example.com/api/greader.php。
原实践从云服务器选型、SSH、Docker 和裸 IP 访问开始,随后接入域名、Caddy 与 HTTPS。本文保留已确认的故障与关键配置,并将复现步骤整理成一条更安全的主线:初始 HTTP 调试通过 SSH 隧道进行。定时抓取、自动备份及最终 HTTPS 客户端同步的完整验收,不作为原记录中已经验证的成果。
- 一台具有公网 IPv4 的 Ubuntu 服务器;本文安装示例适用于 Docker 官方支持的 Ubuntu LTS,使用具有
sudo权限的账户。 - 一台可使用 OpenSSH 的本地电脑,示例使用 Windows PowerShell。
- 一个可管理 DNS 的域名;示例使用 Cloudflare 托管 DNS,且域名已完成 Nameserver 委派。
- 能够修改云防火墙/安全组;服务器能够访问软件仓库、镜像仓库、证书服务和订阅源。
| 占位符 | 替换为 |
|---|---|
SERVER_IP | 云服务器公网 IPv4 |
SSH_USER | Linux 登录账户,与 FreshRSS 用户可以不同 |
USERNAME | FreshRSS 用户名,保留实际大小写 |
rss.example.com | 自己的 RSS 子域名 |
YOUR_ADMIN_IP | 当前用于 SSH 管理的公网 IPv4 |
02服务器与 Linux:先明确职责
轻量应用服务器通常把计算资源、公网带宽和控制台管理打包,适合这类小型个人服务;通用云服务器提供更灵活的网络和资源配置。两者都能运行 FreshRSS。这里的“服务器”是持续在线的计算机,Linux 是其操作系统,FreshRSS 是运行在其中的应用。
原讨论以 2 核、2 GB 内存、3 Mbps 带宽为起点。这可以作为个人少量订阅的起始配置,但并非性能承诺:订阅数量、刷新频率、全文抓取、并发用户和图片流量都会影响资源需求。机房位置应同时考虑客户端到服务器、服务器到订阅源的实际连通性。
本方案使用 Ubuntu,便于沿用 Docker 官方安装流程。创建服务器后记录公网 IP、系统版本和初始登录账户。不要把云平台账户、Linux 账户和 FreshRSS 账户混为一谈。
cat /etc/os-release
uname -m
free -h
df -h检查标准:系统为预期的 Ubuntu 版本,架构受镜像支持,磁盘有足够空间保存镜像、文章和后续备份。
03SSH 与 Ed25519:建立管理通道
SSH 为远程命令行提供加密连接。Ed25519 是一种数字签名算法,用于生成 SSH 密钥对:公钥放在服务器,私钥保留在本地。它不是服务器地址、密码或额外的网络协议。
生成密钥并登记公钥
ssh -V
ssh-keygen -t ed25519 -C "rss-server" -f "$HOME/.ssh/id_ed25519_rss"
Get-Content "$HOME/.ssh/id_ed25519_rss.pub"生成时为私钥设置口令;若指定文件已经存在,不要覆盖现有密钥。把 .pub 文件内容添加到云平台提供的 SSH 公钥管理入口并绑定实例,按平台要求生效。首次连接前,通过云控制台或可信管理通道核对服务器 SSH 主机密钥指纹。
ssh -i "$HOME/.ssh/id_ed25519_rss" SSH_USER@SERVER_IP若服务器已有密码登录通道,也可先登录,再把公钥追加到目标账户的 ~/.ssh/authorized_keys。目录权限应为 700,文件权限为 600,所有者为该账户。不要把本地私钥上传到服务器。
保留当前 SSH 窗口,另开一个窗口验证密钥登录。确认成功且有云控制台恢复通道后,再考虑禁用密码登录或 root 直接登录,避免把自己锁在服务器外。
04安装 Docker Engine 与 Compose
Docker 镜像包含应用及运行依赖,容器是镜像运行后的实例;Compose 用一份 YAML 文件描述服务、端口、网络和数据卷。本文使用 docker compose 插件命令。以下针对干净的 Ubuntu;已有 Docker 环境应先确认版本和安装来源,按官方安装文档处理冲突包。
sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
sudo tee /etc/apt/sources.list.d/docker.sources > /dev/null <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now dockersudo docker version
sudo docker compose version
sudo docker run --rm hello-world检查标准:能显示 Docker 客户端及服务端版本,Compose 可运行,测试容器正常退出。后文统一使用 sudo docker,不依赖将用户加入具备高权限的 docker 用户组。
05先启动 FreshRSS 并完成初始化
创建工作目录和初始配置
mkdir -p ~/services/freshrss
cd ~/services/freshrss
nano compose.yaml把下面内容保存为 ~/services/freshrss/compose.yaml。YAML 使用空格缩进;不要复制命令提示符或代码块围栏。
services:
freshrss:
image: freshrss/freshrss:latest
container_name: freshrss
restart: unless-stopped
ports:
- "127.0.0.1:8080:80"
volumes:
- freshrss_data:/var/www/FreshRSS/data
- freshrss_extensions:/var/www/FreshRSS/extensions
volumes:
freshrss_data:
freshrss_extensions:127.0.0.1:8080:80 将服务器回环地址的 8080 转发到容器 80,不创建面向公网的 8080 入口。两个命名卷分别保存 FreshRSS 数据和扩展;容器重建时可继续挂载。镜像与数据目录的用法参见 FreshRSS Docker 文档。
sudo docker compose config -q
sudo docker compose up -d
sudo docker compose ps
sudo docker compose logs --tail=80 freshrss
curl -I http://127.0.0.1:8080先确认 config -q 无错误,再执行启动。HTTP 返回页面或安装跳转表示服务已响应;仅看到容器为 Up 还不能证明应用完成初始化。
通过 SSH 隧道打开安装页面
ssh -i "$HOME/.ssh/id_ed25519_rss" -N -L 18080:127.0.0.1:8080 SSH_USER@SERVER_IP浏览器访问 http://127.0.0.1:18080。本地 HTTP 通过加密 SSH 隧道传输到服务器;无需向公网开放 8080。原流程早期使用过 http://SERVER_IP:8080,复现时采用隧道可避免在公网明文提交登录凭据。
- 选择界面语言并通过环境检查。
- 个人实例可选择 SQLite,避免额外部署数据库;数据保存在已挂载的数据卷中。
- 创建用户
USERNAME和强网页登录密码。 - 选择 Form Authentication/表单认证,完成安装并登录。
- 添加一个已知可访问的订阅源,手动刷新,验证能读取文章。
保留相同项目目录、项目名称及卷声明,避免重建时连接到另一套空卷。不要用 docker compose down -v 处理一般故障,它会删除该项目的相关数据卷。
06域名、A 记录、TTL 与 DNS Only
DNS 把域名解析为地址。先确认域名注册商处的 Nameserver 指向实际使用的 DNS 服务商,且 Cloudflare 区域已激活,再添加记录:
| 字段 | 示例值 | 含义 |
|---|---|---|
| Type | A | 将名称映射到 IPv4;IPv6 使用 AAAA |
| Name | rss | 在 example.com 区域中形成 rss.example.com |
| IPv4 / Content | SERVER_IP | 服务器公网地址 |
| TTL | Auto | 由 DNS 服务商设置记录缓存有效期 |
| Proxy status | DNS only/灰云 | DNS 返回源站地址,客户端直接连接服务器 |
TTL 是缓存有效时间,不是服务器响应超时,也不是修改后必须等待的固定倒计时。先前缓存的记录或 NXDOMAIN 负缓存可能仍然有效。DNS Only 只提供解析,Cloudflare 不代理这条记录的 HTTP/HTTPS 流量;因此源站 IP 可见,HTTPS 由 Caddy 提供。参见 Cloudflare 代理状态说明及记录字段说明。
nslookup rss.example.com
nslookup rss.example.com 1.1.1.1检查标准:查询结果中的目标地址为 SERVER_IP。输出开头的“服务器”是被询问的 DNS 解析器,不是 FreshRSS 服务器。“非权威应答”是递归解析器的常见响应,不代表结果错误。
如果未部署 IPv6,不要为该主机保留指向错误地址的 AAAA 记录;否则部分客户端或证书验证可能沿 IPv6 连接失败。
07放行必要端口,区分两层防火墙
云平台防火墙/安全组与 Ubuntu 本机防火墙是两个独立检查点。容器端口映射又决定流量是否能到达应用。本文最终入口如下:
| 协议 / 端口 | 入站来源 | 用途 |
|---|---|---|
| TCP 22 | 优先仅 YOUR_ADMIN_IP/32 | SSH;若改过端口,以实际端口为准 |
| TCP 80 | 公网 IPv4 | HTTP 跳转及 ACME HTTP-01 |
| TCP 443 | 公网 IPv4 | HTTPS 及 TLS-ALPN-01 |
| UDP 443 | 公网 IPv4,可选 | HTTP/3;普通 HTTPS 不依赖它 |
| TCP 8080 | 不开放公网 | 只保留回环地址调试 |
公网 IPv4 来源通常写为 0.0.0.0/0。若启用 IPv6,还需对应 AAAA、监听和 IPv6 防火墙规则。先在云控制台添加 TCP 80、443 规则,并确认已有 SSH 通道可用。
sudo ufw status verbose
sudo ss -lntp若已启用 UFW,可按本方案补充规则:
sudo ufw allow from YOUR_ADMIN_IP to any port 22 proto tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# 仅在需要 HTTP/3 时添加
sudo ufw allow 443/udp若 UFW 尚未启用,先确认 SSH 放行规则和恢复通道,再决定启用。Docker 发布端口可能绕过部分 UFW 规则,不能仅凭 UFW 状态认定容器已隔离;应结合云规则、绑定地址和外部连接测试。此行为见 Docker 防火墙限制。
08加入 Caddy:统一 HTTPS 入口
Caddy 在这里负责接收公网请求、终止 TLS、自动管理证书,再把请求转发给 FreshRSS。两个容器位于同一个 Compose 默认网络,Caddy 使用服务名 freshrss 寻址,而不是连接自己容器内的 127.0.0.1。
保存完整的最终配置
保持在 ~/services/freshrss 目录。先备份已有配置,再编辑;以下 Compose 应整体替换原文件,而不是粘贴到旧配置尾部。
cd ~/services/freshrss
cp compose.yaml compose.yaml.before-caddy
nano Caddyfilerss.example.com {
reverse_proxy freshrss:80
}services:
freshrss:
image: freshrss/freshrss:latest
container_name: freshrss
restart: unless-stopped
ports:
- "127.0.0.1:8080:80"
volumes:
- freshrss_data:/var/www/FreshRSS/data
- freshrss_extensions:/var/www/FreshRSS/extensions
caddy:
image: caddy:latest
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
volumes:
freshrss_data:
freshrss_extensions:
caddy_data:
caddy_config:Caddyfile 必须先作为文件存在。:ro 表示容器只读挂载;证书、私钥及 ACME 状态保存在 caddy_data,运行配置保存在 caddy_config。本复盘保留原流程的 latest 写法,但该标签会变化;正式维护应记录镜像摘要并固定经验证的版本。
sudo docker compose config -q
sudo docker compose run --rm --no-deps caddy caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
sudo docker compose up -d
sudo docker compose ps
sudo docker compose logs --tail=100 caddy证书签发与 HTTPS 验证
公网域名触发 Caddy 的自动 HTTPS。Caddy 通过 ACME 向证书颁发机构证明域名控制权,并管理续期和 HTTP 到 HTTPS 的跳转;原日志显示签发方为 Let's Encrypt。默认签发方及重试行为随配置、版本而定,不应把一次日志里的选择写成固定保证。参见 Caddy 自动 HTTPS。
HTTP-01 使用 TCP 80;TLS-ALPN-01 使用 TCP 443。证书验证成功不等于浏览器访问链路已通,仍需从本地电脑验证。参见 Let's Encrypt 验证方式。
Test-NetConnection rss.example.com -Port 443
curl.exe -I http://rss.example.com
curl.exe -I https://rss.example.com检查标准:TCP 测试成功,HTTP 跳转到 HTTPS,HTTPS 返回 FreshRSS 页面或合理的登录跳转,浏览器证书验证通过。不要用忽略证书验证的参数掩盖 TLS 问题。
09将 base_url 改为正式域名
base_url 表示 FreshRSS 的对外基础地址,用于生成绝对链接等。它不会修改 DNS、打开防火墙或申请证书。通过隧道初始化后,应将其改为正式的 HTTPS 地址。
sudo docker exec -u www-data freshrss sh -c 'cp /var/www/FreshRSS/data/config.php /var/www/FreshRSS/data/config.php.before-https'
sudo docker exec -u www-data freshrss php /var/www/FreshRSS/cli/reconfigure.php --base-url https://rss.example.com
sudo docker restart freshrss上述参数见 FreshRSS 配置 CLI。若安装版本不支持,可用该版本的帮助信息确认,或在持久化的 data/config.php 中只修改既有条目:
'base_url' => 'https://rss.example.com',不要把整份配置覆盖成这一行;保留数据库和认证设置。也不要凭空猜测卷在宿主机上的路径。可先查看实际挂载:
sudo docker inspect freshrss --format '{{json .Mounts}}'
sudo docker exec freshrss php -l /var/www/FreshRSS/data/config.php检查标准:语法检查通过,使用正式域名重新登录,页面跳转和生成链接不再指向旧 IP 或本地隧道地址。
10启用 Google Reader API,接入客户端
这里的 Google Reader API 是兼容接口,不需要 Google 账户。它让阅读客户端获取订阅、文章和阅读状态。网页登录与 API 登录是两套并存的认证入口。
| 入口 | 认证方式 | 使用的密码 |
|---|---|---|
| FreshRSS 网页 | Form Authentication | 网页登录密码 |
| Google Reader API | FreshRSS 用户名 + API 认证 | 用户单独设置的 API 密码 |
- 以管理员登录 FreshRSS,在管理/认证相关设置中勾选允许 API 访问;界面名称可能因版本、语言而异。
- 在对应用户的个人资料中设置 API password 并保存。不要为此切换到 HTTP Authentication。
- 使用 FreshRSS 提供的 API 检查页面/链接确认服务器配置,若显示 PASS,再继续验证用户名与 API 密码。
- 在 Fluent Reader Lite 中新增 Google Reader API 服务,填写下表。
| 客户端字段 | 值 |
|---|---|
| 服务类型 | Google Reader API |
| Endpoint / API URL | https://rss.example.com/api/greader.php |
| Username | USERNAME |
| Password | 刚保存的 API 密码 |
端点不要填成网站首页、/api/ 或 /api/fever.php,也不需要把 /accounts/ClientLogin 加到客户端配置里。API 地址与专用密码要求见 FreshRSS Google Reader API 文档。
原始报错日志中的客户端是 Windows 上的 fluent-reader,不能据此声称完成了 Lite 移动端测试。Fluent Reader Lite 是移动客户端,支持 Google Reader API;这里给出其接入方法,实际同步需按下述标准验收。
检查标准:客户端能拉取订阅与文章;将一篇文章标为已读后,网页刷新可见相同状态。网页修改另一篇文章的状态后,客户端再次同步也应更新。完成后移除客户端中保存的旧 HTTP 端点。
11五类真实故障:现象、证据与修复
先判断请求停在哪一层,再修改配置。错误码只能定位到某类响应,具体响应方和原因还需结合日志;代理也可能返回 401 或 403。
401:API 密码不匹配 原记录已确认修复
现象:客户端显示“连接到服务时出错”。FreshRSS 日志同时出现:
Password API mismatch for user USERNAME
GReaderAPI::unauthorized
POST /api/greader.php/accounts/ClientLogin HTTP/1.1 401判断:本例请求已到达 FreshRSS 的认证逻辑,直接证据是 API 密码不匹配。更改 DNS、端口映射或防火墙无法修复这个密码问题。
修复:在正确的 FreshRSS 用户资料中重新设置 API 密码并保存,然后更新客户端的用户名与 API 密码。注意大小写、首尾空格与误填网页登录密码。
验证:重新登录客户端并同步,同时检查 FreshRSS 日志是否还出现 mismatch。若需要独立排除客户端配置,可在可信的服务器终端做一次登录测试:
read -r -s -p 'API password: ' RSS_API_PASSWORD
printf '\n'
printf '%s' "$RSS_API_PASSWORD" | curl -sS \
--data-urlencode 'Email=USERNAME' \
--data-urlencode 'Passwd@-' \
'https://rss.example.com/api/greader.php/accounts/ClientLogin'
unset RSS_API_PASSWORD成功响应应包含 Auth=... 令牌;不要发布或截图分享响应中的凭据。该测试验证登录,仍需客户端同步验证实际读取和状态写回。
403:误选 HTTP Authentication 原记录已确认修复
现象:API 检查出现 PASS,但网页提示:
Error 403 - Forbidden
HTTP Remote-User= ; Remote IP address=CLIENT_IP判断:用户确认选择了 HTTP Authentication,而当前 Web 服务器没有提供所需的 REMOTE_USER 身份。API 自检通过不等于网页登录模式正确。此认证方式的依赖见 FreshRSS 访问控制文档。
修复:通过 SSH 备份配置后恢复表单认证:
sudo docker exec -u www-data freshrss sh -c 'cp /var/www/FreshRSS/data/config.php /var/www/FreshRSS/data/config.php.before-auth-fix'
sudo docker exec -u www-data freshrss php /var/www/FreshRSS/cli/reconfigure.php --auth-type form
sudo docker restart freshrss若需手工修复,在实际挂载的 data/config.php 中把 'auth_type' => 'http_auth' 改成 'auth_type' => 'form',保留其余内容,再做 PHP 语法检查。
验证:网页重新显示 FreshRSS 登录表单并可用网页密码登录;随后使用 API 密码独立验证客户端。
NXDOMAIN:不同解析路径结果不一致 观察明确,根因未完全确认
现象:本地默认解析器返回 Non-existent domain;稍后向公共解析器 1.1.1.1 查询,得到正确的服务器 IP。
判断:证据表明两次查询结果不同。缓存、负缓存、上游解析器或查询时间差都可能参与,不能仅凭此认定路由器故障,也不能宣称所有地区的解析已经更新。
处理:检查记录名称、公网地址、区域激活状态和 Nameserver 委派;对比默认、公共及权威解析器的结果。
nslookup rss.example.com
nslookup rss.example.com 1.1.1.1
nslookup -type=NS example.com
# 将 AUTH_NS 替换为上一步得到的权威名称服务器
nslookup rss.example.com AUTH_NS
ipconfig /flushdnsipconfig /flushdns 只清理 Windows 本机缓存,不会清除路由器或公共解析器缓存。等待对应缓存到期后复测;若权威服务器也没有记录,应先修复权威配置。
验证:实际使用的解析路径返回预期地址,再继续检查 TCP/TLS。DNS 查询不测试 Caddy 或 FreshRSS 是否运行。
443 timeout:拿到证书,浏览器仍然超时 原记录确认云防火墙未放行
现象:Caddy 日志先后出现以下关键信息,浏览器访问 HTTPS 仍超时:
challenge_type: tls-alpn-01
Timeout during connect (likely firewall problem)
challenge_type: http-01
certificate obtained successfully原因:云防火墙只放行了 TCP 80,没有放行 TCP 443。HTTP-01 可以通过 80 完成域名验证,但浏览器的 HTTPS 连接仍依赖 443。
修复:增加云防火墙 TCP 443 入站规则。若证书已签发且 Caddy 已监听,无需因此删除证书或重装应用。
Test-NetConnection rss.example.com -Port 443sudo docker compose ps
sudo docker compose logs --tail=100 caddy
sudo ss -lntp
sudo ufw status verbose验证:外部 TCP 测试成功,浏览器 HTTPS 可访问且证书有效。TcpTestSucceeded: False 仅说明 TCP 建连失败;其他环境还要排查监听、映射、路由等,不能单凭它断定是防火墙。
YAML syntax:配置粘连和重复块 原文件定位到具体错误
现象:Compose 报错 mapping values are not allowed in this context,定位提示 L42.C12。查看邻近行后发现:
caddy_config:- "8080:80"原因:本应独立的 caddy_config: 与旧端口行被粘在一起,尾部还残留重复的 volumes 块。报错位置是解析器发现异常的位置,不一定是最早出错的一行。
nl -ba compose.yaml | sed -n '35,48p'
sudo docker compose config -q修复:先备份,按第 08 节整体恢复完整 Compose 文件,删除粘连和重复块;只用空格缩进。已有自定义设置需先核对再合并,不能盲目覆盖。
验证:config -q 无错误后才执行 sudo docker compose up -d。不要用删除数据卷来修复 YAML 语法。
12最终架构与验收
DNS 查询(控制面)
rss.example.com ── A / DNS Only ──> SERVER_IP
实际访问(数据面)
浏览器 / Fluent Reader / Fluent Reader Lite
│
│ HTTPS · TCP 443
▼
云防火墙 + 宿主机网络规则
│
┌───────▼────────────────────┐
│ Ubuntu / Docker Compose │
│ │
TCP 80 ──>│ Caddy │
跳转/ACME │ TLS 终止、证书续期 │
│ │ │
│ │ HTTP freshrss:80 │
│ ▼ Compose 内部网络 │
│ FreshRSS │
│ 网页 + Google Reader API │
│ │ │
│ └─> data/extensions │
│ Caddy ──> data/config │
└───────────────────────────┘
管理:本地电脑 ── SSH / TCP 22 ──> Ubuntu
调试:SSH 隧道 ──> 127.0.0.1:8080 ──> FreshRSS
抓取:FreshRSS ── 出站 HTTP/HTTPS ──> RSS 订阅源逐层验收清单
- DNS 返回正确公网地址,未留下错误 AAAA 记录。
- 外部 TCP 443 可达,HTTP 自动跳转到 HTTPS,证书验证通过。
- FreshRSS 网页可登录,base_url 为正式 HTTPS 域名。
- API 登录成功,客户端文章列表和阅读状态可双向同步。
- 重启 FreshRSS 后订阅与数据仍在,两个服务状态正常。
- 云防火墙无公网 8080 放行规则;外部设备无法直连 SERVER_IP:8080。
- SSH 密钥登录可用,管理来源限制与恢复通道已经确认。
以上是发布前应逐项完成的验收,不代表原对话已经留下每一项的测试证据。
13收尾与安全建议
收紧入口和凭据
HTTPS 与同步验证后,删除旧的公网 8080 放行规则;只需容器内部访问时,也可移除 FreshRSS 的整个 ports 块,再执行 Compose 更新。保留公网 80/443 和受限的 SSH 入口。若早期在公网 HTTP 下提交过密码,应更换网页密码和 API 密码。
不启用不需要的匿名访问、公开注册或额外认证代理。定期更新系统与应用;不要把私钥、API 令牌、数据目录或带密码的配置上传到博客仓库。Caddy 数据卷包含私钥,应按敏感数据保护。
更新前先记录版本,备份后再升级
sudo docker image inspect freshrss/freshrss:latest --format '{{json .RepoDigests}}'
sudo docker image inspect caddy:latest --format '{{json .RepoDigests}}'可将经测试的版本标签或 image@sha256:IMAGE_DIGEST 写入 Compose,避免可变标签造成版本漂移。升级应先阅读对应发行说明、做一致性备份,再拉取与重建;数据库迁移后不能假定只换回旧镜像就一定可以回滚。
定时抓取:后续实施,单独验收
原记录尚未完成定时抓取配置。可按 FreshRSS 官方镜像文档,在 freshrss 服务中增加如下环境设置作为后续示例,然后重新运行 Compose:
environment:
TZ: Asia/Shanghai
CRON_MIN: '*/20'该示例配置镜像每 20 分钟触发刷新任务;具体订阅还受自身刷新策略、请求限制和错误状态影响。添加后至少观察两个调度周期,检查日志和文章更新时间。客户端“同步”与服务器“抓取订阅源”不是同一件事。
备份:持久化之外还要能够恢复
备份范围包括 Compose、Caddyfile、FreshRSS 数据及扩展卷;Caddy 的持久化状态也可纳入加密备份。对 SQLite,应在停止写入后备份整个数据卷,或采用数据库支持的一致性备份方法,避免直接复制正在写入的数据库。OPML 导出有助于恢复订阅列表,但不能完整恢复文章和阅读状态。
将备份保存在服务器之外,设置保留周期,并在隔离环境做一次恢复演练。自动备份与恢复演练在本次记录中仍是待办,不能用“用了 Volume”代替“已经备份”。
14进阶:拆分 Caddy,为静态博客预留位置
当服务器开始承载多个应用时,可以将 Caddy 从 FreshRSS 的 Compose 项目拆出,作为独立入口管理。FreshRSS 保留原有数据卷,通过共享 Docker 网络连接 Caddy;博客文件以只读方式挂载到 Caddy。
重建 FreshRSS 或切换占用 80/443 的 Caddy 会造成短暂访问中断。以下是后续迁移方案,尚未在本次记录中实际执行。先核对现状、备份和校验,最后才切换;不要使用 down -v、卷清理或 --remove-orphans 来处理这次迁移。
~/services/
├── caddy/
│ ├── compose.yaml
│ └── Caddyfile
├── freshrss/
│ └── compose.yaml
└── blog/
└── html/
├── index.html # 已有首页保留
└── notes/
└── freshrss/
└── index.html # 本篇文章
Docker 外部网络 web
├── caddy
└── freshrss
14.1 先记录当前卷、镜像和配置
在当前 ~/services/freshrss 目录执行。备份名称应使用未占用的名称;若已存在同名备份,先换一个新名称。
cd ~/services/freshrss
cp -p compose.yaml compose.before-split.yaml
cp -p Caddyfile Caddyfile.before-split
sudo docker inspect freshrss --format '{{json .Mounts}}'
sudo docker inspect caddy --format '{{json .Mounts}}'
sudo docker inspect freshrss --format '{{.Config.Image}} {{.Image}}'
sudo docker inspect caddy --format '{{.Config.Image}} {{.Image}}'
sudo docker inspect freshrss --format '{{index .Config.Labels "com.docker.compose.project"}}'
记录 FreshRSS 的 data、extensions,以及 Caddy 的 /data、/config 对应的真实卷名。本文用 EXISTING_FRESHRSS_DATA_VOLUME、EXISTING_FRESHRSS_EXTENSIONS_VOLUME、EXISTING_CADDY_DATA_VOLUME、EXISTING_CADDY_CONFIG_VOLUME 代替它们。若 Mounts 显示的是 bind 挂载,应保留原绝对路径,不能套用下面的命名卷声明。
把当前两个镜像的不可变摘要记录为 FRESHRSS_IMAGE_REF 与 CADDY_IMAGE_REF,在后文替换成实际的 仓库名@sha256:摘要;可用 docker image inspect 查询 RepoDigests。此次只迁移结构,不同时升级应用。现有环境变量、刷新配置和其他自定义挂载也要保留。
先按第 13 节完成 FreshRSS 一致性备份,并保存配置与 Caddy 持久化数据。更换 Compose 目录通常会改变默认项目名:若只写 caddy_data:,新项目可能创建另一套空卷。下面使用 external: true 和明确的 name 复用原卷;卷不存在时直接报错,避免悄悄初始化新数据。参见 Compose 外部卷与名称。
14.2 创建共享网络,先让旧 Caddy 加入
sudo docker network inspect web
# 只有上一步明确显示网络不存在时,才执行:
sudo docker network create web
sudo docker network inspect web确认 web 是预期的本机 bridge 网络,没有与其他用途冲突。检查旧 Caddy 所在网络;若尚未接入 web,再连接一次:
sudo docker inspect caddy --format '{{json .NetworkSettings.Networks}}'
sudo docker network connect web caddy旧 Caddy 先接入 web 后,FreshRSS 切换网络时仍有可用的代理路径。这是临时过渡连接;新 Caddy 的 Compose 将持久声明 web。若已接入,不要重复执行 connect。跨项目的服务名解析见 Compose 网络文档。
14.3 修改 FreshRSS Compose,明确复用原卷
用下列配置替换 FreshRSS 项目的 Compose;所有大写占位符必须替换,现有自定义设置应合并保留。继续使用原项目目录及原项目名称;若原来通过 -p 指定项目名,后续命令也要带相同参数。
services:
freshrss:
image: FRESHRSS_IMAGE_REF
container_name: freshrss
restart: unless-stopped
ports:
- "127.0.0.1:8080:80"
volumes:
- freshrss_data:/var/www/FreshRSS/data
- freshrss_extensions:/var/www/FreshRSS/extensions
networks:
- web
volumes:
freshrss_data:
external: true
name: EXISTING_FRESHRSS_DATA_VOLUME
freshrss_extensions:
external: true
name: EXISTING_FRESHRSS_EXTENSIONS_VOLUME
networks:
web:
external: truecd ~/services/freshrss
sudo docker compose config -q
sudo docker compose up -d --no-deps freshrss
sudo docker inspect freshrss --format '{{json .Mounts}}'
sudo docker network inspect web
curl -I http://127.0.0.1:8080对比前后的卷名,确认完全相同,再检查网页中的订阅和阅读状态。旧 Caddy 可能被提示为 orphan,这是服务从配置移除后的预期现象,此刻先保留它。若出现全新安装页,应立即停止后续迁移并核对卷,不要重新初始化。
14.4 准备博客目录和独立 Caddy
mkdir -p ~/services/caddy
mkdir -p ~/services/blog/html/notes/freshrss
cd ~/services/caddy
nano compose.yamlservices:
caddy:
image: CADDY_IMAGE_REF
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
- ../blog/html:/srv/blog:ro
networks:
- web
volumes:
caddy_data:
external: true
name: EXISTING_CADDY_DATA_VOLUME
caddy_config:
external: true
name: EXISTING_CADDY_CONFIG_VOLUME
networks:
web:
external: true../blog/html 相对于这份 Compose 文件所在目录解析,避免 ~ 展开差异;也可替换成经过核实的绝对路径。原有 Caddy 卷继续保留,证书和 ACME 状态不会因项目改名而自动丢失。
14.5 选择与当前网站相符的 Caddyfile
情况 A:根域名尚未承载网站,或原本就是该目录的静态站。可以使用下面的完整配置。只有尚无首页时,才在 ~/services/blog/html/index.html 新建测试页;不要覆盖已有首页。
example.com {
root * /srv/blog
file_server
}
www.example.com {
redir https://example.com{uri}
}
rss.example.com {
reverse_proxy freshrss:80
}
情况 B:根域名已经反向代理到现有网站应用。先复制原 Caddyfile,保留全部域名与现有设置;只在根域名现有站点块内为文章增加精确目录路由。下例是集成结构,EXISTING_SITE_UPSTREAM 必须替换成原 upstream,并保留原代理参数。现有站点若已使用 handle/route,应合并到原结构中,不要重复定义站点块。
example.com {
redir /notes/freshrss /notes/freshrss/ 308
handle /notes/freshrss/* {
root * /srv/blog
file_server
}
handle {
reverse_proxy EXISTING_SITE_UPSTREAM
}
}
rss.example.com {
reverse_proxy freshrss:80
}此处使用 handle 保留 URL 路径,文件对应 /srv/blog/notes/freshrss/index.html。如果已有站点是静态站,可直接向其现有根目录增加文章子目录。迁移后,新 Caddy 必须仍能到达原网站 upstream;若依赖旧 Docker 网络,应让新 Caddy 同时加入该网络。共享网关的配置错误可能影响多个域名,所以目录隔离并不等于网关故障隔离。
将下载的教程保存为服务器上的 ~/services/blog/html/notes/freshrss/index.html。本地可用以下命令上传,LOCAL_HTML_PATH 替换为实际下载位置:
scp -i "$HOME/.ssh/id_ed25519_rss" "LOCAL_HTML_PATH" SSH_USER@SERVER_IP:~/services/blog/html/notes/freshrss/index.html文章独立访问时,其内置 CSS 不影响其他页面。只把它作为完整页面发布,不将整份 HTML 嵌进网站公共模板。以后更新文章文件通常无需重启 Caddy。
14.6 校验配置后再切换 Caddy
新 Caddyfile 应已创建,并包含原 Caddy 承载的所有站点。停旧容器前先做语法适配检查;该检查不启动公网监听,也不与旧 Caddy 共用证书存储执行签发操作。
cd ~/services/caddy
sudo docker compose config -q
sudo docker run --rm \
-v "$PWD/Caddyfile:/etc/caddy/Caddyfile:ro" \
CADDY_IMAGE_REF \
caddy adapt --config /etc/caddy/Caddyfile --adapter caddyfile --validate校验通过、镜像在本地可用、挂载文件及原卷均已确认后,在短暂维护窗口切换。以下保留旧容器用于快速回滚,不强制删除它:
sudo docker stop caddy
sudo docker rename caddy caddy-before-split
cd ~/services/caddy
sudo docker compose up -d
sudo docker compose ps
sudo docker compose logs --tail=100 caddy旧容器已停止,新容器接管 80/443。不要同时启动两个使用相同端口和证书卷的 Caddy。确认稳定后,可以单独删除已停止的 caddy-before-split 容器;保留其原卷和备份。
14.7 DNS、验证与回滚
若根域名和 www 还没有指向此服务器,且明确由这台服务器接管它们,添加如下记录;已有网站解析正确时保持不动。示例不保留真实 IP。
| 类型 | 名称 | 目标 | 代理状态 |
|---|---|---|---|
| A | @ | SERVER_IP | DNS only |
| A | www | SERVER_IP | DNS only |
| A | rss | SERVER_IP | 保留原配置 |
只有确实配置了 www 跳转时才需要相应 DNS。确认 TCP 80/443 仍放行,等待新域名证书签发,再从本地测试:
curl.exe -I https://rss.example.com
curl.exe -I https://example.com
curl.exe -I https://example.com/notes/freshrss/
curl.exe -I https://www.example.com- FreshRSS 原订阅和阅读状态仍在,API 客户端能同步。
- 首页与至少一个已有页面保持原行为,新文章独立可读。
- www 按配置跳转,证书均有效。
- web 网络包含新 Caddy 和 FreshRSS;挂载仍为原数据卷。
若新 Caddy 无法正常服务,先停掉并移除新项目的 Caddy 容器,释放名称和端口,再启动保留的旧容器:
cd ~/services/caddy
sudo docker compose stop caddy
sudo docker compose rm -f caddy
sudo docker rename caddy-before-split caddy
sudo docker start caddy旧 Caddy 已在步骤 14.2 接入 web,因此可继续代理迁移后的 FreshRSS。若问题来自 FreshRSS 本身,再恢复 compose.before-split.yaml 为原 Compose,并在原目录以原项目名执行 docker compose up -d --no-deps freshrss。网络回滚同样可能短暂中断;任何一步都不删除数据卷。
Caddy 管理域名、TLS 与路由;FreshRSS 项目管理应用及数据;blog/html 保存网页。新增文章只变更对应子目录,新增应用再单独添加服务及入口规则。先完成迁移验收,再考虑升级镜像或其他结构调整。
15参考资料
故障事实来自原部署记录,已去除真实 IP、域名、账户及令牌。配置说明核对了以下官方资料;不同版本的界面和参数可能变化,执行前以所安装版本为准。