FreshRSS is a free, self-hosted RSS and Atom aggregator written in PHP. It handles thousands of feeds per user, supports multiple users, WebSub push updates, web scraping for partial-content feeds, saved searches and filters, labels, themes, and a large extension ecosystem. Google Reader and Fever compatible APIs let you use it with mobile and desktop clients such as Reeder, NetNewsWire, Read You, and FeedMe.
This guide deploys FreshRSS 1.30.0 on Ubuntu 24.04 LTS using the official Docker image with a PostgreSQL 17 database in Docker Compose, a fully automated first-run install, built-in cron feed refreshes, and Nginx on the host as the TLS-terminating reverse proxy.
Why 1.30.0 matters: 1.30.0 is a security-focused release that patches several SSRF, CSRF, and access-control issues. If you run an older FreshRSS anywhere, upgrade it. The project now also recommends its rolling
edgechannel for faster security patches; this guide pins stable releases for predictability and covers the tradeoff under Updating.
What You Will Build
- Docker Engine and the Compose plugin from Docker's official apt repository
- A Compose stack in
/opt/freshrsswithfreshrss/freshrss:1.30.0andpostgres:17-alpine - Unattended install and admin user creation via environment variables
- Feed refresh twice an hour through the image's built-in cron
- FreshRSS published only on
127.0.0.1:8081, fronted by Nginx with Let's Encrypt - Nightly database dumps plus OPML exports
Server Sizing
| Use case | vCPU | RAM | Disk | Notes |
|---|---|---|---|---|
| Single user, a few hundred feeds | 1 | 1 GB | 15 GB | Fine with 1 GB swap |
| Household or small team, up to ~2,000 feeds | 2 | 2 GB | 25 GB | Recommended starting point |
| Many users or heavy web scraping | 2 to 4 | 4 GB | 50 GB+ | Scraping and full-text retention grow the database |
Prerequisites
- A RamNode VPS running Ubuntu 24.04 LTS
- Root or sudo access over SSH
- A DNS
Arecord (andAAAAfor IPv6) for a hostname such asreader.example.com
Replace reader.example.com throughout with your hostname.
Step 1: Prepare the System
sudo apt update && sudo apt -y full-upgrade
sudo apt -y install ca-certificates curl gnupg nginx certbot python3-certbot-nginx ufw unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgradesFirewall:
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enableDocker and UFW: Docker writes its own iptables rules, and ports published as
8081:80bypass UFW entirely. This guide publishes FreshRSS on127.0.0.1:8081only and never publishes PostgreSQL, so nothing in the stack is exposed except through Nginx.
Step 2: Install Docker Engine
Add Docker's signing key and repository:
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 -y install docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo systemctl enable --now docker
docker compose versionCap container log growth so a chatty container cannot fill the disk:
sudo pico /etc/docker/daemon.json{
"log-driver": "json-file",
"log-opts": { "max-size": "10m", "max-file": "3" }
}sudo systemctl restart dockerStep 3: Create the Project Directory and Secrets
sudo mkdir -p /opt/freshrss/{data,extensions,db}
cd /opt/freshrssGenerate credentials:
echo "DB_PASSWORD=$(openssl rand -base64 32 | tr -d '/+=')"
echo "ADMIN_PASSWORD=$(openssl rand -base64 24 | tr -d '/+=')"
echo "ADMIN_API_PASSWORD=$(openssl rand -base64 24 | tr -d '/+=')"Create the environment file with those values:
sudo pico /opt/freshrss/.env# Public URL (no trailing slash)
BASE_URL=https://reader.example.com
# Timezone for the container and cron
TZ=America/New_York
# Database
DB_HOST=freshrss-db
DB_BASE=freshrss
DB_USER=freshrss
DB_PASSWORD=PASTE_DB_PASSWORD
# Initial admin user
ADMIN_USER=admin
ADMIN_EMAIL=you@example.com
ADMIN_PASSWORD=PASTE_ADMIN_PASSWORD
ADMIN_API_PASSWORD=PASTE_ADMIN_API_PASSWORDsudo chmod 600 /opt/freshrss/.envThe admin password is for the web UI. The API password is a separate credential that mobile apps use through the Google Reader and Fever APIs, so a leaked app config never exposes your web login.
Step 4: Write the Compose File
sudo pico /opt/freshrss/compose.yamlservices:
freshrss-db:
image: postgres:17-alpine
container_name: freshrss-db
restart: unless-stopped
environment:
POSTGRES_DB: ${DB_BASE}
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- ./db:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_BASE}"]
interval: 10s
timeout: 5s
retries: 5
freshrss:
image: freshrss/freshrss:1.30.0
container_name: freshrss
restart: unless-stopped
depends_on:
freshrss-db:
condition: service_healthy
ports:
- "127.0.0.1:8081:80"
volumes:
- ./data:/var/www/FreshRSS/data
- ./extensions:/var/www/FreshRSS/extensions
environment:
TZ: ${TZ}
# Refresh feeds at minute 3 and 33 of every hour
CRON_MIN: "3,33"
# Trust X-Forwarded-* from the Docker bridge (host Nginx arrives from here)
TRUSTED_PROXY: 172.16.0.0/12
FRESHRSS_INSTALL: |-
--api-enabled
--base-url ${BASE_URL}
--db-type pgsql
--db-host ${DB_HOST}
--db-base ${DB_BASE}
--db-user ${DB_USER}
--db-password ${DB_PASSWORD}
--default-user ${ADMIN_USER}
--language en
FRESHRSS_USER: |-
--user ${ADMIN_USER}
--password ${ADMIN_PASSWORD}
--api-password ${ADMIN_API_PASSWORD}
--email ${ADMIN_EMAIL}
--language enHow the pieces fit:
| Setting | Purpose |
|---|---|
127.0.0.1:8081:80 | Container's Apache reachable only from the host, never directly from the internet |
CRON_MIN | Enables the image's internal cron to run the feed actualizer at those minutes |
TRUSTED_PROXY | Lets FreshRSS read the real client IP and HTTPS scheme from Nginx's forwarded headers |
FRESHRSS_INSTALL | Runs the CLI installer on first boot when no config exists; skipped afterward |
FRESHRSS_USER | Creates the admin user on first boot |
Step 5: Start the Stack
cd /opt/freshrss
sudo docker compose pull
sudo docker compose up -d
sudo docker compose ps
sudo docker compose logs -f freshrssWait for the logs to show the install and user creation completing, then press Ctrl+C. Confirm the app responds:
curl -sI http://127.0.0.1:8081/i/ | head -n1Run the built-in health check:
sudo docker exec --user www-data freshrss php cli/health.phpStep 6: Configure Nginx and TLS
sudo pico /etc/nginx/sites-available/freshrssserver {
listen 80;
listen [::]:80;
server_name reader.example.com;
client_max_body_size 20M; # OPML imports
location / {
proxy_pass http://127.0.0.1:8081;
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-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_redirect off;
# Long-running manual refreshes and API sync
proxy_read_timeout 300s;
proxy_buffering off;
}
}Enable it and get a certificate:
sudo ln -s /etc/nginx/sites-available/freshrss /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d reader.example.com --redirect -m you@example.com --agree-tos --no-eff-email
sudo certbot renew --dry-runBrowse to https://reader.example.com and sign in with the admin username and password from .env.
Step 7: Post-Install Configuration
Lock down registration and authentication
Under Administration > System configuration:
- Registration: leave closed unless you are running a multi-user instance on purpose. You can set a custom closed-registration message.
- Authentication method: keep Web form (the default). For SSO, FreshRSS supports OpenID Connect and HTTP header auth from a trusted proxy.
- Log level: 1.30.0 adds a configurable
log_levelhere. Leave it at the default unless troubleshooting.
Feeds on local or private networks
1.30.0 is a breaking change here: FreshRSS now refuses to fetch from local networks such as 127.0.0.1 by default to block SSRF. If you subscribe to feeds hosted on the same VPS (an RSS-Bridge or RSSHub container, for example), allow only those hosts:
- Administration > System configuration has an internal host allowlist, or
- set
INTERNAL_HOST_ALLOWLISTin theenvironment:block of thefreshrssservice, thensudo docker compose up -d.
Avoid *, which restores the old allow-everything behavior and reopens the SSRF exposure.
Import feeds
Subscription management > Import / export accepts OPML, and since 1.29.1 also plain .txt files with one feed URL per line. OPML export and import now carry each feed's refresh interval and preserve category order.
Mobile and desktop clients
The API is already enabled by the installer. Point clients at:
| API | Endpoint |
|---|---|
| Google Reader | https://reader.example.com/api/greader.php |
| Fever | https://reader.example.com/api/fever.php |
Authenticate with your username and the API password, not the web password. Each user sets their API password under Settings > Profile. Clients should use POST; FreshRSS now logs a warning when a client authenticates via GET.
Extensions
Drop extension folders into /opt/freshrss/extensions, then enable them under Settings > Extensions. Popular picks include YouTube channel embeds, reading-time estimates, and image proxies. Only install extensions from sources you trust; they run as PHP inside the app.
Step 8: Backups
Capture the database, the data directory (config, user settings, caches), and an OPML export per user.
sudo pico /usr/local/bin/freshrss-backup#!/usr/bin/env bash
set -euo pipefail
cd /opt/freshrss
set -a; . ./.env; set +a
DEST=/var/backups/freshrss
STAMP=$(date +%F)
KEEP_DAYS=14
mkdir -p "$DEST/$STAMP"
chmod 700 "$DEST"
docker compose exec -T freshrss-db pg_dump -U "$DB_USER" -Fc "$DB_BASE" > "$DEST/$STAMP/freshrss.dump"
tar --exclude='data/cache' -czf "$DEST/$STAMP/freshrss-data.tar.gz" data extensions .env compose.yaml
docker exec --user www-data freshrss php cli/export-opml-for-user.php --user "$ADMIN_USER" \
> "$DEST/$STAMP/${ADMIN_USER}.opml"
find "$DEST" -mindepth 1 -maxdepth 1 -type d -mtime +$KEEP_DAYS -exec rm -rf {} +sudo chmod 750 /usr/local/bin/freshrss-backup
echo '45 3 * * * root /usr/local/bin/freshrss-backup' | sudo tee /etc/cron.d/freshrss-backup
sudo /usr/local/bin/freshrss-backup
ls -lh /var/backups/freshrss/$(date +%F)The archive includes .env, so treat backups as secrets and encrypt them when you ship them off-server (restic does this by default).
Restore the database into a fresh stack:
cd /opt/freshrss
sudo docker compose stop freshrss
sudo docker compose exec -T freshrss-db sh -c 'dropdb -U "$POSTGRES_USER" "$POSTGRES_DB" && createdb -U "$POSTGRES_USER" "$POSTGRES_DB"'
sudo docker compose exec -T freshrss-db sh -c 'pg_restore -U "$POSTGRES_USER" -d "$POSTGRES_DB"' \
< /var/backups/freshrss/YYYY-MM-DD/freshrss.dump
sudo docker compose start freshrssUpdating FreshRSS
Stable releases (this guide's default)
sudo /usr/local/bin/freshrss-backup
cd /opt/freshrss
sudo sed -i 's|freshrss/freshrss:1.30.0|freshrss/freshrss:NEW_VERSION|' compose.yaml
sudo docker compose pull freshrss
sudo docker compose up -d
sudo docker compose logs --tail=50 freshrssDatabase schema updates apply automatically on startup.
Edge (rolling) channel
The FreshRSS team now recommends the edge tag for faster security fixes. It tracks the development branch, so you trade some stability for patch speed. If you choose it, set image: freshrss/freshrss:edge, keep the nightly backups, and pull weekly.
PostgreSQL major upgrades
Do not change postgres:17-alpine to a new major version in place; the data directory format is incompatible. Dump with the backup script, start a new empty database container on the new version, and restore.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
| Feeds never refresh | Confirm CRON_MIN is set; check sudo docker compose logs freshrss for actualize output; run a manual refresh with sudo docker exec --user www-data freshrss php app/actualize_script.php |
| Local feed fails after upgrading to 1.30.0 | New local-network block. Add the host to the internal allowlist or INTERNAL_HOST_ALLOWLIST |
| Login loops or mixed-content warnings | BASE_URL must use https://; confirm TRUSTED_PROXY covers the Docker bridge and Nginx sends X-Forwarded-Proto |
Install warning that document root does not point to ./p/ | Only applies to bare-metal installs; the Docker image is already configured correctly |
| Mobile app authentication fails | Use the API password, verify the endpoint path, and check that the API is enabled under Authentication |
| Regex searches throw SQL errors | Fixed in 1.30.0 for MySQL/MariaDB; PostgreSQL deployments were not affected |
502 Bad Gateway | Container down or unhealthy: sudo docker compose ps and inspect logs |
| Disk filling up | Check docker system df; prune old images with sudo docker image prune -a; tighten archiving under Settings > Archiving |
Security Checklist
- FreshRSS on 1.30.0 or newer (security release)
- App published on loopback only; PostgreSQL never published
.envand backups readable by root only- Registration closed unless intentionally multi-user
- Separate API passwords for mobile clients
- Local-network fetching limited to an explicit allowlist, never
* - Container logs capped; images pruned periodically
- Off-server encrypted backups with a tested restore
FreshRSS or Miniflux?
FreshRSS is the better fit when you want extensions, themes, WebSub push, web scraping of partial feeds, rich saved queries, and multi-user administration. Miniflux is the better fit for a minimal, near-zero-maintenance single binary. The companion Miniflux guide covers that deployment.
