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.comat the server - Run
freshrss/freshrssbehind Caddy in onedocker-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
datavolume 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
80and443open 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
80internally. 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_PROXYtells FreshRSS to trust theX-Forwarded-*headers from Caddy. Without it, FreshRSS may buildhttp://links behind yourhttps://proxy and mangle logins. The172.16.0.0/12range covers the default Docker bridge networks.- Pin a real version tag (
1.24.3here) rather thanlatestso an unattendeddocker compose pullcan't surprise you with a major upgrade.
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:
- Language - pick yours and continue.
- 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).
- Database - choose SQLite. There's nothing else to fill in; FreshRSS creates the database file inside your
datavolume. - General configuration - create your admin username and a strong password. This is the account you'll log in with daily.
- 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.rssto 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:
- Go to Settings → Profile.
- Find API management and set an API password. This is separate from your login password - treat it as an app token.
- 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.
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.
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-extensionsvolume 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.