What you are building
You are building a private news desk: one web page that collects every site, blog and channel you follow, marks what you have read, and syncs that state to your phone. FreshRSS fetches the feeds on the server, so your devices only talk to your own address. If you already keep recipes or notes at home, our guides to self-hosting Mealie and Joplin Server follow the same Docker pattern.
Its own documentation says its requirements are really low. The official Docker image already contains the Apache web server and PHP, so your server needs Docker and nothing else. The image is built for 64-bit Intel and AMD, 64-bit ARM and 32-bit ARMv7.
Will a small server be enough?
Yes, for a feed reader on its own. Our dedicated servers start with compact cartridge machines that the page describes as ideal when you have one job to run, and a whole cartridge is still a whole computer: its cores, memory and disk are yours, the port is unmetered, and nobody else is on it. If the same box will also carry a website or a few other apps, the everyday single-processor servers are where the page suggests most people start.
- Want to be able to rescue it yourself? The console screen and power button come with rack servers and blades, plus m510 and m710x cartridges. On the other cartridges you get power control alone, and you would reinstall from the control panel instead.
- For this job, pass over the X-Gene ARM cartridge. It installs nothing newer than Ubuntu 18.04 (16.04 is the other choice), and this guide needs a current Docker.
- One IPv4 address is included and IPv6 costs nothing, so phone apps can reach you either way.
SQLite or PostgreSQL: decide first
For one person or a household, keep SQLite. The Docker README calls the built-in SQLite database the easiest, with good performance, and it lives inside the same data volume you back up. PostgreSQL 10 or newer is what the prerequisites page recommends, with MariaDB 10.6+ or MySQL 8.0+ as alternatives; the official docker-compose-db.yml adds a PostgreSQL container if you expect a crowd. Switching later is possible with the CLI's backup and restore commands, but it is a maintenance evening, so choose now.
Up and running in an evening
Begin on an Ubuntu server with Docker Engine 28+ plus the Compose plugin. The first three lines switch on IPv6 in the firewall before any rule is added, then lock everything except SSH. The Compose file follows the official example, with two changes: the port is reachable from the server itself only, and the version is fixed so nothing upgrades behind your back.
sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw
ufw allow OpenSSH
ufw --force enable
mkdir -p /srv/freshrss
cd /srv/freshrss
cat > compose.yaml <<'EOF'
services:
freshrss:
image: freshrss/freshrss:1.30.4
container_name: freshrss
hostname: freshrss
restart: unless-stopped
logging:
options:
max-size: 10m
ports:
- "127.0.0.1:8080:80"
volumes:
- data:/var/www/FreshRSS/data
- extensions:/var/www/FreshRSS/extensions
environment:
TZ: America/New_York
CRON_MIN: '3,33'
TRUSTED_PROXY: 172.16.0.1/12 192.168.0.1/16
healthcheck:
test: ["CMD", "cli/health.php"]
timeout: 10s
start_period: 60s
interval: 75s
retries: 3
volumes:
data:
extensions:
EOF
docker compose up -d
docker compose ps
Why 127.0.0.1:8080? A Docker port mapping with no address goes out on 0.0.0.0 and [::]: all of the server's IPv4 and IPv6 addresses. On IPv4 that traffic slips past ufw; on IPv6 it does so only if Docker runs its own IPv6 networking, and otherwise lands on docker-proxy, which ufw filters. Binding to 127.0.0.1 sidesteps all of it, and from Engine 28 onward a port bound that way answers locally alone (moby#45610).
There is no password in this file: with SQLite, the only secret is the admin password you type into the wizard. If you add PostgreSQL, put its credentials in an env file created under umask 077 (mode 600), each value in single quotes.
Finish the setup before anyone else can
A fresh FreshRSS shows its install wizard to the first visitor, and that visitor creates the admin account. So you do this part alone, using an SSH tunnel, while the site still has no public address. The sequence is our suggestion; the settings are from the docs.
- On your own computer run
ssh -N -L 9090:127.0.0.1:8080 you@your-server(your own SSH account, not a service user), and openhttp://localhost:9090in a browser. - Choose a language, pass the system check, and leave the database type on SQLite.
- Pick your username and a long password, with the default web form login. This first user is the instance admin.
- The wizard saves the address you used as the base URL (it reads it from the request, in
app/install.php), so it now says localhost. Correct it on the server:docker compose exec --user www-data freshrss cli/reconfigure.php --base-url https://rss.example.com. - In Administration, System configuration, make sure "Max number of accounts" reads 1. The shipped default is already 1 (
max_registrationsinconfig.default.php); the docs FAQ says that value turns self-registration off, because the admin account takes the one place. You can still create accounts for family members yourself.
Putting it on your own address
Now point a name such as rss.example.com at the server and let nginx carry HTTPS. Opening ports 80 and 443 in ufw matters here: nginx is an ordinary program on the server, so without that rule visitors are turned away on both IPv4 and IPv6. The server block is adapted from the official domain-root example: it keeps the proxy settings and starts on port 80 only, so certbot can add HTTPS and the redirect.
ufw allow 80,443/tcp
apt install -y nginx certbot python3-certbot-nginx
cat > /etc/nginx/sites-available/rss.example.com <<'EOF'
server {
listen 80;
listen [::]:80;
server_name rss.example.com;
location / {
# The final / is important (FreshRSS Docker README)
proxy_pass http://127.0.0.1:8080/;
proxy_redirect off;
proxy_buffering off;
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_read_timeout 90;
# Forward the Authorization header for the Google Reader API
proxy_set_header Authorization $http_authorization;
proxy_pass_header Authorization;
}
}
EOF
ln -s /etc/nginx/sites-available/rss.example.com /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
certbot --nginx -d rss.example.com
Keep the two Authorization lines: the official example adds them so the Google Reader API that phone apps use receives its Authorization header. The TRUSTED_PROXY setting from the official Compose file tells FreshRSS which addresses may pass on the visitor's real IP. If you prefer a point-and-click front end, our guide to Nginx Proxy Manager covers that route.
Feeds that refresh themselves
CRON_MIN does it. Leave it out and the container starts no scheduler at all. Set it to minutes past the hour, like 3,33, and a cron job inside the container fetches new articles at those minutes. The docs suggest refreshing about twice an hour, and the update script never fetches the same feed more than once in 20 minutes, so a busier schedule buys you nothing.
The docs list two other ways: a cron entry on the server itself calling app/actualize_script.php through docker exec, or a second copy of the image whose only job is cron. Use one method, not several.
Reading on your phone
Your phone app talks to FreshRSS through one of two APIs. The Google Reader API is the one the project rates best; the Fever API has fewer features and the README calls it less efficient and less safe. The API is switched off out of the box (api_enabled is false in config.default.php).
- In the settings, under Authentication, tick "Allow API access (required for mobile apps)".
- Under Profile, set an API password. It is separate from your login on purpose: the docs say it may end up in less safe places and opens fewer doors.
- Follow the API link beside that field and run "Check full server configuration". PASS means you are ready.
- In the app, enter
https://rss.example.com/api/greader.php, your username and the API password.
Backups, updates and the licence
You manage the server; we keep the hardware and the network running, so the backups are yours to set up. According to the backup docs, the data folder must be saved (its cache can be left out) and extensions is worth keeping. The script below first runs cli/db-backup.php, which saves every user's database as a backup.sqlite file, then packs both folders into a dated archive every night at 04:15.
mkdir -m 700 -p /var/backups/freshrss /var/log/freshrss-backup
cat > /srv/freshrss/nightly-backup.sh <<'EOF'
set -eu
umask 077
cd /srv/freshrss
docker compose exec -T --user www-data freshrss cli/db-backup.php -q
f=/var/backups/freshrss/data-$(date +%F).tgz
docker compose exec -T freshrss tar -czf - --exclude=data/cache -C /var/www/FreshRSS data extensions > "$f.tmp"
mv "$f.tmp" "$f"
find /var/backups/freshrss -name 'data-*.tgz' -mtime +7 -delete
EOF
printf '%s\n' 'SHELL=/bin/sh' \
'15 4 * * * root umask 077; bash /srv/freshrss/nightly-backup.sh > /var/log/freshrss-backup/$(date +\%F).log 2>&1' \
> /etc/cron.d/freshrss-backup
Seven nights of archives is just a starting point. Copy them to another computer regularly, because a backup kept only on the same disks is lost with them. When you need one back, work from /srv/freshrss:
- First prove the archive is there and readable:
tar -tzf /var/backups/freshrss/data-2026-10-23.tgz > /dev/null && echo good. Stop here unless you see good, because the next steps replace what is on the server. - Stop the reader with
docker compose stop freshrss. - Unpack it into the volumes:
docker compose run --rm -T --entrypoint tar freshrss -xzf - -C /var/www/FreshRSS < /var/backups/freshrss/data-2026-10-23.tgz. - Reload the databases from the restored copies:
docker compose run --rm freshrss cli/db-restore.php --force-overwrite --delete-backup. Finally start it again:docker compose up -d.
To update, take a backup, change the version in compose.yaml, and run docker compose pull followed by docker compose up -d; your volumes carry over. The docs ask you to move one version at a time and check that everything works between steps.
FreshRSS ships under the GNU AGPL 3. As a general reading, not legal advice: using it unchanged, for yourself, asks nothing of you. Change the code and let other people use your copy over the internet, and you must offer them your changed source; give someone a copy, such as a modified container image, and the usual GPL duty to share the source comes with it.
Questions people ask
- Is FreshRSS free to run on my own server?
- Yes. It is published under the GNU AGPL 3, a free software licence. Your costs are the server and your own time keeping it updated.
- Do I need a separate database server?
- No. SQLite is built into FreshRSS and stores everything in the data volume, which is the easy path for one reader. PostgreSQL, MariaDB and MySQL are supported if you want one; the prerequisites page recommends PostgreSQL.
- Which phone apps work with it?
- Apps that speak the Google Reader API or the Fever API. The FreshRSS README keeps a table of known apps for Android, iOS, Linux, Windows and macOS, with the API each one uses. Turn on API access and set an API password first.
- How often should FreshRSS check for new articles?
- About twice an hour, as the docs suggest. With Docker,
CRON_MINset to two minute marks, such as3,33, does exactly that. Faster than every 20 minutes is pointless, because the script will not refetch a feed sooner. - Can people sign up on my reader without asking?
- Not unless you allow it. The maximum number of accounts ships at 1, which is filled by your admin user, so the sign-up form stays shut. You can add accounts yourself from Administration whenever you like.