From 6c96ffef01589b3ed95f980fe1ce62661be3a77e Mon Sep 17 00:00:00 2001 From: Vuong Hoang Date: Sun, 26 Apr 2026 11:52:50 -0700 Subject: [PATCH] stacks/miniflux: self-hosted RSS reader + News group on homepage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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//.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. --- configs/homepage/settings.yaml | 5 ++ playbooks/deploy-miniflux.yaml | 82 ++++++++++++++++++++ stacks/miniflux/.env.example | 37 +++++++++ stacks/miniflux/README.md | 120 +++++++++++++++++++++++++++++ stacks/miniflux/compose.yaml | 85 ++++++++++++++++++++ stacks/miniflux/starter-feeds.opml | 32 ++++++++ 6 files changed, 361 insertions(+) create mode 100644 playbooks/deploy-miniflux.yaml create mode 100644 stacks/miniflux/.env.example create mode 100644 stacks/miniflux/README.md create mode 100644 stacks/miniflux/compose.yaml create mode 100644 stacks/miniflux/starter-feeds.opml diff --git a/configs/homepage/settings.yaml b/configs/homepage/settings.yaml index bc31686..27d0f51 100644 --- a/configs/homepage/settings.yaml +++ b/configs/homepage/settings.yaml @@ -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 diff --git a/playbooks/deploy-miniflux.yaml b/playbooks/deploy-miniflux.yaml new file mode 100644 index 0000000..5e73efd --- /dev/null +++ b/playbooks/deploy-miniflux.yaml @@ -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" diff --git a/stacks/miniflux/.env.example b/stacks/miniflux/.env.example new file mode 100644 index 0000000..ddc01f9 --- /dev/null +++ b/stacks/miniflux/.env.example @@ -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 diff --git a/stacks/miniflux/README.md b/stacks/miniflux/README.md new file mode 100644 index 0000000..0b52cfe --- /dev/null +++ b/stacks/miniflux/README.md @@ -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//.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 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//.rss` | Newest posts (firehose) | +| `https://reddit.com/r//top/.rss?t=day` | Top-of-day (less spam) | +| `https://reddit.com/r//top/.rss?t=week` | Top-of-week | +| `https://reddit.com/r//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=|" .env + docker compose pull miniflux + docker compose up -d +' +``` + +Migrations run automatically on container start (`RUN_MIGRATIONS=1` +in compose.yaml). diff --git a/stacks/miniflux/compose.yaml b/stacks/miniflux/compose.yaml new file mode 100644 index 0000000..612c51e --- /dev/null +++ b/stacks/miniflux/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//.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 diff --git a/stacks/miniflux/starter-feeds.opml b/stacks/miniflux/starter-feeds.opml new file mode 100644 index 0000000..c124fc6 --- /dev/null +++ b/stacks/miniflux/starter-feeds.opml @@ -0,0 +1,32 @@ + + + + Starter feeds — fleet ops + selfhosting + LLM + sports + + + + + + + + + + + + + + + +