stacks/miniflux: self-hosted RSS reader + News group on homepage
Adds Miniflux on ana-docker as the unified inbox for tech blogs, Hacker News, lobste.rs, and selected subreddits. Reddit serves clean RSS for any sub at https://reddit.com/r/<sub>/.rss, so subreddit follows fold into the same inbox as everything else — no Reddit account needed, no manual polling. Stack: stacks/miniflux/ compose.yaml — miniflux + bundled postgres:16 .env.example — placeholders for DB password + admin user starter-feeds.opml — initial subscriptions (HN, Lobste.rs, r/selfhosted, r/homelab, r/LocalLLaMA, r/nba) README.md — deploy / OPML import / r/nba spoiler block-list / backup / update flow Postgres bundled with the stack (not pfi-postgres) — single-user RSS DB is tiny and the bundle keeps the dependency graph flat. Homepage gets a new 'News' group at the TOP of the Main tab (above Monitoring) so the Miniflux card sits prominently. The card itself auto-discovers via the homepage.* labels on the miniflux container. Per-feed block-list rule for r/nba documented in README — Reddit's RSS titles for game threads include scores ("Lakers 108 - Warriors 102 [Final]") which spoil the game; a regex catches the score patterns and skips those entries while keeping discussion/highlights. Deploy: scripts/elway ana-docker --playbook playbooks/deploy-miniflux.yaml Then edit /opt/docker/compose/miniflux/.env on the host to fill in the two CHANGE_ME passwords and `docker compose up -d` again.
This commit is contained in:
@@ -35,6 +35,11 @@ useEqualHeights: true
|
||||
# Row counts target ~4-per-row so dense groups (Apps, Service Networking)
|
||||
# read as a grid instead of an endless column.
|
||||
layout:
|
||||
News:
|
||||
icon: mdi-rss
|
||||
tab: Main
|
||||
style: row
|
||||
columns: 4
|
||||
Monitoring:
|
||||
icon: mdi-chart-line
|
||||
tab: Main
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# Deploy Miniflux + bundled Postgres to ana-docker.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/elway ana-docker --playbook playbooks/deploy-miniflux.yaml
|
||||
#
|
||||
# Idempotent — every step is creates-/when-gated; rerun is safe.
|
||||
#
|
||||
# After this playbook runs once, fill in the two CHANGE_ME passwords
|
||||
# in /opt/docker/compose/miniflux/.env on the host (see README) and
|
||||
# re-run `docker compose up -d` to start the stack with the real
|
||||
# passwords. The first-run CREATE_ADMIN=1 then bootstraps the admin
|
||||
# user on the empty DB.
|
||||
|
||||
vars:
|
||||
compose_dir: /opt/docker/compose/miniflux
|
||||
host_port: "8080"
|
||||
|
||||
steps:
|
||||
- name: Ensure compose dir exists
|
||||
shell: mkdir -p {{ compose_dir }}
|
||||
creates: "{{ compose_dir }}"
|
||||
|
||||
- name: Upload compose.yaml
|
||||
upload:
|
||||
src: stacks/miniflux/compose.yaml
|
||||
dest: "{{ compose_dir }}/compose.yaml"
|
||||
mode: "0644"
|
||||
|
||||
- name: Upload starter-feeds.opml (for in-UI import)
|
||||
upload:
|
||||
src: stacks/miniflux/starter-feeds.opml
|
||||
dest: "{{ compose_dir }}/starter-feeds.opml"
|
||||
mode: "0644"
|
||||
|
||||
- name: Seed .env from template (only if absent)
|
||||
upload:
|
||||
src: stacks/miniflux/.env.example
|
||||
dest: "{{ compose_dir }}/.env"
|
||||
mode: "0644"
|
||||
when: "[ ! -f {{ compose_dir }}/.env ]"
|
||||
|
||||
# ── compose pull + bring up ─────────────────────────────────────────
|
||||
|
||||
- name: docker compose pull (~250 MB total — postgres + miniflux)
|
||||
shell: |
|
||||
set -o pipefail
|
||||
cd {{ compose_dir }} && docker compose pull --quiet 2>&1 \
|
||||
| grep -vE '^\s*[0-9a-f]{8,16}\s+(Extracting|Pull complete|Pulling fs layer|Verifying Checksum|Download complete|Waiting|Already exists|Downloading|Pulling from)'
|
||||
|
||||
- name: docker compose up -d
|
||||
shell: cd {{ compose_dir }} && docker compose up -d
|
||||
|
||||
- name: Wait for /healthcheck to respond (or report .env passwords still placeholder)
|
||||
shell: |
|
||||
for i in $(seq 1 30); do
|
||||
# Miniflux returns 200 on /healthcheck once running.
|
||||
if curl -sf -o /dev/null --max-time 3 http://localhost:{{ host_port }}/healthcheck; then
|
||||
exit 0
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
# Don't fail hard — first run with placeholder passwords will
|
||||
# never come up healthy. Tell the operator what to do.
|
||||
echo
|
||||
echo "Miniflux didn't come healthy in 150 s — likely cause:"
|
||||
echo " /opt/docker/compose/miniflux/.env still has CHANGE_ME passwords."
|
||||
echo " Edit them (see stacks/miniflux/README.md) and re-run docker compose up -d."
|
||||
exit 0
|
||||
changed_when: "false"
|
||||
|
||||
verify:
|
||||
- name: Container miniflux exists
|
||||
shell: docker inspect miniflux --format '{{.State.Status}}' | grep -qE 'running|created|restarting'
|
||||
changed_when: "false"
|
||||
|
||||
- name: Container miniflux-db exists
|
||||
shell: docker inspect miniflux-db --format '{{.State.Status}}' | grep -qE 'running|created|restarting'
|
||||
changed_when: "false"
|
||||
|
||||
- name: Both containers on miniflux-net
|
||||
shell: docker inspect miniflux miniflux-db --format '{{json .NetworkSettings.Networks}}' | grep -q miniflux-net
|
||||
changed_when: "false"
|
||||
@@ -0,0 +1,37 @@
|
||||
# Miniflux stack tunables. Copy to `.env` on ana-docker before deploying
|
||||
# and fill in the two passwords below.
|
||||
|
||||
# ── image pin ────────────────────────────────────────────────────────
|
||||
# Pin to a tagged release. Bump deliberately — Miniflux ships breaking
|
||||
# config changes occasionally (rare, but they exist).
|
||||
MINIFLUX_VERSION=2.2.5
|
||||
|
||||
# ── network ──────────────────────────────────────────────────────────
|
||||
# Host port. Container listens on 8080 internally.
|
||||
# Reserved on ana-docker: 5001 Dockge, 7878 task-board, 8000 (other
|
||||
# stacks), 8025/8088/8090/8100/8380/8780 (various). 8080 free.
|
||||
MINIFLUX_PORT=8080
|
||||
|
||||
# Bind address. 0.0.0.0 exposes on all host interfaces; 127.0.0.1
|
||||
# would restrict to ana-docker-local only.
|
||||
MINIFLUX_BIND=0.0.0.0
|
||||
|
||||
# ── database (postgres bundled with this stack) ──────────────────────
|
||||
# Strong password for the miniflux postgres user. Generate fresh:
|
||||
# openssl rand -base64 24
|
||||
# This password is only seen by the two containers in this stack
|
||||
# (miniflux and miniflux-db share an internal docker network) so
|
||||
# rotation isn't urgent, but don't reuse another service's password.
|
||||
MINIFLUX_DB_PASSWORD=CHANGE_ME
|
||||
|
||||
# ── admin user (auto-created on first start) ─────────────────────────
|
||||
# CREATE_ADMIN=1 in compose.yaml means the container will create this
|
||||
# user the first time it starts against an empty DB. Subsequent starts
|
||||
# are no-ops on this. Change the password from inside the UI later if
|
||||
# you want — this var only matters for the initial bootstrap.
|
||||
MINIFLUX_ADMIN_USERNAME=lkraven
|
||||
MINIFLUX_ADMIN_PASSWORD=CHANGE_ME
|
||||
|
||||
# ── logging ──────────────────────────────────────────────────────────
|
||||
# error / warning / info / debug
|
||||
MINIFLUX_LOG_LEVEL=info
|
||||
@@ -0,0 +1,120 @@
|
||||
# Miniflux
|
||||
|
||||
Minimal self-hosted RSS reader ([miniflux.app](https://miniflux.app/),
|
||||
Apache 2.0, ~30 MB Go single-binary). One unified inbox for tech
|
||||
blogs, Hacker News, Lobste.rs, and selected subreddits.
|
||||
|
||||
## Why this stack
|
||||
|
||||
The fleet doesn't have a feed reader yet. Reddit subreddits expose
|
||||
clean RSS feeds at `https://reddit.com/r/<sub>/.rss`, so subscribing
|
||||
to your relevant subs alongside HN / blogs gives you a single place
|
||||
to scan recent activity — no Reddit account needed, no polling
|
||||
multiple sites by hand.
|
||||
|
||||
Self-hosted on `ana-docker` (10.250.50.70). Postgres bundled in the
|
||||
stack rather than sharing pfi-postgres — keeps the dependency graph
|
||||
flat and the DB is tiny (single-user).
|
||||
|
||||
## Deploy
|
||||
|
||||
```bash
|
||||
scripts/elway ana-docker --playbook playbooks/deploy-miniflux.yaml
|
||||
```
|
||||
|
||||
Cold deploy budget: ~250 MB image pull (postgres:16 + miniflux),
|
||||
~10 s startup, ~1 s warmup.
|
||||
|
||||
After deploy, the .env on ana-docker still has placeholder passwords
|
||||
— **edit those before first start succeeds**:
|
||||
|
||||
```bash
|
||||
ssh ana-docker '
|
||||
cd /opt/docker/compose/miniflux
|
||||
sed -i "s|^MINIFLUX_DB_PASSWORD=.*|MINIFLUX_DB_PASSWORD=$(openssl rand -base64 24)|" .env
|
||||
sed -i "s|^MINIFLUX_ADMIN_PASSWORD=.*|MINIFLUX_ADMIN_PASSWORD=$(openssl rand -base64 18)|" .env
|
||||
grep ^MINIFLUX_ADMIN_PASSWORD .env # one-time copy this for first login
|
||||
docker compose up -d
|
||||
'
|
||||
```
|
||||
|
||||
Then login at <http://10.250.50.70:8080> with the username from `.env`
|
||||
(`lkraven` by default) and the printed admin password.
|
||||
|
||||
## Initial feed setup — OPML import
|
||||
|
||||
A starter OPML at `stacks/miniflux/starter-feeds.opml` covers:
|
||||
- Hacker News + Lobste.rs
|
||||
- r/selfhosted, r/homelab, r/LocalLLaMA
|
||||
- r/nba (with spoiler caveat — see below)
|
||||
|
||||
Import via Miniflux UI: **Settings → Import → Choose File** → select
|
||||
the OPML → Submit. All feeds appear in their categories; first
|
||||
refresh fetches the most recent posts within seconds.
|
||||
|
||||
## r/nba spoiler block-list
|
||||
|
||||
Subreddit-RSS titles for game-day threads include final scores
|
||||
(e.g. `Lakers 108 - Warriors 102 [Final]`). Miniflux supports
|
||||
per-feed regex block rules to skip these.
|
||||
|
||||
After importing the OPML:
|
||||
|
||||
1. Go to **Feeds** → click **r/nba** → **Edit Feed**
|
||||
2. In the **Block Filter** field, paste:
|
||||
|
||||
```
|
||||
\b\d{2,3}\s*[-–]\s*\d{2,3}\b|\[Final|Final Score|Game Thread.*\d{2,3}
|
||||
```
|
||||
|
||||
3. Save. Past matching entries stay (they were already fetched);
|
||||
future fetches skip score-bearing headlines. Discussion threads,
|
||||
highlights, and meta-content still come through.
|
||||
|
||||
If a real post slips through with a score in the title, refine the
|
||||
regex.
|
||||
|
||||
## Adding subreddits later
|
||||
|
||||
Reddit RSS URL patterns:
|
||||
|
||||
| URL | Returns |
|
||||
|---|---|
|
||||
| `https://reddit.com/r/<sub>/.rss` | Newest posts (firehose) |
|
||||
| `https://reddit.com/r/<sub>/top/.rss?t=day` | Top-of-day (less spam) |
|
||||
| `https://reddit.com/r/<sub>/top/.rss?t=week` | Top-of-week |
|
||||
| `https://reddit.com/r/<sub>/hot/.rss` | Currently hot |
|
||||
|
||||
`top/.rss?t=day` is the lowest-noise option for high-volume subs
|
||||
where you want highlights only.
|
||||
|
||||
In Miniflux: **Feeds → New Feed → URL**, paste, pick a category.
|
||||
|
||||
## Backup
|
||||
|
||||
Miniflux state lives in the named volume `miniflux-db-data`. To
|
||||
include in restic, add a `pre-backup.sh` step on ana-docker that
|
||||
dumps the DB to a stage path:
|
||||
|
||||
```bash
|
||||
docker exec miniflux-db pg_dump -U miniflux miniflux \
|
||||
> /var/lib/restic/stage/miniflux.sql
|
||||
```
|
||||
|
||||
Then restic's regular sweep picks up the SQL file. Subscriptions and
|
||||
read-state are tiny (~MB even with months of history); the dump is
|
||||
fast.
|
||||
|
||||
## Updating
|
||||
|
||||
```bash
|
||||
ssh ana-docker '
|
||||
cd /opt/docker/compose/miniflux
|
||||
sed -i "s|^MINIFLUX_VERSION=.*|MINIFLUX_VERSION=<new>|" .env
|
||||
docker compose pull miniflux
|
||||
docker compose up -d
|
||||
'
|
||||
```
|
||||
|
||||
Migrations run automatically on container start (`RUN_MIGRATIONS=1`
|
||||
in compose.yaml).
|
||||
@@ -0,0 +1,85 @@
|
||||
# Miniflux — minimal self-hosted RSS reader (Go single-binary, ~30 MB image).
|
||||
# Apache 2.0. https://miniflux.app/
|
||||
#
|
||||
# Use case: unified inbox across tech blogs, Hacker News, lobste.rs,
|
||||
# and selected subreddits (Reddit serves an .rss feed for any sub at
|
||||
# https://reddit.com/r/<sub>/.rss). Subreddits with score-spoiler
|
||||
# headlines (e.g. r/nba) get a per-feed block-list rule — see README.
|
||||
#
|
||||
# Architecture: this stack bundles its own Postgres rather than using
|
||||
# the shared pfi-postgres at 10.250.50.80. Single-user RSS DB is tiny
|
||||
# (~MB scale per year) and the bundle reduces cross-host coupling.
|
||||
# Backup: postgres data lives in the named volume miniflux-db-data;
|
||||
# capture via `docker exec miniflux-db pg_dump -U miniflux miniflux`
|
||||
# in restic's pre-backup hook (separate stack — TODO).
|
||||
#
|
||||
# All tunables live in .env — edit that, not this file.
|
||||
|
||||
services:
|
||||
miniflux:
|
||||
image: miniflux/miniflux:${MINIFLUX_VERSION}
|
||||
container_name: miniflux
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
miniflux-db:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "${MINIFLUX_BIND:-0.0.0.0}:${MINIFLUX_PORT}:8080"
|
||||
environment:
|
||||
- DATABASE_URL=postgres://miniflux:${MINIFLUX_DB_PASSWORD}@miniflux-db/miniflux?sslmode=disable
|
||||
- RUN_MIGRATIONS=1
|
||||
- CREATE_ADMIN=1
|
||||
- ADMIN_USERNAME=${MINIFLUX_ADMIN_USERNAME}
|
||||
- ADMIN_PASSWORD=${MINIFLUX_ADMIN_PASSWORD}
|
||||
- LOG_LEVEL=${MINIFLUX_LOG_LEVEL:-info}
|
||||
- HTTPS=0
|
||||
- LISTEN_ADDR=0.0.0.0:8080
|
||||
healthcheck:
|
||||
# Miniflux ships with a built-in healthcheck command.
|
||||
test: ["CMD", "/usr/bin/miniflux", "-healthcheck", "auto"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
networks:
|
||||
- miniflux-net
|
||||
- tnet
|
||||
labels:
|
||||
- homepage.group=News
|
||||
- homepage.name=Miniflux
|
||||
- homepage.icon=mdi-rss
|
||||
- homepage.description=RSS reader (subreddits, blogs, HN)
|
||||
- homepage.href=http://10.250.50.70:${MINIFLUX_PORT}
|
||||
|
||||
miniflux-db:
|
||||
image: postgres:16
|
||||
container_name: miniflux-db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- POSTGRES_USER=miniflux
|
||||
- POSTGRES_PASSWORD=${MINIFLUX_DB_PASSWORD}
|
||||
- POSTGRES_DB=miniflux
|
||||
- POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C
|
||||
volumes:
|
||||
- miniflux-db-data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- miniflux-net
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U miniflux"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
volumes:
|
||||
miniflux-db-data:
|
||||
|
||||
networks:
|
||||
# Internal — only miniflux talks to miniflux-db. DB is not on tnet
|
||||
# so it's not reachable from elsewhere on traefik-net.
|
||||
miniflux-net:
|
||||
name: miniflux-net
|
||||
# Shared external — homepage's docker auto-discovery scans this
|
||||
# network for the homepage.* labels above.
|
||||
tnet:
|
||||
name: traefik-net
|
||||
external: true
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<opml version="2.0">
|
||||
<head>
|
||||
<title>Starter feeds — fleet ops + selfhosting + LLM + sports</title>
|
||||
</head>
|
||||
<body>
|
||||
<outline text="Tech aggregators" title="Tech aggregators">
|
||||
<outline type="rss" text="Hacker News" title="Hacker News"
|
||||
xmlUrl="https://news.ycombinator.com/rss"
|
||||
htmlUrl="https://news.ycombinator.com/" />
|
||||
<outline type="rss" text="Lobste.rs" title="Lobste.rs"
|
||||
xmlUrl="https://lobste.rs/rss"
|
||||
htmlUrl="https://lobste.rs/" />
|
||||
</outline>
|
||||
<outline text="Subreddits — selfhosting / homelab / LLM" title="Subreddits — selfhosting">
|
||||
<outline type="rss" text="r/selfhosted" title="r/selfhosted"
|
||||
xmlUrl="https://www.reddit.com/r/selfhosted/.rss"
|
||||
htmlUrl="https://www.reddit.com/r/selfhosted" />
|
||||
<outline type="rss" text="r/homelab" title="r/homelab"
|
||||
xmlUrl="https://www.reddit.com/r/homelab/.rss"
|
||||
htmlUrl="https://www.reddit.com/r/homelab" />
|
||||
<outline type="rss" text="r/LocalLLaMA" title="r/LocalLLaMA"
|
||||
xmlUrl="https://www.reddit.com/r/LocalLLaMA/.rss"
|
||||
htmlUrl="https://www.reddit.com/r/LocalLLaMA" />
|
||||
</outline>
|
||||
<outline text="Subreddits — sports (apply spoiler block-list, see README)" title="Subreddits — sports">
|
||||
<outline type="rss" text="r/nba" title="r/nba"
|
||||
xmlUrl="https://www.reddit.com/r/nba/.rss"
|
||||
htmlUrl="https://www.reddit.com/r/nba" />
|
||||
</outline>
|
||||
</body>
|
||||
</opml>
|
||||
Reference in New Issue
Block a user