SSD Nodes Learn Hosting plans →
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 the command line installer, then create the account with cli/create-user.php. 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.30.4, published on October 21, 2026, is the current release as of October 29, 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.30.4.tar.gz
tar xzf 1.30.4.tar.gz
sudo mv FreshRSS-1.30.4 /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 the php-sqlite3 extension but no database server setup, and stores each user's data in a separate data/users/_user_/db.sqlite file. Adding users does not put their writes into one shared SQLite file; this guide uses MariaDB as part of the LAMP stack.

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 cannot store four byte characters. Unsupported characters cause an error under MariaDB's default strict mode; truncation depends on SQL mode and statement options such as IGNORE.

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. Opening FreshRSS does not schedule feed fetching; without a scheduler, use the update link in the navigation menu to fetch articles manually. 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 a sensible schedule, but each feed's refresh interval still applies: the default is one hour, and individual feeds support intervals down to fifteen minutes. Run it once by hand first:

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

Healthy output reports the refresh run and ends without a PHP error; feed names appear when articles are added or updated. If it prints nothing at all, check PHP's error output and the FreshRSS logs; silence alone does not identify the cron user as the cause. A permission error on data/ calls for checking ownership and write permissions.

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 many RSS apps support. 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/ to run the server configuration checks automatically. A working setup returns PASS. If the check fails, check the virtual host's AllowEncodedSlashes On setting and the reported error. 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 transfers your feed list to other readers that support OPML. It does not preserve FreshRSS-specific settings such as refresh frequency, credentials, user agent or XPath web scraping rules; use a SQLite export to preserve those too.

Upgrading is unpacking a newer release over the same directory and re-running the ownership commands. Follow the project's version-by-version upgrade path and verify FreshRSS after each step. 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. If you installed from an older tarball such as 1.29.1, move to 1.30.4: the 1.30.x security releases include session ID rotation on authentication changes and bounds on ZIP imports, while 1.30.4 also updates the alternative Alpine Docker images for severe PHP vulnerabilities. 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?

Opening the page does not schedule feed fetching; without a scheduler, use the update link to refresh manually. 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. If it is silent, check PHP's error output and the FreshRSS logs, then verify that the cron user has write access 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/, where the automatic configuration check 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 needs the php-sqlite3 extension, but no database server or database password to manage. Each user gets a separate SQLite database file, so adding a second user does not create contention on one shared file or require MariaDB. Use MariaDB if you want to use the database server in your LAMP stack. 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, correct the ownership of the files it created under data/ before retrying.

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

Very little. FreshRSS is designed to run on modest hardware, but resource use depends on your feeds, article sizes and retention settings. Disk grows with retention, so set an article purge policy in the archiving settings rather than keeping everything forever.

Was this article useful?