SSD Nodes Learn 8GB RAM — $66/yr
Guides Matt ConnorBy Matt Connor

Self-hosted RSS: install FreshRSS on a VPS

Install FreshRSS on your own Ubuntu VPS with Apache, PHP and MariaDB: the release tarball, virtual host, cron refresh, and the mobile app API.

What you are building

A self-hosted RSS reader is a feed reader that runs on a server you own, so nobody can shut it down or change what it shows you. This guide puts FreshRSS on an Ubuntu 24.04 VPS: Apache in front, PHP behind it, MariaDB for storage, and one cron job that fetches new articles. RSS (really simple syndication) is the file format a site publishes so software can read its articles. FreshRSS is the PHP application that collects those files, keeps the articles, and gives you a web interface plus an API (application programming interface) that phone apps speak.

The install itself is small: unpack a release, create a database, write one virtual host, run one command line installer. Most of the work below is the part people get wrong afterwards, which is the refresh job, encoded slashes for the mobile API, and file ownership.

FreshRSS 1.29.1 is the current release as of July 2026, and it needs PHP 8.1 or newer. Ubuntu 24.04 ships PHP 8.3, so the distribution packages are enough and you do not need a third party PHP repository.

Start from a working LAMP stack

FreshRSS is an ordinary PHP application, so it needs the same base as any other one. If you have not built that base yet, follow the LAMP stack setup for Ubuntu 24.04 first and come back here. The short version:

sudo apt update
sudo apt install -y apache2 mariadb-server php libapache2-mod-php
sudo systemctl enable --now apache2 mariadb

systemctl status apache2 should report active (running). If Apache will not start, the usual cause is another process already holding port 80, and sudo ss -ltnp | grep :80 names it.

The PHP extensions FreshRSS needs

FreshRSS treats libxml, cURL, JSON, PDO_MySQL, PCRE and ctype as mandatory. It also wants mbstring, iconv, Zlib and ZipArchive, and GMP on a 32 bit system. On Ubuntu these arrive as distribution packages:

sudo apt install -y php-curl php-mbstring php-xml php-zip php-mysql php-intl php-gmp
sudo systemctl restart apache2

Check what PHP actually loaded with php -m. A missing extension does not stop the installer from starting. It stops it at the requirements screen with a red line naming the extension, which is a confusing place to discover the problem, so confirm now. Restarting Apache matters because libapache2-mod-php keeps PHP inside the Apache process, so a newly installed extension is invisible to the web server until it restarts.

Download the release

Install FreshRSS outside the default web root, and point Apache at it. Keeping the application directory separate from the document root means only the public folder is ever reachable over HTTP.

cd /tmp
curl -fsSLO https://github.com/FreshRSS/FreshRSS/archive/refs/tags/1.29.1.tar.gz
tar xzf 1.29.1.tar.gz
sudo mv FreshRSS-1.29.1 /srv/freshrss

Now the permissions, which the FreshRSS documentation is strict about: the web server user owns the tree, the group can read every file, and the group can write to ./data/.

sudo chown -R www-data:www-data /srv/freshrss
sudo chmod -R g+r /srv/freshrss
sudo chmod -R g+w /srv/freshrss/data

Skip this and the installer fails while writing its configuration, because PHP runs as www-data and www-data cannot write into a directory owned by root.

Create the database

FreshRSS supports SQLite, MariaDB, MySQL and PostgreSQL. SQLite needs no setup and is fine for one person with a few hundred feeds. MariaDB is the better choice once several people share the instance, because concurrent writes from the refresh job and the web interface no longer contend for a single file lock.

sudo mariadb -e "CREATE DATABASE freshrss CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
sudo mariadb -e "CREATE USER 'freshrss'@'localhost' IDENTIFIED BY 'ReplaceThisPassword';"
sudo mariadb -e "GRANT ALL PRIVILEGES ON freshrss.* TO 'freshrss'@'localhost';"
sudo mariadb -e "FLUSH PRIVILEGES;"

Use utf8mb4 and not utf8. Feeds carry emoji and non-Latin scripts, and the old three byte utf8 encoding truncates an article title at the first four byte character.

The Apache virtual host

The public directory is p/, not the top of the tree. Everything else, including the configuration file holding your database password, sits above the document root where Apache will never serve it.

<VirtualHost *:80>
	ServerName rss.example.com
	DocumentRoot /srv/freshrss/p/

	<Directory /srv/freshrss/p>
		AllowOverride AuthConfig FileInfo Indexes Limit
		Require all granted
	</Directory>

	ErrorLog ${APACHE_LOG_DIR}/freshrss_error.log
	CustomLog ${APACHE_LOG_DIR}/freshrss_access.log combined

	AllowEncodedSlashes On
</VirtualHost>

Save that as /etc/apache2/sites-available/freshrss.conf and enable it:

sudo a2enmod rewrite
sudo a2ensite freshrss
sudo a2dissite 000-default
sudo apache2ctl configtest
sudo systemctl reload apache2

configtest should print Syntax OK. AllowEncodedSlashes On looks optional and is not: the Google Reader API sends feed identifiers containing %2F, and without this directive Apache rejects them, so mobile apps fail to sync while the web interface works perfectly.

Add HTTPS before you log in

You are about to type a password into this site, so get a certificate first. Point an A record at the server, then follow the Certbot setup for Apache on Ubuntu and run sudo certbot --apache -d rss.example.com. Certbot rewrites the virtual host for port 443 and adds the redirect. Confirm with curl -I https://rss.example.com/, which should return a 200 or a redirect to the login page.

Run the installer from the command line

FreshRSS has a browser installer, but the command line version is repeatable and leaves a record of exactly what you chose.

sudo -u www-data php /srv/freshrss/cli/do-install.php \
  --default-user admin --auth-type form --environment production \
  --base-url https://rss.example.com --language en --api-enabled \
  --db-type mysql --db-host localhost --db-user freshrss \
  --db-password 'ReplaceThisPassword' --db-base freshrss
sudo -u www-data php /srv/freshrss/cli/create-user.php \
  --user admin --password 'a-long-passphrase' --api-password 'a-different-passphrase'

Run both as www-data. Running them as root creates configuration files root owns, and the web interface then fails to save any setting later. --environment production matters too, because the development setting prints PHP notices into the page.

Load https://rss.example.com/ and log in as admin.

Why feeds do not refresh on their own

Nothing polls your feeds until you tell it to. FreshRSS refreshes while a browser is open on it, which means an instance you visit twice a day shows you articles that are twelve hours stale. The fix is the script the project ships for this, app/actualize_script.php, run from cron.

sudo crontab -u www-data -e

Add one line:

*/20 * * * * php /srv/freshrss/app/actualize_script.php > /tmp/FreshRSS.log 2>&1

Twenty minutes is the sensible floor, because the script refuses to refresh any single feed more often than once every twenty minutes, so a tighter schedule only burns CPU. Run it once by hand first:

sudo -u www-data php /srv/freshrss/app/actualize_script.php

Healthy output names each feed it fetched and ends without a PHP error. If it prints nothing at all, the cron user is wrong, and a permission error on data/ means the chmod -R g+w step was skipped.

Add your first feeds

Use the plus button at the top left of the interface, paste a site address, and FreshRSS discovers the feed link for you. Most sites still publish one even when they do not advertise it, usually at /feed, /rss or /atom.xml. Categories are folders, and moving a feed between them is a drag.

Coming from another reader, export an OPML file there and import it under the subscription management page. OPML (outline processor markup language) is the standard feed list format, and every reader worth leaving supports it. A large import is slow on the first refresh because every feed is fetched once, so give the first cron run time before you judge the speed.

Read it on your phone

FreshRSS speaks the Google Reader API, which almost every RSS app supports. Two things must be true. Under the authentication settings, "Allow API access" has to be on, which the --api-enabled flag above already set. Under your profile, the API password field has to hold a value, and it is deliberately separate from your login password because a phone is an easier device to lose.

Visit https://rss.example.com/api/ and choose "Check full server configuration". A working setup returns PASS. A failure here is almost always the missing AllowEncodedSlashes On line. In the app, give the server address as https://rss.example.com/api/greader.php, the username as your FreshRSS user, and the API password as the password.

The Docker alternative

If you would rather not maintain PHP and Apache by hand, the project publishes an official freshrss/freshrss image, and one compose file gives you the application and its database together. The trade is the usual one: fewer moving parts on the host, one more layer to debug when something breaks, and a reverse proxy still needed for TLS (transport layer security). If that suits you better, the Docker Compose basics for a VPS cover the file format, and the cron line becomes docker exec --user www-data freshrss php ./app/actualize_script.php.

Backups and upgrades

Two things hold your state: the database, and /srv/freshrss/data/. Dump the first with sudo mysqldump freshrss > freshrss.sql, copy the second, and keep both somewhere other than this server. Your subscription list is also worth an occasional OPML export, since that file rebuilds your reading setup on any RSS software at all.

Upgrading is unpacking a newer release over the same directory and re-running the ownership commands. FreshRSS applies its own database migrations on the next page load. Back up first, because a failed migration on a database you cannot restore is an unrecoverable position. A reader is a low risk service to run and a good first candidate if you are working through a list of what is worth self-hosting.

FAQ

Why do my feeds only update when I open FreshRSS?

Because no scheduler exists until you create one. FreshRSS refreshes feeds while a browser session is open and does nothing when the tab is closed. Add the cron line calling app/actualize_script.php as the www-data user, then run the script by hand once and read its output. Silence usually means cron is running it as the wrong user, so PHP cannot write to data/.

My mobile app cannot connect, but the website works. Why?

The Google Reader API puts encoded slashes (%2F) inside request paths, and Apache rejects those by default. Add AllowEncodedSlashes On inside the virtual host and reload Apache. Confirm the fix by opening https://rss.example.com/api/ and running "Check full server configuration", which should report PASS. Also check that the API password is set in your profile, since it is separate from your login password.

Should I use SQLite or MariaDB?

SQLite for a single user, because there is nothing to install and no password to manage. MariaDB once more than one person reads on the instance, or once you pass a few hundred feeds, because the refresh job and the web interface write at the same time and a single file lock becomes the limit. Moving between them later is possible through the export and import commands, so this is not a permanent decision.

The installer fails when it writes its configuration. What is wrong?

PHP runs as www-data under Apache, and that user cannot write into a directory owned by root. Re-run sudo chown -R www-data:www-data /srv/freshrss and sudo chmod -R g+w /srv/freshrss/data, then start the installer again. If you already ran the command line installer as root, delete the files it created under data/ before retrying, because their ownership is the actual problem.

How much server does a self-hosted RSS reader need?

Very little. A few hundred feeds on a small plan is comfortable, since the load is short bursts of HTTP fetching every twenty minutes and the database stays small once old articles are purged. Disk grows with retention, so set an article purge policy in the archiving settings rather than keeping everything forever.

#freshrss#rss#self-hosting#php#apache