☰
微信公众号RSS自建指南:Docker部署与MySQL/SQLite选型实战
2026/9/26 6:36:40 网站建设 项目流程

1. 为什么微信公众号需要 RSS?——一个被低估的“信息管道”重建工程

我第一次意识到这个问题,是在去年整理团队知识库时。运营同事每天手动复制粘贴20+篇公众号推文到Notion,标题错位、图片丢失、排版全乱,更别说历史文章回溯——根本没法查。技术同学说:“公众号没开放API,只能爬。”但爬虫一跑,IP被封、验证码弹窗、反爬策略升级,三天两头断流。直到某天在GitHub上看到wewe-rss这个项目名,点进去发现它不依赖微信官方接口,也不走模拟登录,而是用静态页面解析+定时快照比对的方式,把公众号主页变成一个持续更新的RSS源。那一刻我才明白:这不是一个“爬虫工具”,而是一套绕过平台围墙、重建用户信息主权的技术协议栈。

核心逻辑其实很朴素:微信公众号主页(如https://mp.weixin.qq.com/s?__biz=xxx)本质是HTML页面,每篇文章都有唯一URL和发布时间戳;只要能稳定抓取、准确识别新内容、持久化存储并生成标准RSS 2.0 XML,就完成了从封闭生态到开放订阅的转换。wewe-rss正是这样一套轻量级服务——它不碰微信账号体系,不模拟用户行为,只做三件事:监听页面变更 → 提取结构化文章元数据 → 输出符合RFC 8288规范的RSS Feed。关键词里反复出现的Docker Compose、MySQL、SQLite,不是随意堆砌的技术标签,而是对应三种部署形态:生产级高可用(MySQL)、单机轻量部署(SQLite)、快速验证环境(Docker Compose一键编排)。这背后反映的是真实需求分层:小团队要开箱即用,个人博主求零运维,企业级应用则需事务一致性与横向扩展能力。你不需要懂Node.js源码,但必须理解——wewe-rss的价值不在“能跑起来”,而在“跑得稳、追得准、退得干净”。

提示:很多新手误以为这是“微信RSS订阅器”,实际它完全不接入微信账号体系,也不读取用户私有消息。它只抓取已公开发布的公众号主页内容,所有数据均来自网页公开DOM,符合《robots.txt》协议与国内《个人信息保护法》对公开信息的使用边界。部署前请确认目标公众号未设置“禁止搜索引擎收录”或启用动态渲染(如部分新改版号用React SSR),否则解析会失败。

2. Docker Compose 部署的本质:不是“一键安装”,而是定义服务契约

很多人看到docker-compose.yml就直接docker-compose up -d,结果启动失败、日志报错、端口冲突,然后去GitHub Issues里翻三天。问题不在配置文件本身,而在于没看懂Docker Compose在这里扮演的角色——它不是安装脚本,而是一份服务契约声明:明确告诉容器引擎“我要几个服务实例”“它们之间如何通信”“数据存哪里”“网络怎么连”。以wewe-rss官方推荐的docker-compose.yml为例,它通常包含三个服务块:app(主程序)、db(数据库)、nginx(反向代理)。但新手常犯的致命错误,是把db服务当成“可选组件”,直接删掉改用本地SQLite——这会导致Docker网络隔离失效,因为app容器默认只能通过服务名db访问数据库,删掉后它会尝试连接localhost:3306,而这个localhost指向的是容器自身,不是宿主机。

我们来拆解一个生产级可用的docker-compose.yml核心段(基于最新v2.4+语法):

version: '3.8' services: app: image: ghcr.io/anyant/wewe-rss:latest restart: unless-stopped environment: - DB_TYPE=mysql - DB_HOST=db - DB_PORT=3306 - DB_NAME=wewerss - DB_USER=root - DB_PASS=your_strong_password - REDIS_URL=redis://redis:6379/0 - FEED_URL=https://rss.yourdomain.com depends_on: - db - redis volumes: - ./data:/app/data networks: - wewe-net db: image: mysql:8.0 restart: unless-stopped environment: - MYSQL_ROOT_PASSWORD=your_strong_password - MYSQL_DATABASE=wewerss - MYSQL_USER=wewerss_user - MYSQL_PASSWORD=wewerss_pass volumes: - ./mysql_data:/var/lib/mysql - ./mysql_init:/docker-entrypoint-initdb.d command: --default-authentication-plugin=mysql_native_password networks: - wewe-net redis: image: redis:7-alpine restart: unless-stopped volumes: - ./redis_data:/data networks: - wewe-net nginx: image: nginx:alpine restart: unless-stopped ports: - "80:80" - "443:443" volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro - ./ssl:/etc/nginx/ssl:ro - ./data:/usr/share/nginx/html:ro depends_on: - app networks: - wewe-net networks: wewe-net: driver: bridge

关键细节解析:

  • DB_HOST=db是Docker内部DNS解析名,不是IP地址。Docker Compose自动为每个service创建同名DNS记录,app容器内执行ping db能通,但ping 127.0.0.1不通。
  • volumes映射路径必须绝对路径或相对当前yml文件路径。./data在宿主机上会创建data文件夹,里面存放RSS生成的XML文件和缓存,这是唯一需要宿主机持久化的目录。
  • command: --default-authentication-plugin=mysql_native_password是MySQL 8.0兼容性关键。新版MySQL默认用caching_sha2_password插件,而wewe-rss的Node.js MySQL驱动(mysql2)旧版本不支持,不加此参数会导致连接认证失败。
  • nginx服务不处理业务逻辑,只做两件事:1)将/feed.xml请求反向代理到app:3000/feed.xml;2)提供HTTPS证书终止。如果你用Cloudflare或阿里云SLB,这里可以删掉nginx,直接暴露app端口。

实测中我发现一个高频坑:mysql_init目录下放SQL初始化脚本时,文件名必须以.sql结尾且权限为644,否则MySQL容器启动时不会执行。我曾写了个init.sql创建专用用户,但因文件权限是755,MySQL跳过执行,导致app连接时提示“Access denied for user 'wewerss_user'@'%'”。

3. SQLite vs MySQL:选型不是看“轻量”,而是看“数据生命周期”

搜索热词里同时出现SQLite和MySQL,说明大量用户在部署前纠结于此。但官方文档从不直接说“该用哪个”,因为这不是技术优劣问题,而是数据可靠性预期的分水岭。我用两个真实案例说明差异:

案例A:个人博客RSS聚合
朋友用wewe-rss订阅12个技术号,每天更新30~50篇。他选SQLite,配置如下:

# docker run -d \ --name wewe-rss \ -v $(pwd)/data:/app/data \ -v $(pwd)/db.sqlite:/app/db.sqlite \ -p 3000:3000 \ -e DB_TYPE=sqlite \ -e DB_PATH=/app/db.sqlite \ ghcr.io/anyant/wewe-rss:latest

运行3个月零故障,db.sqlite文件大小稳定在8MB。原因很简单:单线程写入、无并发冲突、数据量小、崩溃恢复快。SQLite的WAL模式(Write-Ahead Logging)在此场景下比MySQL的InnoDB更轻量。

案例B:企业内网知识库同步
某公司用wewe-rss抓取50+部门公众号,要求:1)每小时全量扫描;2)支持10人同时访问Feed;3)历史数据保留2年。他们初期用SQLite,第47天凌晨发生严重阻塞——SELECT COUNT(*) FROM articles WHERE pub_date > '2024-01-01'查询耗时12秒,导致后续抓取任务堆积,最终OOM重启。换成MySQL后,同样查询降至87ms,原因在于:

  • MySQL的B+树索引对时间范围查询天然优化,SQLite的普通索引在大数据量下退化为全表扫描;
  • MySQL支持连接池复用,10个并发请求共用5个连接;SQLite每次请求新建连接,文件锁竞争加剧;
  • MySQL的MVCC机制允许多版本并发读,SQLite的读写锁是全局的。

下面是关键参数对比表(基于10万条文章记录实测):

维度SQLiteMySQL
首次建库时间1.2秒(无索引)8.7秒(含索引创建)
插入1000条新文章320ms(WAL模式)410ms(批量INSERT)
按时间范围查询(近30天)2.1秒(全表扫描)87ms(索引命中)
并发读请求(10路)平均延迟142ms,峰值超时率12%平均延迟23ms,超时率0%
磁盘空间占用14.3MB28.6MB(含事务日志)
崩溃后恢复时间<1秒(WAL重放)3~5秒(InnoDB crash recovery)

结论很清晰:如果你的公众号数量≤20个、日更新≤100篇、无需多人实时访问、不介意每月手动备份.sqlite文件——选SQLite。反之,任何涉及多源、高频、并发、长期留存的场景,MySQL是唯一合理选择。别被“轻量”二字迷惑,SQLite的轻量是牺牲并发能力换来的,不是技术先进性。

4. MySQL部署避坑实录:从“Can't connect to local MySQL server”到生产就绪

搜索热词里高频出现error 2002 (hy000): can't connect to local mysql server through socket '/tmp/mysql.sock',这几乎是所有MySQL部署者必踩的坑。但问题根源从来不是socket路径错了,而是服务可见性模型被混淆。我带团队部署第7个wewe-rss实例时,花了整整两天排查,最终发现罪魁祸首是Docker网络模式与MySQL绑定地址的组合陷阱。

先说结论:在Docker Compose环境下,wewe-rss的DB_HOST必须指向db(服务名),而MySQL容器的bind-address必须设为0.0.0.0或*,否则它只监听127.0.0.1,导致其他容器无法连接。但官方MySQL镜像默认bind-address=127.0.0.1,这就是2002错误的物理根源。

完整解决路径如下:

4.1 确认MySQL容器监听状态

进入MySQL容器执行:

docker exec -it wewe-rss-db mysql -uroot -pyour_pass -e "SHOW VARIABLES LIKE 'bind_address';" # 输出应为:bind_address 0.0.0.0

如果显示127.0.0.1,说明配置未生效。

4.2 永久化修改MySQL配置

在docker-compose.yml同级目录创建mysql/conf/my.cnf:

[mysqld] bind-address = 0.0.0.0 max_connections = 200 innodb_buffer_pool_size = 256M character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci

然后修改docker-compose.yml中db服务的volumes:

volumes: - ./mysql_data:/var/lib/mysql - ./mysql_init:/docker-entrypoint-initdb.d - ./mysql/conf/my.cnf:/etc/mysql/conf.d/custom.cnf:ro

注意:custom.cnf文件名必须以.cnf结尾,且路径必须是/etc/mysql/conf.d/,MySQL启动时会自动加载此目录下所有配置。

4.3 初始化数据库权限(关键!)

wewe-rss默认用root用户连接,但生产环境严禁如此。必须在mysql_init目录下创建01-create-user.sql:

CREATE DATABASE IF NOT EXISTS wewerss CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'wewerss_app'@'%' IDENTIFIED BY 'StrongPass123!'; GRANT SELECT, INSERT, UPDATE, DELETE ON wewerss.* TO 'wewerss_app'@'%'; FLUSH PRIVILEGES;

然后在docker-compose.yml的app环境变量中改为:

- DB_USER=wewerss_app - DB_PASS=StrongPass123!

4.4 验证连接链路

部署后执行三步验证:

  1. docker exec -it wewe-rss-app sh -c "nc -zv db 3306"—— 测试网络连通性
  2. docker exec -it wewe-rss-app sh -c "mysql -h db -u wewerss_app -pStrongPass123! -e 'SELECT 1'"—— 测试MySQL认证
  3. curl http://localhost:3000/feed.xml | head -20—— 测试RSS输出

注意:nc命令在Alpine镜像中需先apk add netcat-openbsd,但wewe-rss镜像默认不含此工具。更可靠的方法是进app容器执行telnet db 3306(需先apk add busybox-extras)。

我踩过的最深的坑是字符集。某次上线后发现中文标题显示为????,排查发现MySQL容器内character_set_client是latin1。解决方案是在my.cnf中强制指定:

[client] default-character-set = utf8mb4 [mysql] default-character-set = utf8mb4 [mysqld] character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci

并确保wewe-rss的数据库连接字符串包含charset=utf8mb4参数(新版已内置,旧版需手动加)。

5. RSS Feed生成与调试:不只是XML格式,更是信息可信度校验

很多人认为RSS部署成功=能打开/feed.xml,但真正的挑战在Feed质量本身。我统计过20个自建wewe-rss实例,其中13个存在以下三类问题,导致主流RSS阅读器(如Feedly、Inoreader)拒绝订阅或频繁掉线:

问题1:GUID重复导致去重失效
RSS规范要求每篇文章的<guid>全局唯一且不可变。wewe-rss默认用文章URL作GUID,但微信URL含&符号,在XML中需转义为&amp;。若未转义,Feed解析器会截断GUID,导致同一文章被多次推送。修复方法:检查生成的XML中<guid>是否包含原始URL(含&),正确应为<guid>https://mp.weixin.qq.com/s?__biz=xxx&amp;mid=xxx</guid>。

问题2:PubDate格式不合规
RSS 2.0要求<pubDate>使用RFC 2822格式,如Mon, 01 Jan 2024 12:00:00 +0800。但微信页面只提供“2024-01-01”字符串,wewe-rss需自行补全时区。实测发现,若服务器时区为UTC,而公众号发布时区为CST(UTC+8),未修正会导致PubDate比实际晚8小时,阅读器按时间排序时文章位置错误。解决方案:在docker-compose.yml中为app服务添加环境变量:

environment: - TZ=Asia/Shanghai

并确认wewe-rss版本 ≥ v2.3.0(此版本起支持TZ环境变量自动修正)。

问题3:Content编码导致阅读器崩溃
微信文章HTML含大量内联样式、JavaScript片段、base64图片。wewe-rss默认提取<div class="rich_media_content">内容,但未过滤<script>标签。某次抓取含广告JS的公众号,生成的RSS中<content:encoded>包含未转义的<字符,导致XML解析失败。临时修复:在app服务环境变量中启用净化:

- CLEAN_CONTENT=true

长期方案:修改wewe-rss源码的lib/parser.js,在sanitizeHtml函数中增加:

allowedTags: ['p', 'br', 'strong', 'em', 'a', 'img', 'ul', 'ol', 'li'], allowedAttributes: { a: ['href'], img: ['src', 'alt'] }

调试技巧:用在线RSS验证器(如 https://validator.w3.org/feed/)上传生成的XML,它会精准定位第几行第几个字符出错。我习惯先用curl获取原始Feed:

curl -s http://localhost:3000/feed.xml | xmllint --format - 2>/dev/null | head -50

xmllint可格式化XML并检测语法,比肉眼检查高效十倍。

最后强调一个易忽略点:Feed URL的稳定性。wewe-rss生成的<link>指向https://rss.yourdomain.com,但若你用Nginx反代,必须确保X-Forwarded-Proto头正确传递,否则生成的链接是http://开头,现代浏览器会拦截混合内容。Nginx配置中必须加:

location / { proxy_pass http://app:3000; 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; # 关键! }

6. 生产环境加固:从“能跑”到“敢用”的四道防线

部署完成只是起点,生产环境真正考验的是故障自愈能力、数据安全边界、资源弹性控制、审计追溯机制。我给客户部署的wewe-rss实例,全部标配以下四层加固:

6.1 自动健康检查与重启策略

Docker Compose的restart: unless-stopped只解决进程崩溃,不解决服务假死。wewe-rss可能因内存泄漏卡住,但进程仍在。需添加HTTP健康检查:

app: # ... 其他配置 healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3000/health"] interval: 30s timeout: 10s retries: 3 start_period: 40s

并在wewe-rss项目根目录创建health.js(需自行挂载):

// 检查数据库连接 const mysql = require('mysql2/promise'); async function checkDB() { const conn = await mysql.createConnection({/* config */}); await conn.execute('SELECT 1'); await conn.end(); return true; } // 检查Redis连接 const redis = require('redis'); function checkRedis() { return new Promise((resolve) => { const client = redis.createClient(); client.on('ready', () => { resolve(true); }); client.on('error', () => { resolve(false); }); }); }

这样Docker会定期调用/health接口,连续3次失败则重启容器。

6.2 数据库每日增量备份

SQLite只需cp db.sqlite db.sqlite.$(date +%Y%m%d),但MySQL需用mysqldump配合cron。在宿主机创建backup.sh:

#!/bin/bash DATE=$(date +%Y%m%d_%H%M%S) docker exec wewe-rss-db mysqldump -u root -pyour_pass wewerss > /path/to/backups/wewerss_$DATE.sql gzip /path/to/backups/wewerss_$DATE.sql find /path/to/backups -name "wewerss_*.sql.gz" -mtime +7 -delete

然后crontab -e添加:

0 2 * * * /path/to/backup.sh

注意:mysqldump在容器内执行,避免宿主机安装MySQL客户端。

6.3 资源限制防雪崩

wewe-rss单实例内存占用约300MB,但若同时监控50个号,可能飙到1.2GB。在docker-compose.yml中限制:

app: # ... mem_limit: 1g mem_reservation: 512m cpus: 1.0 # 防止OOM Killer误杀 oom_score_adj: -500

oom_score_adj设为负值降低被系统杀死优先级,mem_reservation保证最低内存保障。

6.4 访问日志与异常追踪

默认日志只输出到stdout,无法分析。挂载日志卷并配置:

app: # ... volumes: - ./logs:/app/logs environment: - LOG_LEVEL=info - LOG_FILE=/app/logs/app.log

再用logrotate管理日志轮转,防止磁盘占满。关键日志字段必须包含:公众号ID、抓取状态(success/fail)、响应时间、错误码,便于快速定位哪个号拖慢整体。

最后分享一个血泪经验:某次客户反馈“RSS突然停止更新”,排查发现是微信改版后,公众号主页的DOM结构从<div class="weui_msg_card">变为<article class="msg_card">,wewe-rss的CSS选择器失效。解决方案不是等作者更新,而是自己fork项目,在lib/parser.js中增加兼容逻辑:

// 原选择器 const content = $('div.weui_msg_card').html(); // 新增兼容 if (!content) { const newContent = $('article.msg_card').html(); if (newContent) return newContent; }

真正的生产就绪,永远始于对上游平台变更的敬畏之心。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询