All articles
TutorialsJun 06, 2026 · 21 min read

Self-Host FreshRSS on a VPS for Your Own RSS Reader

Self-Host FreshRSS on a VPS for Your Own RSS Reader

The open web still publishes RSS. Almost every blog, news site, YouTube channel, GitHub release page, and podcast exposes a feed, and a good reader turns that firehose into a calm, chronological inbox you actually control. The catch is that hosted readers come and go - Google Reader is the famous casualty, but plenty of smaller services have quietly shut down and taken your subscriptions with them.

FreshRSS fixes that by being yours. It's a fast, open-source PHP aggregator that runs comfortably on a 512 MB VPS, stores everything in SQLite by default, and speaks the Google Reader and Fever sync APIs so the mobile app you already like keeps working. This guide takes you from a fresh server to a working public URL with HTTPS, an admin account, automatic feed refreshing, and mobile sync.

TL;DR

  • Install Docker and Docker Compose on a small VPS
  • Point a subdomain like rss.example.com at the server
  • Run freshrss/freshrss behind Caddy in one docker-compose.yml
  • Walk through the web installer, pick SQLite, create the admin user
  • Enable the API so Reeder, NetNewsWire, or Readrops can sync
  • Let the built-in cron refresh feeds every 20 minutes
  • Back up the data volume daily

Total time: about 20 minutes.

What You Need

  • A VPS with at least 512 MB RAM running Ubuntu 22.04 or 24.04
  • A domain or subdomain you can point at the server
  • Ports 80 and 443 open to the internet for Let's Encrypt
  • Root or sudo access

FreshRSS is light. With a few hundred feeds it sits around 80-120 MB of RAM, and SQLite keeps the storage footprint tiny. If you already run other Docker workloads, this slots in beside them on the same box.

Why FreshRSS Over a Hosted Reader

Quick orientation, because people weigh this differently:

  • Hosted readers (Feedly, Inoreader, NewsBlur) are convenient but rented. Free tiers cap your feed count, the company can pivot or shut down, and your reading history lives on someone else's server.
  • FreshRSS is a single container you own. No feed limits, no telemetry, no upsell. The trade is that you run the updates and backups yourself - which, on a VPS you already pay for, is close to free.

The killer feature is API compatibility. FreshRSS implements the Google Reader API, so the best native clients on every platform - Reeder, NetNewsWire, Readrops, FeedMe, Fluent Reader - sync against it as if it were a commercial service.

Step 1: Point a Subdomain at Your VPS

In your DNS provider, add an A record:

rss.example.com → YOUR_VPS_IPV4

Add an AAAA record if you use IPv6. Verify it resolves:

dig +short rss.example.com

DNS has to point at your VPS before Caddy can fetch a certificate.

Step 2: Install Docker and Docker Compose

On a fresh Ubuntu box:

sudo apt update sudo apt install -y ca-certificates curl sudo install -m 0755 -d /etc/apt/keyrings sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \ -o /etc/apt/keyrings/docker.asc sudo chmod a+r /etc/apt/keyrings/docker.asc echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \ https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Confirm:

docker --version docker compose version

Step 3: Open the Firewall

If you use UFW:

sudo ufw allow 22/tcp sudo ufw allow 80/tcp sudo ufw allow 443/tcp sudo ufw enable

Caddy needs port 80 for the ACME HTTP challenge and 443 for HTTPS.

Step 4: Create the Project Directory

sudo mkdir -p /opt/freshrss cd /opt/freshrss sudo mkdir -p freshrss-data freshrss-extensions caddy-data caddy-config

Everything persistent lives under /opt/freshrss. Back up this one directory and you can rebuild the stack on any host.

Step 5: Write the Compose File

FreshRSS is a single container, and SQLite means no separate database service to babysit. Create /opt/freshrss/docker-compose.yml:

services: freshrss: image: freshrss/freshrss:1.24.3 container_name: freshrss restart: unless-stopped environment: TZ: Europe/Berlin CRON_MIN: "*/20" TRUSTED_PROXY: "172.16.0.0/12" volumes: - ./freshrss-data:/var/www/FreshRSS/data - ./freshrss-extensions:/var/www/FreshRSS/extensions networks: - freshrss-net caddy: image: caddy:2 container_name: freshrss-caddy restart: unless-stopped ports: - "80:80" - "443:443" volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - ./caddy-data:/data - ./caddy-config:/config networks: - freshrss-net networks: freshrss-net:

A few notes:

  • The FreshRSS container serves on port 80 internally. We don't publish it on the host because Caddy reaches it on the Docker network.
  • CRON_MIN: "*/20" runs the built-in feed refresh every 20 minutes. The image ships its own cron, so you don't need a host crontab for updates.
  • TRUSTED_PROXY tells FreshRSS to trust the X-Forwarded-* headers from Caddy. Without it, FreshRSS may build http:// links behind your https:// proxy and mangle logins. The 172.16.0.0/12 range covers the default Docker bridge networks.
  • Pin a real version tag (1.24.3 here) rather than latest so an unattended docker compose pull can't surprise you with a major upgrade.
Set `TZ` to your actual timezone. FreshRSS timestamps articles and schedules the refresh cron against it. Leaving it on UTC isn't fatal, but "new today" filters and the refresh window will feel off by your offset.

Step 6: Write the Caddyfile

Create /opt/freshrss/Caddyfile:

rss.example.com { encode zstd gzip reverse_proxy freshrss:80 }

Caddy will request a Let's Encrypt certificate for rss.example.com automatically on first boot. No extra config needed.

Step 7: Start the Stack

cd /opt/freshrss sudo docker compose up -d sudo docker compose logs -f

Watch the logs until Caddy reports the certificate was issued. Then open https://rss.example.com in a browser. You should land on the FreshRSS install wizard.

Step 8: Run the Install Wizard

The browser-based installer walks through five short screens:

  1. Language - pick yours and continue.
  2. Checks - the image ships every required PHP extension, so this page should be all green. If anything is red, your volume permissions are wrong (see Troubleshooting).
  3. Database - choose SQLite. There's nothing else to fill in; FreshRSS creates the database file inside your data volume.
  4. General configuration - create your admin username and a strong password. This is the account you'll log in with daily.
  5. Done - click through to the login screen and sign in.

SQLite is the right call for a single-user instance and easily handles thousands of articles. If you plan to host dozens of users, you can rerun this step against a PostgreSQL container instead, but most readers never need it.

Step 9: Add Your First Feeds

Once you're in, there are three ways to subscribe:

  • Paste a URL. Click Subscription management → Add, paste either a feed URL or a plain site URL. FreshRSS auto-discovers the feed link on most sites.
  • Import OPML. Migrating from another reader? Export an OPML file there, then Import / Export → Import it here. All your subscriptions and folders come across in one shot.
  • YouTube and Reddit. FreshRSS reads the hidden feeds these sites still publish. For a channel, use https://www.youtube.com/feeds/videos.xml?channel_id=CHANNEL_ID; for a subreddit, append .rss to its URL.

Organize feeds into categories (folders) from day one - it's far easier than retagging hundreds later.

Step 10: Enable Mobile and Desktop App Sync

This is what makes a self-hosted reader actually pleasant to use. FreshRSS exposes the Google Reader API, and most native clients sync against it.

In the web UI:

  1. Go to Settings → Profile.
  2. Find API management and set an API password. This is separate from your login password - treat it as an app token.
  3. Save.

Then in your reader app, choose the account type and fill in:

  • Server / endpoint URL: https://rss.example.com/api/greader.php
  • Username: your FreshRSS admin username
  • Password: the API password you just set

Clients known to work over the Google Reader API:

  • iOS / macOS: Reeder, NetNewsWire, Fiery Feeds
  • Android: Readrops, FeedMe, Fluent Reader
  • Desktop: Fluent Reader, Newsflash (Linux)

If a client only offers "Fever API," FreshRSS supports that too at https://rss.example.com/api/fever.php - enable it on the same settings page.

The API password is a long-lived credential that syncs your entire reading account without a second factor. Use a unique random string, store it in your password manager, and rotate it if a device is lost. Anyone holding it can read and modify your subscriptions.

Step 11: Confirm Feeds Are Refreshing Automatically

Because you set CRON_MIN, the container refreshes feeds on its own. Verify it's running:

sudo docker compose logs freshrss | grep -i cron

You should see periodic refresh entries. To force a refresh immediately - useful right after a big OPML import:

sudo docker exec --user www-data freshrss \ php /var/www/FreshRSS/app/actuator.php --actionName refresh

You can also tune per-feed refresh intervals and article retention under each feed's settings, which keeps the database lean if you follow high-volume news sites.

Step 12: Back Up the Data Volume

Everything that matters - the SQLite database, your subscriptions, read state, and favorites - lives in /opt/freshrss/freshrss-data. Lose it and you've lost your reading setup.

SQLite is a single file in your volume. FreshRSS has no cloud and no second copy by default. Set up this backup before you import hundreds of feeds you'd hate to re-add by hand.

Create /usr/local/bin/freshrss-backup.sh:

#!/usr/bin/env bash set -euo pipefail BACKUP_DIR="/var/backups/freshrss" DATE="$(date +%F-%H%M)" mkdir -p "$BACKUP_DIR" tar -czf "$BACKUP_DIR/freshrss-$DATE.tar.gz" -C /opt/freshrss freshrss-data find "$BACKUP_DIR" -name "freshrss-*.tar.gz" -mtime +14 -delete

FreshRSS uses SQLite in WAL mode, so a tar of the data directory is safe to take while the container runs - the WAL and shared-memory files are captured alongside the main database. For a belt-and-braces snapshot, you can stop the container first, but for a personal reader the live tar is fine.

Make it executable and schedule a daily run:

sudo chmod +x /usr/local/bin/freshrss-backup.sh echo "20 3 * * * root /usr/local/bin/freshrss-backup.sh" | \ sudo tee /etc/cron.d/freshrss-backup

For off-site safety, sync /var/backups/freshrss to S3, Backblaze B2, or another VPS with rclone on the same schedule. To restore: stop the stack, replace freshrss-data/ with the contents of a backup, start the stack again.

Step 13: Upgrade FreshRSS Safely

FreshRSS ships regular releases. The safe upgrade path:

cd /opt/freshrss # bump the image tag in docker-compose.yml first, then: sudo docker compose pull sudo docker compose up -d

Always take a fresh backup first. The container applies any database migrations on startup, and rolling back is far easier with a known-good snapshot than with a half-migrated database.

Optional: Put FreshRSS Behind a VPN

A personal reader that only you use is a fine candidate for VPN-only access. Pair this with our Tailscale guide so rss.example.com only resolves inside your tailnet. The mobile clients sync fine over WireGuard or Tailscale, and you remove the public attack surface entirely. If you'd rather keep it public but locked down, our SSH hardening and fail2ban guide covers the server side.

Troubleshooting

Caddy returns 502 Bad Gateway. The FreshRSS container hasn't finished starting or crashed on boot. Check sudo docker compose logs freshrss. Most often it's a volume permission issue - run sudo chown -R 33:33 /opt/freshrss/freshrss-data /opt/freshrss/freshrss-extensions (UID 33 is www-data inside the image) and restart.

Install wizard shows red on the Checks page. The data directory isn't writable by the container. Same fix as above: sudo chown -R 33:33 /opt/freshrss/freshrss-data, then reload the page.

Logins redirect in a loop or links point to http://. FreshRSS isn't trusting the proxy headers. Confirm TRUSTED_PROXY is set in the compose file and matches the Docker network range, then recreate the container with sudo docker compose up -d.

Mobile app says "authentication failed." You're using your login password instead of the API password, or the API endpoint URL is wrong. Set an API password under Settings → Profile, and point the client at https://rss.example.com/api/greader.php exactly.

Feeds never update on their own. CRON_MIN was empty or malformed. Check sudo docker compose logs freshrss | grep -i cron, confirm the value is quoted like "*/20", and recreate the container.

Going Further

  • Add extensions. FreshRSS has a community extensions repo for things like full-text article fetching, reading-time estimates, and custom themes. Drop them in the freshrss-extensions volume and enable them in settings.
  • Full-text feeds. Many sites publish summary-only feeds. The built-in "Article CSS selector" per feed, or extensions like xExtension-FreshRSS-Extension-FullText, pull the whole article into the reader so you never leave it.
  • Share a public folder. FreshRSS can publish a category as a read-only public page or its own RSS feed - handy for a curated "what I'm reading" stream you can hand to others.
  • Run it alongside other tools. This same Caddy reverse-proxy pattern powers our guides for Memos and ntfy. One small VPS happily hosts all three on separate subdomains.

Self-hosted FreshRSS is the kind of service you set up once and forget about. The feeds keep flowing, the apps keep syncing, and the next time a hosted reader shuts down you'll read the news about it in your own reader.


Need a small VPS to run a stack like this? Our Linux plans include fast NVMe storage, IPv6, and plenty of headroom for Docker workloads. See the options.