FreshRSS is a lightweight, self-hosted RSS and Atom feed aggregator with multi-user support, extensions and APIs that mobile and desktop feed readers can sync with. In this tutorial you will run FreshRSS and PostgreSQL with Docker Compose on Ubuntu 24.04, publish it over HTTPS with Nginx, import your feeds, connect a mobile app through the Google Reader API and set up backups.
Prerequisites
To follow this guide you need:
- A server running Ubuntu 24.04 LTS, for example a CubePath VPS. FreshRSS is light: 1 GB of RAM is enough for a few users.
- A non-root user with
sudoprivileges. - Docker Engine and the Docker Compose plugin installed. See How to install Docker on Linux.
- A domain name, referred to as
your_domain(for examplerss.example.com), with a DNS A record pointing to your server.
Step 1 - Writing the Docker Compose file
Create the project directory. The extensions folder holds third-party extensions (Step 6):
sudo mkdir -p /opt/freshrss/extensions
cd /opt/freshrss
Generate a database password and store it in a .env file, which Docker Compose reads automatically:
echo "DB_PASSWORD=$(openssl rand -hex 24)" | sudo tee /opt/freshrss/.env > /dev/null
sudo chmod 600 /opt/freshrss/.env
Create the Compose file:
sudo nano /opt/freshrss/compose.yaml
services:
freshrss:
image: freshrss/freshrss:latest
restart: unless-stopped
depends_on:
- freshrss-db
ports:
- "127.0.0.1:8080:80"
environment:
TZ: Europe/Madrid
CRON_MIN: "3,18,33,48"
TRUSTED_PROXY: 172.16.0.1/12 192.168.0.1/16
volumes:
- data:/var/www/FreshRSS/data
- ./extensions:/var/www/FreshRSS/extensions
freshrss-db:
image: postgres:17
restart: unless-stopped
environment:
POSTGRES_DB: freshrss
POSTGRES_USER: freshrss
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- db:/var/lib/postgresql/data
volumes:
data:
db:
What these settings do:
CRON_MINmakes the container refresh all feeds at minutes 3, 18, 33 and 48 of every hour. You do not need a cron job on the host.TRUSTED_PROXYtells FreshRSS to trust the forwarded headers sent by Nginx, which reaches the container through the Docker bridge network. Without it, FreshRSS logs the proxy's address instead of the client's and may build wrong URLs.127.0.0.1:8080:80publishes the web port only on localhost. Ports published by Docker bypass UFW, so this keeps FreshRSS reachable only through Nginx.
Step 2 - Starting FreshRSS
Start both containers:
cd /opt/freshrss
sudo docker compose up -d
Check that they are running and that FreshRSS answers on the local port:
sudo docker compose ps
curl -sL -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/
200
FreshRSS serves its installation wizard, which you will complete after setting up HTTPS.
Step 3 - Configuring Nginx and HTTPS
Install Nginx and Certbot:
sudo apt update
sudo apt install nginx certbot python3-certbot-nginx
Create the server block:
sudo nano /etc/nginx/sites-available/freshrss
server {
listen 80;
listen [::]:80;
server_name your_domain;
location / {
proxy_pass http://127.0.0.1:8080/;
proxy_redirect off;
proxy_buffering off;
proxy_read_timeout 90s;
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;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header Authorization $http_authorization;
proxy_pass_header Authorization;
}
}
The Authorization lines make sure API clients' credentials reach FreshRSS.
Enable the site, open the firewall and request a certificate:
sudo ln -s /etc/nginx/sites-available/freshrss /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo certbot --nginx -d your_domain
Step 4 - Running the installation wizard
Open https://your_domain in your browser. The wizard checks the PHP environment and then asks for the database. Enter:
| Field | Value |
|---|---|
| Type of database | PostgreSQL |
| Host | freshrss-db |
| Database username | freshrss |
| Database password | the value of DB_PASSWORD in /opt/freshrss/.env |
| Database | freshrss |
| Table prefix | freshrss_ (default) |
Print the password with:
sudo cat /opt/freshrss/.env
In the next screen, create the administrator account and keep the Web form authentication method. When the wizard finishes, log in with the new account.
To confirm that the installation is complete and the database is reachable, list the users from the command line. FreshRSS's CLI tools must run as the www-data user inside the container:
cd /opt/freshrss
sudo docker compose exec --user www-data freshrss cli/list-users.php
admin
Step 5 - Adding feeds and setting retention
To add a feed, click Subscription management (or the + next to the feed list), paste the URL of a site or its RSS/Atom feed and choose a category.
If you are moving from another reader, export your subscriptions as an OPML file there and import it in FreshRSS under Subscription management > Import / export.
To check that refreshing works without waiting for the next scheduled run, refresh your user's feeds from the command line:
sudo docker compose exec --user www-data freshrss cli/actualize-user.php --user admin
New articles then appear in the main view. From now on, the container refreshes feeds automatically at the minutes set in CRON_MIN.
Old articles accumulate in the database over time. Under Settings > Archiving, set how long to keep articles and the maximum number to keep per feed. Starred articles are always kept.
Step 6 - Installing an extension
Extensions add features such as embedded YouTube players or custom styling. The official collection lives in the FreshRSS/Extensions repository. Clone it to a temporary directory and copy the extension you want, for example the YouTube extension, into the mounted extensions folder:
sudo apt install git
git clone --depth 1 https://github.com/FreshRSS/Extensions.git /tmp/freshrss-extensions
sudo cp -r /tmp/freshrss-extensions/xExtension-YouTube /opt/freshrss/extensions/
Open Settings > Extensions. The YouTube extension appears in the list; click the toggle to enable it. Only administrators can enable system-wide extensions, while user extensions are enabled per account.
Step 7 - Connecting mobile and desktop apps
FreshRSS implements the Google Reader API and the Fever API. The Google Reader API supports categories and read status sync, so prefer it when your app offers both.
First allow API access for the whole instance as administrator: Settings > Authentication > check Allow API access and save.
Then each user sets a dedicated API password under Settings > Profile > API management. Apps use this password instead of the login password, so a leaked phone app password does not expose the web account.
Test the Google Reader API from your computer, replacing the username and API password:
curl 'https://your_domain/api/greader.php/accounts/ClientLogin?Email=admin&Passwd=your_api_password'
SID=admin/0f1e...
LSID=null
Auth=admin/0f1e...
An Auth= line means the API works. You can also open https://your_domain/api/ in a browser, which runs a self-check and shows the endpoint URLs.
In your app, choose FreshRSS or "Google Reader API" as the account type and enter:
Server: https://your_domain/api/greader.php
Username: your FreshRSS username
Password: your API password
Apps that support FreshRSS this way include NetNewsWire (iOS and macOS), Read You and FeedMe (Android), and Fluent Reader (desktop).
Step 8 - Adding users and backing up
To create an account for someone else from the command line:
sudo docker compose exec --user www-data freshrss cli/create-user.php --user alice --password 'a_strong_password'
Administrators can also manage users under Settings > Manage users.
For backups, dump the PostgreSQL database, which holds all users' feeds, articles and settings:
sudo mkdir -p /var/backups/freshrss
cd /opt/freshrss
sudo docker compose exec -T freshrss-db pg_dump -U freshrss freshrss \
| gzip | sudo tee /var/backups/freshrss/freshrss-$(date +%F).sql.gz > /dev/null
Also export each user's subscription list as OPML, which is useful to move to another reader:
sudo docker compose exec -T --user www-data freshrss cli/export-opml-for-user.php --user admin \
| sudo tee /var/backups/freshrss/admin-$(date +%F).opml > /dev/null
Copy /var/backups/freshrss off the server with your usual backup tool, and schedule both commands in /etc/cron.d if you want daily copies.
Updating FreshRSS
Take a database dump first, then pull the new images and recreate the containers. FreshRSS updates its database schema automatically:
cd /opt/freshrss
sudo docker compose pull
sudo docker compose up -d
Keep the PostgreSQL major version (postgres:17) pinned. Moving to a new major version requires a dump and restore, not just a new image.
Troubleshooting
Feeds stop refreshing. Check the container log with sudo docker compose logs --tail 50 freshrss and confirm CRON_MIN is set. Run cli/actualize-user.php as shown in Step 5 to see errors for a specific user. Individual feeds with errors are marked in the feed list; open them to see the HTTP error returned by the site.
API login fails in the app but the web login works. Check that API access is allowed for the instance, that you are using the API password rather than the login password, and that the server URL ends in /api/greader.php. The https://your_domain/api/ self-check page points out common problems.
Wrong URLs or redirects to http:// or port 8080. FreshRSS does not trust the proxy headers. Make sure TRUSTED_PROXY is set as in Step 1 and the Nginx block sends X-Forwarded-Proto and X-Forwarded-Port, then run sudo docker compose up -d. The base URL detected during installation is stored as base_url in /var/www/FreshRSS/data/config.php inside the container; if it is wrong, correct it there and restart the container.
Conclusion
FreshRSS now runs on Ubuntu 24.04 with PostgreSQL behind Nginx with HTTPS, refreshes feeds every 15 minutes and syncs with mobile apps through the Google Reader API. As next steps, organize feeds into categories and filters (for example, marking articles with certain keywords as read automatically), create accounts for your team, and schedule the database dump with cron so your subscriptions and starred articles are always recoverable.
