Why self-host FreshRSS on a VPS
FreshRSS is an RSS/Atom feed aggregator written in PHP, designed to be light on resources while handling thousands of articles. Self-hosting on a VPS solves three problems that closed services do not address: durability (your monitoring does not vanish overnight), privacy (nobody profiles your subscriptions) and freshness (you set the refresh frequency via cron, with no imposed quota). A VPS, unlike shared hosting, gives you full SSH access, the right to run a Docker container and to schedule tasks — exactly what FreshRSS needs to refresh feeds in the background.
Concrete benefits of a self-hosted FreshRSS
- Native API compatible with Fever and Google Reader: synchronize with Reeder, FeedMe or Fluent Reader on mobile.
- GReader / scraping extension: retrieve the full content of truncated articles thanks to XPath filters.
- Multi-user with per-account quotas: ideal for sharing an instance within an agency.
- Minimal memory footprint (200 to 300 MB): runs on the smallest of VPSes without flinching.
- Cron-driven refresh: you decide whether the feeds update every 5 minutes or every hour.
- Trivial backup: a SQLite/MySQL database and a data folder are enough to restore everything.
Technical requirements
FreshRSS is one of the most frugal self-hosting applications. Plan for 1 vCPU and 1 GB of RAM for a personal instance with a few users; 10 GB of disk is amply sufficient, the bulk of it taken up by the article history. On the software side: Docker and the Docker Compose plugin installed on the VPS, a domain name (or a subdomain like rss.yourdomain.com) pointed with an A record to the server's IP, and port 80/443 open for the reverse proxy. PHP does not need to be installed on the host: the official freshrss/freshrss image bundles everything.
Deploy FreshRSS step by step
Prepare the VPS and Docker
Connect via SSH, update the system (
apt update && apt upgrade -y), then install Docker:curl -fsSL https://get.docker.com | sh. Add your user to the docker group:usermod -aG docker $USER. Check withdocker compose version. Create the project folder:mkdir -p /opt/freshrss && cd /opt/freshrss.Write the docker-compose.yml
Create
docker-compose.ymlwith the following content:services: freshrss: image: freshrss/freshrss:latest container_name: freshrss restart: unless-stopped environment: CRON_MIN: "*/20" TZ: "Europe/Paris" volumes: - ./data:/var/www/FreshRSS/data - ./extensions:/var/www/FreshRSS/extensions ports: - "127.0.0.1:8080:80"The port is bound to
127.0.0.1only: solely the local reverse proxy can reach it.Configure nginx as reverse proxy
Install nginx:
apt install -y nginx. Create/etc/nginx/sites-available/freshrss:server { listen 80; server_name rss.yourdomain.com; location / { proxy_pass http://127.0.0.1:8080; 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; } }Enable the vhost:
ln -s /etc/nginx/sites-available/freshrss /etc/nginx/sites-enabled/ && nginx -t && systemctl reload nginx.Obtain the TLS certificate with Certbot
Install Certbot and the nginx plugin:
apt install -y certbot python3-certbot-nginx. Issue the certificate:certbot --nginx -d rss.yourdomain.com. Certbot automatically modifies the nginx vhost to listen on 443 and redirect HTTP to HTTPS. Automatic renewal is enabled by default via the certbot systemd timer.Launch the container
Start the stack from
/opt/freshrss:docker compose up -d. Follow the initialization:docker compose logs -f. The container applies migrations and creates the data tree on first startup. Then verify FreshRSS responds:curl -I https://rss.yourdomain.com.Complete the web installation
Open
https://rss.yourdomain.comin your browser. The wizard asks you to choose the database backend: select SQLite for personal use (single file, no service to manage), or MySQL if dozens of users are expected. Create the administrator account with a strong password.Enable the API
Go to Profile → API Management. Check "Enable GReader/Fever API" and set an API password separate from your main password (it will be transmitted in clear text by some clients). Note the API base URL:
https://rss.yourdomain.com/api/greader.php. You can test immediately withcurl -X POST https://rss.yourdomain.com/api/greader.php/accounts/ClientLogin -d [email protected]&Passwd=<apipass>.Import your OPML
Export your subscriptions from your old reader (Feedly, Inoreader…) in OPML format. In FreshRSS, go to Subscriptions → Import/Export → Import an OPML file. Your categories and feeds are recreated within seconds. Verify the first manual refresh:
docker compose exec freshrss php ./cli/actualize.php.Verify the automatic cron
The
CRON_MIN=*/20variable configures the container's internal cron to refresh all feeds every 20 minutes. Confirm it is running:docker compose exec freshrss crontab -l. If you prefer to control it from the host, disable the internal cron (CRON_MIN=empty) and add a system crontab:*/15 * * * * docker exec freshrss php /var/www/FreshRSS/app/actualize_script.php 2>&1 | logger -t freshrss.
For sites that publish only an excerpt in their feed, install the official "FreshRSS Content Selector" extension: configure a CSS selector (e.g. article.post-content) per subscription and FreshRSS will fetch the full text on each refresh. Combine it with the filters extension to automatically hide sponsored articles detected by keyword.
Advanced FreshRSS configuration
Once the base instance is running, three optimization areas turn FreshRSS from a simple reader into a proper monitoring tool:
Extensions: RSS-Bridge and FullTextRss. Copy the extension into /opt/freshrss/extensions/, then enable it in the Extensions section of the interface. RSS-Bridge generates an RSS feed for sites that do not provide one (public Twitter/X, Instagram, LinkedIn), running as a bridge on your VPS. FullTextRss (self-hosted version of the FiveFilters library) completes truncated articles by extracting the HTML body according to pre-defined site rules — over 2,000 sites are already configured in the community rule base.
Refresh frequency. The CRON_MIN=*/20 value suits most monitoring needs. For critical sources (security alerts, stock prices), go down to */5. To reduce load on your VPS with many inactive subscriptions, go up to */60. FreshRSS also respects ETag and Last-Modified HTTP headers: if a feed has not changed, the refresh costs almost nothing in bandwidth.
SQLite vs MySQL. SQLite handles up to a few hundred thousand articles and a handful of users; its simplicity (single file) makes it a solid option for personal use. Beyond 5 simultaneous users or 500,000 articles, MySQL (or MariaDB) offers better read query performance. Migration from SQLite to MySQL is possible via the convert-db.php CLI tool included in the container.
The FreshRSS API for your AI agents and automations
FreshRSS is one of the few self-hosted aggregators that implements two standard APIs: the Google Reader API (/api/greader.php) and the Fever API (/api/fever.php). These publicly documented APIs enable frictionless integration with dozens of automation tools.
n8n integration. In an n8n workflow, use the HTTP Request node with Basic authentication (FreshRSS API credentials). Trigger the workflow on new unread articles by polling the GReader endpoint: GET /api/greader.php/reader/api/0/stream/contents/user/-/state/com.google/reading-list?n=20&xt=user/-/state/com.google/read. Chain with an OpenAI node to summarize each article, then send the summary to Slack or by email. All within your own infrastructure.
Make (ex-Integromat) integration. Make's HTTP module allows the same REST calls. A typical scenario: every hour, fetch starred articles, enrich them with an LLM via a webhook, and push them into Notion or Airtable to build a structured knowledge base.
AI agents. Frameworks like LangChain or LlamaIndex can query the FreshRSS API directly to feed a RAG (Retrieval-Augmented Generation) pipeline on your personal monitoring. Your instance becomes a private, structured, fresh data source, inaccessible to public models.
FreshRSS vs Miniflux vs Tiny Tiny RSS
Scroll the table
| Criterion | FreshRSS | Miniflux | Tiny Tiny RSS |
|---|---|---|---|
| Language / stack | PHP + SQLite/MySQL | Go (single binary) | PHP + PostgreSQL |
| Minimum RAM | ~50 MB (without Docker) | ~20 MB | ~100 MB |
| Standard API | Google Reader + Fever | Miniflux REST + Fever | Google Reader (plugin) |
| Extensions / plugins | Rich (30+ official) | Limited (native filters) | Many third-party plugins |
| License | AGPL-3.0 | Apache-2.0 | GPL-3.0 |
Backing up and migrating FreshRSS
One of the advantages of self-hosting is complete control over your data. Two backup levels cover all scenarios:
OPML export (subscriptions). From Subscriptions → Import/Export → Export as OPML, you generate an XML file containing all your feeds and categories. This is the minimal backup: it lets you reimport your feed list on any FreshRSS instance or any other compatible RSS reader. Automate it with a crontab: 0 3 * * * docker exec freshrss php /var/www/FreshRSS/cli/export-opml-file.php > /opt/backups/freshrss-opml-$(date +%F).xml.
Full backup (database + data). To also retain the reading history and stars, back up the entire Docker volume: rsync -a /opt/freshrss/data/ /opt/backups/freshrss-data-$(date +%F)/. If using SQLite, the database is in data/users/<user>/non_default_feeds.sqlite. For MySQL, a mysqldump is sufficient.
Migration between VPSes. The process is linear: (1) stop the source container (docker compose stop); (2) copy the data/ folder to the new VPS via rsync or scp; (3) deploy the same docker-compose.yml on the new VPS and start it; (4) update the DNS A record to the new IP. Total time for a personal instance: under 10 minutes, zero data loss.
Troubleshooting: 4 common errors
1. Inactive cron — feeds no longer update. Symptom: the "Last updated" column remains frozen. First check that the internal cron is running: docker compose exec freshrss crontab -l. If the output is empty, the CRON_MIN variable is missing or empty in your docker-compose.yml. Add CRON_MIN: "*/20" under environment: and recreate the container (docker compose up -d --force-recreate).
2. Feed that never updates despite an active cron. Most common cause: the feed consistently returns HTTP 429 (rate-limit) or 403. Check under Subscriptions → Manage → Last error. For a site that blocks requests without a User-Agent, add an explicit User-Agent in the feed's advanced settings (e.g. FreshRSS/1.24 (+https://rss.yourdomain.com)). For a feed behind Cloudflare, RSS-Bridge with the CloudFlare bridge bypasses the JS challenge.
3. SSL error "certificate verify failed". Occurs when the FreshRSS container tries to reach an HTTPS feed with a self-signed or expired certificate. Two options: disable SSL verification for that specific feed (checkbox to uncheck in the subscription's advanced options), or update the container's CA store if the entire SSL stack is broken (docker compose exec freshrss update-ca-certificates).
4. FullTextRss extension silent — content remains truncated. Check that the extension is enabled in Extensions and that the FullTextRss service URL is correct in its configuration. If using the self-hosted version, test the URL directly in the browser: https://your-ftr.domain.com/extract.php?url=<article_url>. An empty JSON response indicates the site's selector is not in the rule base — create a custom rule file in site_patterns/ and mount it as a Docker volume.
Use case: developer tech-watch feed
Add GitHub release feeds for every dependency in your project (https://github.com/<owner>/<repo>/releases.atom), the Hacker News feed (https://news.ycombinator.com/rss), the blogs of key maintainers in your ecosystem, and CVE feeds for your runtime and OS.
FreshRSS delivers the raw chronological timeline — no algorithmic sorting, no promoted posts. Create an auto-star rule: any article containing 'security advisory' or 'breaking change' gets starred and triggers an ntfy notification to your phone. You get the signal, without the noise of social platforms.