---
title: 使用 Docker 搭建 miniflux 和 RSSHub，重建资讯订阅体系
date: 2019-12-07T23:30:00+08:00
tags:
  - Docker
  - RSS
  - 技术

---

去年开始博主我大致确立了一个以 RSS 聚合为主，其它信源随缘查看的[资讯订阅流程](https://blog.izgq.net/archives/1216/)。其中一大需求是同步不同客户端的阅读记录，需要一个服务器端运行的订阅器。当时用的是 Tiny Tiny RSS 和它的 fever 插件，结合 tt-rss 安卓客户端和 Reeder 实现。

忙起来之后，碎片信息积攒得多了，有点超载。闲暇时间，也多消耗在读书类的 APP 和微信的公众号、看一看这类身边人分享的信息之中。加之手上服务器多了，这一套东西也无暇维护，不值得投入精力在反反复复的安装和修改配置上。最近这段时间也逐渐把手头的各个网站都迁移到 Docker，到了 tt-rss 这个站，想不清过去在这里魔改过什么，索性整个服务器重装了，这一套体系被我再次摧毁。

经过这样的建立、崩溃、重组、再崩溃的经历，RSS 这一工具在我的角度的使用场景也明确到了两个字：**必读**。RSS 满足了从关注的人、关注的领域开始发散的树形结构，与微信构筑的特有的以人的关系网为中心、不刻意的“口耳相传”这样的圈层结构相结合，形成我目前的资讯的主要来源。

RSS 这一块，进一步梳理之后可以得到以下需求：

- 由于阅读不局限于一个地方，tt-rss 的强大功能反而更不适合，需尽可能地小巧精悍
- 获得的信息不局限于日常所见，要穿过检查站，不能被 spy 和 filter，传输层加密是必要的
- 不想在运维上花太多精力，希望安装配置有记录，方便后续的迁移和维护

对应的解决方案：

1. miniflux：恰到好处的 Web 端阅读器，更新勤快，效率高，支持与各种服务对接。
2. 域名 + HTTPS：满足加密的需求，Let's Encrypt 有免费的 HTTPS 证书
3. Docker + Docker Compose：一次配置，一键部署
4. RSSHub：消息源格式标准化，以便统一订阅

## 操作记录

基础环境：Ubuntu 18.04 + [Docker CE](https://docs.docker.com/install/linux/docker-ce/ubuntu/) + [Docker Compose](https://docs.docker.com/compose/)

此处假设读者对 Docker 和 Docker Compose 均有一定了解（能用它跑起想要的服务）。

### 1. 配置 Web 服务器

这里的关键是如何配置一个支持 HTTPS 的接入的“网关”，以 Nginx 作为网站的总入口。Nginx 有配套的 Docker 镜像，我们只需要拉取镜像，然后加载一些自定义配置即可。

Nginx 配置的核心在于 `nginx.conf`，这里我结合 Docker 镜像内部的配置，最后沉淀下来的配置目录结构如下：

```
site-env
├── nginx-config        # Nginx 自定义配置
|   ├── conf.d     # 各个站点的配置文件
|   ├── snippets   # 代码片段
|   ├── ssl        # HTTPS 证书相关文件
|   └── nginx.conf # 覆盖 Docker 容器初始的配置入口
├── logs                # 存放日志文件
└── docker-compose.yml  # Docker Compose 配置文件
```

首先在 `docker-compose.yml` 配置一个 Nginx 服务：

```
version: "3.6"

services:
  nginx:
    image: nginx:1.17-alpine
    container_name: docker-nginx
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./logs:/var/log/nginx
      - ./nginx-config/nginx.conf:/etc/nginx/nginx.conf
      - ./nginx-config/conf.d:/etc/nginx/conf.d
      - ./nginx-config/snippets:/etc/nginx/snippets
      - /etc/nginx/ssl:/ssl
      - ./nginx-config/ssl/dhparam.pem:/ssl/dhparam.pem
      # - /var/www/websites:/wwwroot
    extra_hosts:
      - "localhost:127.0.0.1"
    healthcheck:
      test: ["CMD-SHELL", "wget -q --spider --proxy off http://localhost/get-health || exit 1"]
      interval: 5s
      retries: 12
    logging:
        driver: "json-file"
        options:
            max-size: "100m"
```

几点关键：

1. container_name 字段，这一字段指定生成的容器的名字为 `docker-nginx`，在证书签发的流程会用上。
2. volumes 的 `dhparam.pem`，自定义 Diffie-Hellman 密钥交换参数，增强安全性
3. /etc/nginx/ssl 指定宿主机证书保存位置，与容器内 /ssl 路径绑定，内部配置会在此查找证书

然后还需创建宿主机保存证书的目录：

```
$ mkdir /etc/nginx
$ mkdir /etc/nginx/ssl
```

生成 dhparam.pem：

```
$ cd nginx-conf/ssl/
$ sh generate-dhparam.sh
```

稍等片刻生成完毕，拉取镜像、启动 Nginx 服务器。

```
$ docker-compose up -d
```

这一步涉及到的配置已整理到 GitHub 仓库 [zgq354/docker-nginx-env](https://github.com/zgq354/docker-nginx-env)，更多细节参考仓库的内容。

### 2. 域名和证书的签发

我这里使用的域名的 DNS 绑在 Cloudflare，用一键脚本 [acme.sh](https://acme.sh/) 的 DNS 验证方式，签发支持泛域名 HTTPS 证书（假设这里域名为 domain.com）。

```
$ export CF_Key="***"
$ export CF_Email="xxxxx@gmail.com"
$ acme.sh --issue --dns dns_cf -d domain.com -d *.domain.com
```

待验证通过即可。

### 3. 安装证书

调用 acme.sh 的安装证书指令，把安装后的 `reload-cmd` 设为重启 Nginx 容器：

```
acme.sh --installcert -d example.com \
        --key-file   /etc/nginx/ssl/example.com.key \
        --fullchain-file /etc/nginx/ssl/fullchain.cer \
        --reloadcmd  "docker restart docker-nginx"
```

到了这一步，所有的 HTTPS 服务需要的证书已经准备好了，容器里面的 `/ssl/example.com.key` 和 `/ssl/fullchain.cer` 与其一一对应。

在继续下一步之前，确认已在 DNS 添加需要的域名解析记录。

### 4. 进一步配置需要的服务

结合第一步的目录结构，按照 Nginx 的姿势，给出写下 server 配置，放在 `nginx-conf/conf.d/` 之下。

关于配置的内容，这里需要额外引入 `ssl_certificate` 和 `ssl_certificate_key`，抽提出的其它参数放在了 `snippets/ssl-params.conf`，也需一并引入，参考：

```
server {
    listen 443 ssl http2;

    ssl_certificate /ssl/fullchain.cer;
    ssl_certificate_key /ssl/example.com.key;

    include snippets/ssl-params.conf;

    server_name rss.example.com;
    server_tokens off;

    access_log /var/log/nginx/wbrss-access.log;
    error_log /var/log/nginx/wbrss-error.log;

    location / {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header X-NginX-Proxy true;
        proxy_pass http://service.weiborss:3000/;
    }
}
```

按以上配置，在[SSL Labs 的测试中](https://www.ssllabs.com/ssltest/)可以获得 A+ 的评级。

![SSL Labs 报告](https://cdn.zgq.me/assets/posts/2019/docker-miniflux-rsshub/4261389845.png)

Nginx 外的服务则可以通过在 `docker-compose.yml` 里面加入新的内容来配置。然后 `docker-compose up -d` 重新启动就会自动拉取镜像跑起来了。这里我配置了 miniflux, RSSHub, weibo-rss 三个站点，以及它们各自依赖的服务：

`docker-compose.yml` 参考：

```
version: "3.6"

services:
  nginx:
    image: nginx:1.17-alpine
    container_name: docker-nginx
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./logs:/var/log/nginx
      - ./nginx-config/nginx.conf:/etc/nginx/nginx.conf
      - ./nginx-config/conf.d:/etc/nginx/conf.d
      - ./nginx-config/snippets:/etc/nginx/snippets
      - /etc/nginx/ssl:/ssl
      - ./nginx-config/ssl/dhparam.pem:/ssl/dhparam.pem
      # - /var/www/websites:/wwwroot
    extra_hosts:
      - "localhost:127.0.0.1"
    healthcheck:
      test: ["CMD-SHELL", "wget -q --spider --proxy off http://localhost/get-health || exit 1"]
      interval: 5s
      retries: 12
    logging:
        driver: "json-file"
        options:
            max-size: "100m"
  miniflux:
    image: miniflux/miniflux:latest
    expose:
      - 8080
    depends_on:
      - db
    environment:
      - DATABASE_URL=postgres://miniflux:secret@db/miniflux?sslmode=disable
      - RUN_MIGRATIONS=1
      - CREATE_ADMIN=1
      - BASE_URL=https://rss.example.com/
      - ADMIN_USERNAME=admin
      - ADMIN_PASSWORD=adminpassword
  db:
    image: postgres:latest
    environment:
      - POSTGRES_USER=miniflux
      - POSTGRES_PASSWORD=secret
    volumes:
      - miniflux-db:/var/lib/postgresql/data

  service.rsshub:
    image: diygod/rsshub
    restart: always
    environment:
      NODE_ENV: production
      CACHE_TYPE: redis
      REDIS_URL: 'redis://db.redis:6379/'
      PUPPETEER_WS_ENDPOINT: 'ws://service.browserless:3000'
    depends_on:
      - db.redis
      - service.browserless

  service.browserless:
    image: browserless/chrome
    restart: always

  service.weiborss:
    image: zgq354/weibo-rss
    restart: always

  db.redis:
    image: redis
    restart: always
    volumes:
        - redis-data:/data
volumes:
  miniflux-db:
  redis-data:
```

nginx-conf/conf.d/rss.conf

```
# miniflux
server {
    listen 443 ssl http2;

    ssl_certificate /ssl/fullchain.cer;
    ssl_certificate_key /ssl/example.com.key;

    include snippets/ssl-params.conf;

    server_name rss.example.com;
    server_tokens off;
    access_log /var/log/nginx/rss-access.log;
    error_log /var/log/nginx/rss-error.log;

    location / {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header X-NginX-Proxy true;
        proxy_pass http://miniflux:8080/;
    }
}

# rsshub.example.com
server {
    listen 443 ssl http2;

    ssl_certificate /ssl/fullchain.cer;
    ssl_certificate_key /ssl/example.com.key;

    include snippets/ssl-params.conf;

    server_name rsshub.example.com;
    server_tokens off;
    access_log /var/log/nginx/rsshub-access.log;
    error_log /var/log/nginx/rsshub-error.log;

    location / {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header X-NginX-Proxy true;
        proxy_pass http://service.rsshub:1200/;
    }
}

# weibo-rss
server {
    listen 443 ssl http2;

    ssl_certificate /ssl/fullchain.cer;
    ssl_certificate_key /ssl/example.com.key;

    include snippets/ssl-params.conf;

    server_name wbrss.example.com;
    server_tokens off;
    access_log /var/log/nginx/wbrss-access.log;
    error_log /var/log/nginx/wbrss-error.log;

    location / {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header X-NginX-Proxy true;
        proxy_pass http://service.weiborss:3000/;
    }
}
```

最终达成的效果：

![docker ps](https://cdn.zgq.me/assets/posts/2019/docker-miniflux-rsshub/3423803091.png)

## 总结

经过一年来的折腾，我之前总结的关于资讯订阅的姿势，大的方向没什么问题，需要调整的大都是一些细节。RSS Feed 是资讯源在数学角度最本质的抽象，但生活是具体的，我们更多时候是需要结合生活的节奏，调整其中的各种实现细节，实现我们需要的效果。

## 参考

1. [miniflux 配置文档](https://miniflux.app/docs/configuration.html)
2. [RSSHub 部署文档](https://docs.rsshub.app/install/#an-zhuang)
3. [使用 Docker 搭建你自己的 RSS 服务（Miniflux） - 掘金](https://juejin.im/post/5c46a1bbe51d455bd36b71a1)
4. [用 miniflux 自建 RSS 服务的指南 | Chinglish Small Talk](https://ofey404.github.io/2019/07/18/miniflux-rss-handbook/)
