LUNAROPS · OPERATIONAL UPLINK 100% UPTIME 1,247d POSTS 893 JEFF.MOON@LUNAROPS.DEV UTC --:--:--

Miniflux: Minimal Self-Hosted RSS

homelabself-hostingdockerrssgoprivacyopen-source

RSS never died. Millions of people still use it — they just moved to centralized services like Feedly and Inoreader where someone else manages the infrastructure. If you’d rather own your reading workflow, Miniflux is the cleanest option in the self-hosted RSS space.

Miniflux is a deliberately minimalist feed reader: a single statically-compiled Go binary, a PostgreSQL database, and a server-rendered web UI with no JavaScript bloat. It’s fast, it’s simple to operate, and it integrates with Fever API clients so you can use a native mobile app if you prefer. Created by Frédéric Guillot and licensed Apache 2.0, it’s been actively maintained for years with a clear philosophy: improve what exists rather than pile on features.


Architecture

The entire application is a single Go binary with zero runtime dependencies. No Node.js, no Ruby, no external asset pipeline — just a binary and a PostgreSQL database. Static assets (HTML, CSS, the minimal JavaScript for keyboard shortcuts) are embedded into the binary at build time using Go’s embed package.

PostgreSQL is the only supported database. This is a deliberate choice to keep the codebase focused; there’s no SQLite or MySQL path, and none is planned. The database does real work — full-text search, JSONB storage, window functions — so the constraint makes the implementation cleaner than trying to abstract over multiple engines.

Resource requirements are modest: a single core and 50–200 MB of RAM handles hundreds of feeds comfortably. The bottleneck is almost always network I/O during feed polling, not CPU or memory.


Deploying with Docker Compose

Basic Setup

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
services:
  miniflux:
    image: miniflux/miniflux:latest
    container_name: miniflux
    ports:
      - "8080:8080"
    depends_on:
      db:
        condition: service_healthy
    environment:
      - DATABASE_URL=postgres://miniflux:secret@db/miniflux?sslmode=disable
      - RUN_MIGRATIONS=1
      - CREATE_ADMIN=1
      - ADMIN_USERNAME=admin
      - ADMIN_PASSWORD=changeme
      - BASE_URL=http://localhost:8080
    restart: unless-stopped

  db:
    image: postgres:16
    container_name: miniflux-db
    environment:
      - POSTGRES_USER=miniflux
      - POSTGRES_PASSWORD=secret
      - POSTGRES_DB=miniflux
    volumes:
      - miniflux-db:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "miniflux"]
      interval: 10s
      start_period: 30s
    restart: unless-stopped

volumes:
  miniflux-db:

RUN_MIGRATIONS=1 applies database schema migrations automatically on startup. CREATE_ADMIN=1 bootstraps the initial admin account using ADMIN_USERNAME and ADMIN_PASSWORD — these are only used on the first run and ignored thereafter.

The depends_on health check ensures Miniflux waits for PostgreSQL to be ready before starting. Without this, Miniflux exits immediately on startup because it can’t connect.

Behind Traefik

For HTTPS termination via Traefik:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
networks:
  traefik:
    external: true
  internal:

services:
  miniflux:
    image: miniflux/miniflux:latest
    container_name: miniflux
    depends_on:
      db:
        condition: service_healthy
    environment:
      - DATABASE_URL=postgres://miniflux:secret@db/miniflux?sslmode=disable
      - RUN_MIGRATIONS=1
      - BASE_URL=https://rss.yourdomain.com
      - POLLING_FREQUENCY=60
      - POLLING_SCHEDULER=entry_frequency
      - WORKER_POOL_SIZE=10
      - PROXY_IMAGES=http-only
    volumes:
      - ./configs:/etc/miniflux
    networks:
      - traefik
      - internal
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.miniflux.rule=Host(`rss.yourdomain.com`)"
      - "traefik.http.routers.miniflux.entrypoints=websecure"
      - "traefik.http.routers.miniflux.tls.certresolver=letsencrypt"
      - "traefik.http.services.miniflux.loadbalancer.server.port=8080"
    restart: unless-stopped

  db:
    image: postgres:16
    container_name: miniflux-db
    environment:
      - POSTGRES_USER=miniflux
      - POSTGRES_PASSWORD=secret
      - POSTGRES_DB=miniflux
    volumes:
      - miniflux-db:/var/lib/postgresql/data
    networks:
      - internal
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "miniflux"]
      interval: 10s
      start_period: 30s
    restart: unless-stopped

volumes:
  miniflux-db:

The database is on the internal network only — it’s not reachable from outside the Docker network. Miniflux sits on both networks, bridging the two.

Set BASE_URL to your external URL. Miniflux uses this for generating correct links in API responses and feed-level settings. If it’s wrong, API clients get broken URLs.


Environment Variable Reference

Variable Default Purpose
DATABASE_URL PostgreSQL connection string (required)
RUN_MIGRATIONS 0 Set to 1 to auto-migrate on startup
CREATE_ADMIN 0 Set to 1 to create admin on first run
ADMIN_USERNAME Bootstrap admin username
ADMIN_PASSWORD Bootstrap admin password
BASE_URL http://localhost External URL — used in API and links
LISTEN_ADDR 0.0.0.0:8080 Internal bind address
POLLING_FREQUENCY 60 Minutes between polling runs
POLLING_SCHEDULER round_robin round_robin or entry_frequency
BATCH_SIZE 100 Feeds processed per polling run
WORKER_POOL_SIZE 5 Concurrent feed fetchers
FETCHER_TIMEOUT 30 HTTP timeout in seconds
PROXY_IMAGES none none, http-only, or all
FETCH_YOUTUBE_WATCH_TIME 0 Extract YouTube video duration
POLLING_PARSING_ERROR_LIMIT 10 Errors before disabling a feed (use ≥1)
OAUTH2_PROVIDER google or oidc for SSO
FETCHER_ALLOW_PRIVATE_NETWORKS 0 Set to 1 for internal/homelab feeds

Polling Schedulers

Miniflux offers two feed polling strategies:

round_robin (default) — polls feeds in a fixed rotating order at a consistent interval. Every feed gets checked on the same schedule regardless of how often it actually updates. Predictable, simple, works well when you have a small to moderate feed list where you know the sources.

entry_frequency — analyses each feed’s historical update frequency over the past week and polls accordingly. A feed that publishes 20 times a day gets checked more often than one that publishes weekly. This reduces unnecessary requests to dormant feeds and concentrates polling bandwidth on active sources. Better choice for large or mixed collections.

BATCH_SIZE controls how many feeds are processed per polling run. With POLLING_FREQUENCY=60 and BATCH_SIZE=100, up to 100 feeds are refreshed each hour. Increase WORKER_POOL_SIZE to fetch those feeds concurrently.

For a homelab with 200+ feeds, a reasonable configuration:

1
2
3
4
- POLLING_FREQUENCY=30
- POLLING_SCHEDULER=entry_frequency
- WORKER_POOL_SIZE=15
- BATCH_SIZE=200

Feed Rules: Filtering, Rewriting, and Scraping

This is where Miniflux earns its keep for power users. Rules are configured per-feed in the feed settings.

Block and Keep Rules

Block rules discard entries whose titles or content match a regex pattern. Keep rules do the inverse — discard everything that doesn’t match. Use RE2 regex syntax.

Block rule examples:

(?i)sponsored
(?i)press release
(?i)\[ad\]

Keep rule example (only keep Kubernetes-related entries):

(?i)kubernetes|k8s|helm|kubectl

Max-age rule — drop entries older than a threshold:

max-age:7d

Units: ns, us, ms, s, m, h, d. Useful for feeds that republish old content.

Rewrite Rules

Rewrite rules transform URLs before Miniflux fetches content. The format is search_pattern@@@replacement:

# Strip tracking parameters from URLs
^(https://example\.com/article/[^?]+).*$@@@$1

# Force HTTPS
^http://(.+)$@@@https://$1

# Point to a different page variant (e.g., full article vs. paginated)
^(.+\.html).*$@@@$1?seite=all

Content Scraper

Many feeds provide only a summary or a truncated excerpt. The content scraper fetches the original page and extracts the full article — entirely locally, using a Readability-based parser with no external service.

Miniflux ships with predefined scraper rules for 100+ popular websites. For others, you can write a CSS selector rule per feed:

.article-body
article > div.content
main .post-content

If no custom rule matches, the Readability algorithm takes over as a fallback.

You can trigger scraping manually per entry with the d keyboard shortcut, or configure it to run automatically for a feed.


Keyboard Shortcuts

Miniflux’s keyboard-driven UI is one of its signature features. Navigation is Vi-style:

Global navigation:

Shortcut Action
g h History
g f Feeds
g c Categories
g b Bookmarks (starred)
g s Settings
? Show shortcut help
/ Search

Reading list navigation:

Shortcut Action
j / Next item
k / Previous item
o Open selected item
v Open original URL in new tab
m Toggle read/unread
A Mark all visible as read
f Star/unstar entry
d Fetch full content (scraper)
s Save to external service
# Remove feed
Escape Close dialogs

Firefox note: Firefox blocks the v shortcut for programmatic new-tab opens. You’ll need to authorize it in Firefox’s accessibility settings or just use middle-click.


Mobile Clients via Fever API

Miniflux implements the Fever API, which allows dedicated RSS client apps to use it as a backend. Enable it in Settings → Integrations — Miniflux auto-generates the Fever credentials.

iOS clients:

  • Reeder — the most polished option; gesture-based, offline caching, excellent typography
  • Unread — minimalist and focused on the reading experience
  • ReadKit — native macOS/iOS, good for cross-device sync
  • NetNewsWire — free and open-source, solid if you’re already in the Apple ecosystem

Android:

  • Fleuron — built specifically for Miniflux using Material You design; the best native-feeling option

Miniflux also implements a subset of the Google Reader API for clients built around that protocol. Configure it separately in Settings → Integrations → Google Reader API with a dedicated username and password.

The responsive web UI itself works well on mobile for users who don’t want a separate app — it’s genuinely usable on a phone without pinching and zooming.


SSO with OIDC

Miniflux supports OAuth2/OIDC for single sign-on. If your homelab already has Authentik, Keycloak, or Authelia as an identity provider, you can wire Miniflux into it:

1
2
3
4
5
6
7
environment:
  - OAUTH2_PROVIDER=oidc
  - OAUTH2_CLIENT_ID=miniflux
  - OAUTH2_CLIENT_SECRET=your-client-secret
  - OAUTH2_OIDC_DISCOVERY_ENDPOINT=https://authentik.yourdomain.com/application/o/miniflux
  - OAUTH2_REDIRECT_URL=https://rss.yourdomain.com/oauth2/oidc/callback
  - OAUTH2_USER_CREATION=1

Note: Miniflux automatically appends /.well-known/openid-configuration to the discovery endpoint URL. Don’t include that suffix in OAUTH2_OIDC_DISCOVERY_ENDPOINT — it will result in a double-append and a 404.

OAUTH2_USER_CREATION=1 allows new users to be created automatically on first SSO login. Without it, users must be pre-created in Miniflux before they can log in via SSO.


REST API

Every operation in the UI is available via REST API. Authentication supports two methods: HTTP Basic Auth or an API key in the X-Auth-Token header. Generate API keys in Settings → API Keys.

Base URL: https://rss.yourdomain.com/v1/

Common operations:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# Check authentication and get user info
curl -H "X-Auth-Token: your-token" https://rss.yourdomain.com/v1/me

# List all feeds
curl -H "X-Auth-Token: your-token" https://rss.yourdomain.com/v1/feeds

# Add a new feed
curl -X POST \
  -H "X-Auth-Token: your-token" \
  -H "Content-Type: application/json" \
  -d '{"feed_url": "https://feeds.example.com/rss", "category_id": 1}' \
  https://rss.yourdomain.com/v1/feeds

# Get unread entries
curl -H "X-Auth-Token: your-token" \
  "https://rss.yourdomain.com/v1/entries?status=unread&limit=50"

# Mark an entry as read
curl -X PUT \
  -H "X-Auth-Token: your-token" \
  -H "Content-Type: application/json" \
  -d '{"status": "read"}' \
  https://rss.yourdomain.com/v1/entries/12345

# Star an entry
curl -X PUT \
  -H "X-Auth-Token: your-token" \
  https://rss.yourdomain.com/v1/entries/12345/bookmark

# Export OPML
curl -H "X-Auth-Token: your-token" \
  https://rss.yourdomain.com/v1/export -o feeds.opml

# Import OPML
curl -X POST \
  -H "X-Auth-Token: your-token" \
  -H "Content-Type: text/xml" \
  --data-binary @feeds.opml \
  https://rss.yourdomain.com/v1/import

Official client libraries exist for Go (miniflux.app/v2/client) and Python (miniflux on PyPI). The Go client is maintained in the same repository as the server.


Webhooks

Miniflux can POST to a webhook URL whenever new entries arrive. Requests are HMAC-SHA256 signed — validate the X-Miniflux-Signature header in your handler to confirm authenticity.

This makes it straightforward to wire Miniflux into n8n, a custom API, or a notification service. The community n8n node (n8n-nodes-miniflux) also exposes Miniflux operations directly in n8n’s visual workflow builder.

A simple webhook workflow:

  1. New entry arrives in the “Security” category
  2. Miniflux POSTs to your n8n webhook
  3. n8n filters for high-severity CVE entries
  4. Sends a Slack notification or creates a ticket

External Service Integrations

Miniflux has built-in save-to integrations for read-later and bookmarking services, configured in Settings → Integrations:

  • Wallabag — self-hosted read-later (the obvious homelab pairing)
  • Pocket — the mainstream option
  • Instapaper — clean archival format
  • Pinboard — for the bookmark-everything crowd

Once configured, the s keyboard shortcut saves the current article to your chosen service.


OPML Migration

Moving from another RSS reader? Export OPML from your current tool and import it:

1
2
3
4
5
6
# Import via API
curl -X POST \
  -H "X-Auth-Token: your-token" \
  -H "Content-Type: text/xml" \
  --data-binary @my-feeds.opml \
  https://rss.yourdomain.com/v1/import

Or use the web UI: Feeds → Import.

If migrating from Miniflux v1, expect some duplicate entries on the first refresh after import — entries are keyed differently between versions and Miniflux will see them as new. This clears up on the next polling cycle.


Comparison with Alternatives

Miniflux FreshRSS Tiny Tiny RSS NewsBlur
Language Go PHP PHP Python/Django
Database PostgreSQL only SQLite / PostgreSQL / MySQL PostgreSQL MongoDB / PostgreSQL
Deployment Single binary PHP app PHP app Multi-container
UI philosophy Minimal, server-rendered Feature-rich Feature-rich, plugin-heavy Feature-rich
Mobile Fever API + responsive web Fever API Paid apps Native apps
Keyboard shortcuts Yes (Vi-style) Limited Yes Yes
Content scraper Yes (built-in) Via plugins Via plugins Yes
Full-text search Yes (PostgreSQL FTS) Yes Yes Yes
SSO/OIDC Yes Via plugins Limited No
Webhook support Yes Via plugins Via plugins No
Operational complexity Low Medium Medium-High High

Choose Miniflux if you want something that just works with minimal operational overhead, you value a clean reading experience over a feature checklist, and you already have PostgreSQL somewhere.

Choose FreshRSS if you need MySQL/SQLite support or want a richer plugin ecosystem without PostgreSQL.

Choose TTRSS if you have very specific filtering or plugin requirements and don’t mind a rougher admin experience.


Gotchas

POLLING_PARSING_ERROR_LIMIT=0 is broken. Setting it to 0 incorrectly marks feeds as broken even when they have no errors. Use any value ≥1. The default of 10 is reasonable.

Private network feeds are blocked by default. If you’re subscribing to feeds hosted on your internal network (other homelab services, internal wikis), add:

1
2
- FETCHER_ALLOW_PRIVATE_NETWORKS=1
- INTEGRATION_ALLOW_PRIVATE_NETWORKS=1

BASE_URL must match your external URL exactly. If you access Miniflux at https://rss.yourdomain.com but BASE_URL is set to http://localhost:8080, API responses and mobile clients will get broken URLs.

No horizontal scaling. Miniflux doesn’t cluster. One instance + one PostgreSQL is the deployment model. For high availability, run a hot-standby PostgreSQL replica and failover manually.

PostgreSQL backups are the whole story. Since everything lives in PostgreSQL, your backup strategy is simply: backup the database. pg_dump on a schedule, or continuous archiving with WAL shipping, is sufficient.


The Bottom Line

Miniflux is the RSS reader for people who want to read feeds, not configure a feed reader. The deployment is as simple as self-hosted software gets — one binary, one database, one compose file — and the operational surface area is tiny.

The keyboard shortcuts and content scraper make it genuinely pleasant for heavy RSS users. The Fever API means you can use a polished native client on your phone without giving up the self-hosted backend. And the REST API and webhook support make it composable with the rest of your homelab automation stack.

If you’ve been putting off self-hosting an RSS reader because the options felt too heavy, Miniflux is the one to try first.

Comments