我先把话说在前面:RSS 这个古董协议,到现在依然是我获取信息的主干道。尤其 Google Reader 死掉之后,自托管 RSS 阅读器这件事反而成了刚需。Miniflux 是我测下来最适合个人和小团队长期跑的方案,Go 写的单二进制,部署不折腾,界面简洁到像一块白豆腐,但背后该有的功能一个不少。这篇文章不整虚的,就讲怎么用 Docker Compose 从零搭一套带高可用意味的 Miniflux,直接把思路、配置、坑和运维检查清单都给你。
1. 为什么选择 Docker Compose 承载 Miniflux,以及"高可用"的真实含义
在动手写 Compose 文件之前,得先把两个问题想明白:为什么是 Docker Compose,以及你自己对"高可用"的预期到底是什么。
1.1 从单机二进制到容器化的演进逻辑
Miniflux 官方其实给了很清爽的安装方式——一个 40MB 左右的二进制文件,配一个 PostgreSQL 就能跑。单机部署确实够轻,问题在于升级、备份恢复、迁移主机这几个环节,手动操作越多,事故概率越大。用 Docker Compose 本质上是把"进程管理、网络配置、依赖编排、环境变量注入"四件事用声明式文件一次性确定下来。同一套 Compose 文件在家里的 NAS 和云主机上可以保持完全一致的行为,这个一致性是运维爽感的主要来源。
Docker Compose 另外一个被忽略的价值是"依赖生命周期管理"。Miniflux 必须依赖 PostgreSQL,数据库如果起不来,应用容器就算启动了也会在健康检查里失败。Compose 的 depends_on 加上健康状态等待,能确保启动顺序可控。这比 systemd 守护三个独立服务要直观得多。
1.2 高可用是个分层问题
实话说,个人或者小型团队跑 RSS 阅读器,99% 的情况下用不上企业级的自动故障转移。但高可用也不是遥不可及的概念,落到这个场景,它至少分成三个层次:
- 数据不丢:PostgreSQL 数据有定期备份,且备份可以被验证恢复。
- 服务能重建:应用容器崩溃后能自动重启,而不是等人工介入。
- 访问可靠:数据库出现单点故障时,能切换到备用节点继续服务,或者至少保证从备份恢复的时间可控。
本文会覆盖第一层和第二层,第三层给出一种轻量的 PostgreSQL 主从切换方案,不是 Patroni 那种重量级全家桶,而是适合 1-2 台主机的务实做法。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 架构设计:从单容器到双节点 PostgreSQL 主从
既然要谈高可用,先别急着写代码。把架构图画在脑子里,比直接改 Compose 文件重要得多。
2.1 目标拓扑说明
整套系统由三类角色组成:
- Nginx 作为唯一入口,负责 TLS 终结、HTTP 到 HTTPS 跳转、反向代理到 Miniflux 容器。
- Miniflux 应用容器,可以水平扩展为多个实例,所有实例共享同一个 PostgreSQL。
- PostgreSQL 数据层,采用主从异步流复制,主库故障时手动/半自动触发切换。
这套拓扑的巧妙之处在于:Miniflux 本身是无状态应用,用户会话 token 和订阅数据全部存在数据库里,所以应用层可以随意扩缩容。数据库层的"主从"解决的是单点故障,而不是读写性能——RSS 阅读器的写入频率不高,一台从库用于只读备用已经很充裕。
2.2 持久化与网络规划
容器重启可以丢内存,但绝不能丢数据。所以数据卷要单独命名,绑定宿主机的固定目录,避免 Docker 自动创建的匿名卷在容器重建时被清理。网络部分我建议使用 Compose 内部网络,限制容器间互相暴露端口,只有 Nginx 需要对外监听 80/443。
端口分配上,内部网络不暴露 PostgreSQL 5432 给宿主机,只让 Miniflux 容器访问。Nginx 对外监听,Miniflux 和 PostgreSQL 一律不发布到宿主机,这在云服务器上能少很多扫描攻击的麻烦。
3. Compose 文件逐段拆解:每行配置都有它的道理
直接看生产可用的 docker-compose.yml,然后我逐段解释关键点。注意这里面删减了很多花哨的配置,只保留真正影响稳定性的部分。
yaml复制version: "3.8"
x-app-base: &app-base
restart: unless-stopped
networks:
- miniflux-net
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
environment:
TZ: "Asia/Shanghai"
services:
db-primary:
image: postgres:15-alpine
container_name: miniflux-db-primary
<<: *app-base
volumes:
- pg-primary-data:/var/lib/postgresql/data
- ./backup:/backup
environment:
POSTGRES_DB: miniflux
POSTGRES_USER: miniflux
POSTGRES_PASSWORD: ${DB_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U miniflux -d miniflux"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
command: |
postgres
-c wal_level=replica
-c max_wal_senders=10
-c wal_keep_size=128
-c hot_standby=on
-c archive_mode=on
-c archive_command=/bin/sh -c 'cp %p /backup/wal/%f'
app:
image: miniflux/miniflux:2.1.4
container_name: miniflux-app
<<: *app-base
depends_on:
db-primary:
condition: service_healthy
environment:
DATABASE_URL: postgres://miniflux:${DB_PASSWORD}@db-primary:5432/miniflux?sslmode=disable
RUN_MIGRATIONS: "1"
BASE_URL: https://rss.example.com
CREATE_ADMIN: "1"
ADMIN_USERNAME: ${ADMIN_USERNAME}
ADMIN_PASSWORD: ${ADMIN_PASSWORD}
POLLING_FREQUENCY: "15"
GIN_MODE: release
expose:
- "8080"
db-standby:
image: postgres:15-alpine
container_name: miniflux-db-standby
<<: *app-base
depends_on:
db-primary:
condition: service_healthy
volumes:
- pg-standby-data:/var/lib/postgresql/data
environment:
POSTGRES_DB: miniflux
POSTGRES_USER: miniflux
POSTGRES_PASSWORD: ${DB_PASSWORD}
command: |
bash -c '
until pg_basebackup -h db-primary -U repl -D /var/lib/postgresql/data -R -X stream -P
do
echo "Waiting for primary..."
sleep 5
done
echo "primary_conninfo = host=db-primary port=5432 user=repl password=${DB_PASSWORD} application_name=standby" >> /var/lib/postgresql/data/postgresql.auto.conf
exec postgres
'
nginx:
image: nginx:1.25-alpine
container_name: miniflux-nginx
<<: *app-base
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/nginx/certs:ro
depends_on:
- app
volumes:
pg-primary-data:
pg-standby-data:
networks:
miniflux-net:
driver: bridge
3.1 为什么选择 postgres:15-alpine 和 miniflux:2.1.4
PostgreSQL 15 是目前兼容性和新特性平衡最好的版本。Alpine 镜像小,但注意它用的 musl libc,某些扩展可能编译麻烦。Miniflux 这里我们不追最新,固定大版本确切的 minor 版本,避免某次升级带来意外迁移。2.1.4 是当前稳定线,RSS 解析和抓取逻辑经过长期验证。
3.2 从库启动脚本的"障眼法"
眼尖的读者会发现 db-standby 的启动命令有点绕。标准的 PostgreSQL 从库部署流程是:先 pg_basebackup 拉取主库全量数据,然后配置 standby.signal 和 primary_conninfo,最后启动 postgres。
我在容器里直接把这个流程揉进 command,好处是"容器一启动,自动完成初始化,不需要手动进入容器敲命令"。坏处是如果主库还没准备好,容器会反复重试 pg_basebackup。这段脚本在真实场景里非常稳,因为 pg_basebackup 本身就是幂等的,重试只会覆盖同一个目录。
3.3 healthcheck 的意义:解决"顺序启动"的假象
depends_on 如果不带 condition 条件,那只是启动顺序的约束,不是健康状态的约束。很多新手在这里踩坑:PostgreSQL 容器启动了,但还在初始化,Miniflux 就跑起来连数据库,报一堆连接拒绝。
所以我们给 db-primary 配了 pg_isready 健康检查,并且在 app 的 depends_on 里写明 condition: service_healthy。只有当 PostgreSQL 真正能接受连接,Miniflux 才会启动。这是 Compose 编排的精髓,也是很多教程忽略的细节。
4. PostgreSQL 高可用实战:主从、自动重连与切换
架构文档写起来轻松,真正让高可用落地是在故障切换这块。RSS 阅读器的数据虽然不像金融系统那样分秒必争,但订阅列表和阅读状态丢了,一样让人抓狂。
4.1 同步复制还是异步复制?
在 PostgreSQL 里有 synchronous_commit 和 synchronous_standby_names 两个参数控制同步级别。Miniflux 的写入场景是:插入文章、更新阅读状态、更新订阅关系。数据量不大,但对"不丢已读记录"有要求。
我在生产环境配置的是异步复制,然后靠一套定期备份兜底。原因很实在:同步复制在双节点场景下,如果从库宕机,主库会卡死在等待提交上,反而降低了可用性。异步复制如果发生主从切换,最多丢失最后一小段 WAL,对 RSS 阅读器来说,最多就是有几篇文章没来得及入库存入 WAL,影响微乎其微。
所以不要把"同步/异步"妖魔化,选型要跟着业务容忍度走。
4.2 数据库账号与安全配置
主从复制需要专门的复制账号,不能用 miniflux 这个业务账号去跑 pg_basebackup。
sql复制CREATE USER repl REPLICATION LOGIN PASSWORD '复制的密码';
GRANT CONNECT ON DATABASE miniflux TO repl;
在生产环境里,这个账号应该只允许从内网连接,所以我建议在 pg_hba.conf 里加一行:
code复制host replication repl db-primary scram-sha-256
从库的 primary_conninfo 里不要用超管账号,复制账号权限足够,风险最小化。
4.3 故障切换的完整演练过程
先说清楚,我没有在 Compose 里引入 Patroni 或者 etcd,因为对双节点而言太重了。下面的切换流程是半自动的,你需要手动在两台容器之间切换一次,但期间不会丢数据,操作链路清晰。
主库故障后的恢复流程:
- 确认主库确实不可用,而不是网络抖动。在从库容器里执行
pg_isready -h db-primary。 - 在从库容器里执行
pg_ctl promote。如果是容器,可以进容器执行pg_ctl promote -D /var/lib/postgresql/data,也可以touch /var/lib/postgresql/data/promote.signal来触发。 - 确认从库已经变成可写,执行
select pg_is_in_recovery();返回 f 表示已切主。 - 修改应用连接的 DATABASE_URL,把 db-primary 改成 db-standby,重启 app 容器。
- 原主库修复后,需要重新作为新主库的从库加入,而不是让它自动恢复成主库。这时候要清空原主库数据目录,重新 pg_basebackup 拉取新主库的数据。
整个流程大概 5-10 分钟,比一般备份恢复要快。这也是为什么我觉得小场景下,Patroni 不是必需品——自动化故障转移的复杂度,有时候比人工切换的风险还高。
4.4 备份策略:基础备份 + WAL 归档
写进 Compose 的 archive_command 把 WAL 归档到宿主机 /backup/wal 目录。有了 WAL,就可以做时间点恢复(PITR)。基础备份我用 cron 定时执行,每周日凌晨 2 点跑一次 pg_basebackup,连同 WAL 目录一起上传到对象存储。
这里有个血泪教训:备份脚本定时执行了三年,从没做过恢复演练。直到一次真的需要恢复时,发现备份文件损坏。所以请务必定期在测试环境验证备份可用性。我自己现在是用一个独立脚本做月度恢复演练,脚本逻辑很简单:起一个临时 PostgreSQL 容器,把基础备份和 WAL 灌进去,确认能正常启动和查询。
5. 应用层高可用:多实例水平扩展与反向代理
数据库层稳了,应用层就简单多了。Miniflux 无状态,理论上你可以把 app 服务水平扩展成 3 个副本。但这里有一个容易被忽略的问题:Miniflux 的抓取调度。
5.1 Miniflux 的抓取任务与多实例冲突
Miniflux 内部有一个调度器,负责定时抓取订阅源。默认情况下,每个实例都会执行自己的调度器。如果你跑 3 个实例,同一个订阅源可能被 3 个实例同时抓取。虽然 Miniflux 内部有去重机制,但会造成无谓的请求量,甚至被订阅源封 IP。
解决方案有几个方向:
- 只跑一个应用实例,不加副本。对 RSS 阅读器来说,单实例足够。
- 如果坚持要多个实例,就把外部调度器独立出来,用 Redis 锁或计划任务控制只有一个实例执行抓取。
- 把抓取频率调低,让重复抓取的影响降到最低。
我自己的选择是默认单实例,通过 nginx 负载均衡到多个实例的写法只留给那些订阅源特别多、抓取耗 CPU 的场景。
5.2 Nginx 配置要点
Nginx 在这里干的活比想象中多:TLS 终止、HTTP/2、Gzip 压缩、代理头设置。重点在这里的配置:
nginx复制upstream miniflux {
server app:8080;
keepalive 16;
}
server {
listen 80;
server_name rss.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name rss.example.com;
ssl_certificate /etc/nginx/certs/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/privkey.pem;
location / {
proxy_pass http://miniflux;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
client_max_body_size 2m;
gzip on;
gzip_types text/css application/javascript application/json;
}
proxy_set_header Connection "" 和 keepalive 16 是配合使用,让 Nginx 到后端的连接复用,减少 TCP 握手开销。Miniflux 的响应本身很小,这套配置足够丝滑。
5.3 HTTPS 证书轮换的自动化
certbot 的证书三个月一换,手动换太痛苦。我建议在宿主机上跑一个 certbot 容器,用 webroot 模式验证域名,证书生成后直接输出到挂载目录。由于 Compose 文件里的 nginx 已经挂载了 ./certs 目录,重启 nginx 容器即可生效。
实际维护中,我用 cron 每月跑一次 docker exec miniflux-nginx nginx -s reload,如果证书文件没变,reload 也不会影响现有长连接。别问我为什么不用 certbot 的 DNS 验证——HTTP 验证在通用场景下最简单可靠,不依赖 DNS 服务商的 API。
6. 环境变量管理与敏感信息保护
Compose 文件里我写了 ${DB_PASSWORD} 这种占位符,这是为了避免把密码硬编码进仓库。但环境变量文件本身也要注意安全。
6.1 使用 .env 文件隔离敏感配置
在项目根目录创建 .env 文件:
code复制DB_PASSWORD=你的数据库密码
ADMIN_USERNAME=admin
ADMIN_PASSWORD=初始化管理员密码
Compose 会自动读取 .env 文件填到变量里。注意 .env 文件不要提交到 Git 仓库,在 .gitignore 里加上 .env。
但 .env 有个问题:它是明文。如果你的服务器被入侵,整个 .env 就裸奔了。更进一步,可以用 Docker Secret 或类似 sops 的工具加密。在小规模场景下,把 .env 文件的权限设为 600,仅 root 可读,已经能防住大部分风险。
6.2 数据库密码轮换的最佳实践
PostgreSQL 的密码轮换不是简单改个 env 再重启容器。因为 Miniflux 连接数据库时用的是 DATABASE_URL 里的密码,改了密码后,旧的连接会全部断开。轮换的正确姿势:
- 在 PostgreSQL 里用 ALTER USER 修改密码。
- 同步修改 .env 和 Miniflux 容器的 DATABASE_URL。
- 先重启 app 容器,确认连接正常。
- 再重启 db 容器,确保主备都用了新密码。
顺序反了会导致应用短暂连不上数据库。
6.3 关于 RUN_MIGRATIONS 的坑
Miniflux 有一个 RUN_MIGRATIONS 环境变量,设为 1 会在启动时自动执行数据库迁移。听起来很方便,但在多实例场景里,如果两个实例同时启动,可能会发生迁移锁竞争。所以建议:扩容到多实例时,RUN_MIGRATIONS 只在一个实例上开启,或者在数据库层面设置 advisory lock。单实例场景倒是没这个问题,直接开就行。
7. 从零启动的完整步骤与验证清单
前面讲了这么多原理,这里给一份可以直接照抄的启动步骤。假设你已经有一台装了 Docker 的主机,域名也解析好了。
7.1 目录结构准备
在服务器上创建项目目录:
bash复制mkdir -p /opt/miniflux/{backup/wal,certs,nginx}
cd /opt/miniflux
创建 .env 文件,填入密码和管理员账号。申请好证书后,把 fullchain.pem 和 privkey.pem 放到 certs 目录。nginx.conf 用上面那一段。
7.2 启动数据库主库并初始化
这里有个细节,第一次启动 db-primary 时,从库的 pg_basebackup 脚本会因为没有主库而一直等待。所以稳妥的做法是:先只启动 db-primary,等它健康,再启动整个 Compose。
bash复制docker compose up -d db-primary
docker compose ps
确认 db-primary 状态为 healthy 后,启动其余服务:
bash复制docker compose up -d
第一次启动后,Miniflux 会自动执行迁移,并创建管理员账号。
7.3 从库初始化的手动兜底
如果 db-standby 因为各种原因没有同步成功,进容器手动执行一次 basebackup:
bash复制docker exec -it miniflux-db-standby bash
rm -rf /var/lib/postgresql/data/*
pg_basebackup -h db-primary -U repl -D /var/lib/postgresql/data -R -X stream -P
然后重启 db-standby 容器。
7.4 验证清单
启动完成后,按顺序验证这些点:
- 浏览器访问 https://rss.example.com,能打开登录页。
- 用管理员账号登录,能正常添加订阅源,且能拉到文章。
- 进入 db-primary 容器,创建一个测试订阅,等到自动同步,然后查从库是否有对应数据。
bash复制docker exec -it miniflux-db-primary psql -U miniflux -d miniflux -c "select count(*) from entries;"
docker exec -it miniflux-db-standby psql -U miniflux -d miniflux -c "select count(*) from entries;"
两个查询结果应该一致。从库显示相同行数,说明流复制工作正常。
- 看应用日志有没有报错:
bash复制docker compose logs app --tail 100
常见的报错集中在数据库连接、订阅源解析格式、OAuth 配置这几个地方。
8. 升级、备份恢复与运维检查清单
部署完成只是开始,长期运维才是真正拉开差距的地方。
8.1 Compose 版本升级的正确姿势
Miniflux 每次发版,我都会看 release notes,然后走这套流程:
- 备份数据库:pg_dump 整个库,存到宿主机 /backup/pre-upgrade.dump。
- 在测试环境用同样的 Compose 升级,确认迁移没有问题。
- 生产环境 pull 新镜像,docker compose up -d app,观察日志。
- 如果迁移失败,回滚镜像 tag,恢复到旧版本。
升级本身不可怕,可怕的是没备份就升。
8.2 备份恢复的完整实操
假设灾难发生,需要恢复数据。用基础备份和 WAL 来做时间点恢复:
bash复制# 在临时目录里解压基础备份
mkdir -p /tmp/pg_restore
tar xzf /backup/base_backup.tar.gz -C /tmp/pg_restore
# 把 WAL 文件放到 pg_wal 目录
cp /backup/wal/* /tmp/pg_restore/pg_wal/
# 在恢复配置里指定 recovery_target_time
echo "restore_command = 'cp /backup/wal/%f %p'" >> /tmp/pg_restore/postgresql.conf
echo "recovery_target_time = '2025-01-15 10:00:00'" >> /tmp/pg_restore/postgresql.conf
touch /tmp/pg_restore/recovery.signal
# 用这个目录启动临时容器
docker run -d --name pg-restore \
-v /tmp/pg_restore:/var/lib/postgresql/data \
-e POSTGRES_PASSWORD=temp \
postgres:15-alpine
容器起来后,连进去看数据是否完整。如果恢复失败,排查顺序是:基础备份是否完整、WAL 是否有缺口、recovery_target_time 是否设置正确。
8.3 日常巡检命令
我建议每月跑一次巡检,重点看这几个指标:
- 主从延迟:
select now() - pg_last_xact_replay_timestamp() as replay_lag; - 数据库连接数:
select count(*) from pg_stat_activity; - 容器内存使用:
docker stats --no-stream - 订阅源抓取失败率:Miniflux 后台的分析页面有抓取统计,重点关注持续失败的源。
8.4 日志轮转与磁盘清理
PostgreSQL 的 WAL 归档如果没清理,会把磁盘塞满。我的策略是:WAL 归档保留 7 天,基础备份保留 4 份(覆盖一个月),对象存储里再留一份冷备。宿主机上跑一个 cron 脚本,清理超过保留期限的文件。
bash复制find /backup/wal -type f -mtime +7 -delete
find /backup/base -type f -mtime +28 -delete
9. 我踩过的几个真实坑,以及对应的处理办法
最后这部分,就是纯经验了。不是教科书里的标准答案,是实际操作中被现实毒打之后的总结。
9.1 从库数据目录权限变成 root 导致启动失败
第一次手动跑 pg_basebackup 的时候,我是在容器外执行的,导致数据目录的所有者变成了 root,容器启动时 postgres 用户没有权限读。修复办法很简单:把容器内数据目录的所有者改回 postgres。
bash复制docker exec -it miniflux-db-standby chown -R postgres:postgres /var/lib/postgresql/data
遇到任何从库无法启动的问题,第一反应先看权限,往往能省很多时间。
9.2 Miniflux 订阅抓取超时导致"推送变慢"的错觉
有段时间我发现订阅源更新后,Miniflux 要好几个小时才能抓到。排查后发现是其中一个订阅源响应速度极慢,把调度器的并发抓取线程占满了。Miniflux 在设置里可以调整抓取并发度,默认可能是 5,调到 10 之后,整体抓取速度明显改善。但别调到太高,小心被订阅源封 IP。
9.3 Docker Compose 文件里忘了设置 TZ
时间错乱在日志分析时非常误导人。Compose 里的容器默认 UTC 时区,如果 Miniflux 的容器没设置 TZ 环境变量,订阅源的"最后抓取时间"在后台显示和本地时间相差 8 小时。排查问题时你会怀疑人生。现在我把 TZ 放到了公共锚点里,确保所有容器统一。这个细节真的救过我好几次。
9.4 升级 PostgreSQL 大版本之前,一定先读 release notes
PostgreSQL 15 到 16 的数据目录格式有变化,直接挂载旧数据目录会导致启动失败。我的做法是:升级大版本时,用 pg_dump_all 全量导出,然后在新容器里导入,而不是直接 mount 数据目录。虽然慢,但安全。
9.5 Nginx 容器重启后端口被占用的诡异现象
如果你在宿主机上又跑了别的 web 服务,Nginx 容器重启时偶尔会报 80 端口被占用。排查后发现是另一个服务残留了 socket 监听。后来我统一用 ss -lntp | grep :80 快速定位占用方,再处理。容器化部署最怕的是"宿主机的隐性问题",这类问题往往不在 Compose 文件里。
10. 这套方案还能怎么演进
如果你用了一年半载,觉得这套架构已经不够用了,往这几个方向扩展是顺理成章的:
- 把 PostgreSQL 换成 Patroni + etcd 的完整 HA 方案,实现自动故障转移。注意这会带来额外的复杂度,三台机器起步。
- 把 Miniflux 容器搬到 Kubernetes,用 Deployment 的 HPA 自动扩缩容。但对这个场景来说,有一种大炮打蚊子的感觉。
- 把备份从宿主机目录迁移到对象存储(S3/MinIO),用 WAL-G 做增量备份和恢复,恢复时间可以从小时级降到分钟级。
在我个人看来,RSS 阅读器这种低频工具,最理想的运维状态就是"配置一次,稳定跑一年,升级时花十分钟看一眼迁移日志"。现在这套 Docker Compose 方案已经陪我跑了两年多,期间经历过一次主机迁移、两次 PostgreSQL 升级、无数次的 Miniflux 小版本更新,每次都是平滑过渡。你现在照着搭一套,大概率也能收获同样的稳定感。
