Operations teams often start with hosted feed readers, then run into limits around account control, data residency, and integration with existing observability workflows. If you want a self-hosted RSS platform that stays lightweight but still supports production standards, FreshRSS is a strong fit. In this guide, you will deploy FreshRSS on Ubuntu using Docker Compose for orchestration, Traefik for TLS termination and routing, and PostgreSQL for durable storage. The goal is not a demo setup; it is an operational deployment you can run, monitor, back up, and upgrade with confidence.
In practical environments, RSS is not just a reading tool. Security teams use it for CVE disclosures, platform teams use it for release-note monitoring, and product teams use it to follow ecosystem signals. A production-grade FreshRSS deployment gives your organization one controlled endpoint for these information streams without relying on third-party account policies. Throughout this guide, each step is written to balance reliability and maintainability so handover between engineers is straightforward.
Architecture and flow overview
The stack has three runtime services and one operational boundary:
- FreshRSS application container serves the web UI and scheduled refresh workers.
- PostgreSQL container stores users, subscriptions, read states, and metadata.
- Traefik terminates HTTPS, auto-manages certificates, and routes requests to FreshRSS.
- Persistent volumes keep application and database state independent from container lifecycle.
Traffic flow is straightforward: browser requests reach Traefik on ports 80/443, Traefik handles ACME and TLS, then forwards to FreshRSS on the internal Docker network. FreshRSS uses environment-defined credentials to reach PostgreSQL. Backups are taken from DB dumps plus application volume snapshots so you can restore quickly after host or container failure. This split also makes troubleshooting easier because ingress, app logic, and database layers each have clean log boundaries.
Operationally, this layout supports predictable scaling paths: you can tune database resources independently from the app, move Traefik to a shared edge layer later, and retain the same Compose contract for day-2 upgrades. If you need multi-service tenancy on one host, this pattern reduces accidental cross-service coupling.
# Expected network path
# Client -> Traefik (443) -> FreshRSS container (8080 internal)
# FreshRSS -> PostgreSQL (5432 internal)
# Persistent data -> named Docker volumes
If the copy button does not work in your browser/editor view, manually select the command block and copy it.
Prerequisites
- Ubuntu 22.04/24.04 server with at least 2 vCPU, 4 GB RAM, and 30+ GB disk.
- A DNS A/AAAA record pointing a hostname like
rss.example.comto your server. - Docker Engine and Docker Compose plugin installed.
- Firewall allowing inbound 80/443 only; SSH restricted to trusted sources.
- A secret manager or safe vault for credentials and token material.
Before deployment, confirm your host clock is synchronized and outbound internet access is available for image pulls and ACME validation. Also decide where logs and backup artifacts are retained, because operational success is mostly about repeatability, not initial boot.
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg
sudo install -m 0755 -d /usr/share/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker.gpg] 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-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker $USER
If the copy button does not work in your browser/editor view, manually select the command block and copy it.
Step-by-step deployment
1) Prepare project layout
Use a dedicated deployment directory and lock down local environment files. Keep secrets outside version control and never bake credentials into Compose YAML directly. This also helps during incident response because your operational files are easy to inventory quickly.
mkdir -p ~/freshrss-prod && cd ~/freshrss-prod
mkdir -p traefik backups scripts
touch .env
chmod 600 .env
If the copy button does not work in your browser/editor view, manually select the command block and copy it.
2) Create environment file with production secrets
Generate strong passwords and set explicit database values. Rotate these values when handing over operations or after any suspected exposure. Avoid weak defaults copied from examples; in production they become latent incident triggers.
cat > .env <<'ENV'
DOMAIN=rss.example.com
[email protected]
TZ=UTC
POSTGRES_DB=freshrss
POSTGRES_USER=freshrss_app
POSTGRES_PASSWORD=CHANGE_TO_LONG_RANDOM_SECRET
FRESHRSS_ADMIN_USER=admin
FRESHRSS_ADMIN_PASSWORD=CHANGE_TO_LONG_RANDOM_SECRET
FRESHRSS_SALT=CHANGE_TO_LONG_RANDOM_SECRET
ENV
If the copy button does not work in your browser/editor view, manually select the command block and copy it.
3) Define Docker Compose stack
This Compose file pins major versions, applies restart policies, and isolates service communication to the private backend network. Traefik labels handle host routing and HTTPS policy centrally. Keep images pinned and update intentionally so rollbacks are deterministic.
cat > docker-compose.yml <<'YAML'
services:
traefik:
image: traefik:v3.0
command:
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL}
- --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
- --certificatesresolvers.letsencrypt.acme.httpchallenge=true
- --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web
ports: ["80:80", "443:443"]
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik:/letsencrypt
restart: unless-stopped
networks: [frontend]
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes: [pgdata:/var/lib/postgresql/data]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 6
restart: unless-stopped
networks: [backend]
freshrss:
image: freshrss/freshrss:1.24.2
depends_on:
db:
condition: service_healthy
environment:
TZ: ${TZ}
CRON_MIN: "3,33"
DB_BASE: ${POSTGRES_DB}
DB_HOST: db
DB_USER: ${POSTGRES_USER}
DB_PASSWORD: ${POSTGRES_PASSWORD}
FRESHRSS_INSTALL: |-
--api-enabled
--base-url https://${DOMAIN}
--default_user ${FRESHRSS_ADMIN_USER}
--language en
FRESHRSS_USER: |-
--user ${FRESHRSS_ADMIN_USER}
--password ${FRESHRSS_ADMIN_PASSWORD}
SALT: ${FRESHRSS_SALT}
volumes:
- freshrss_data:/var/www/FreshRSS/data
- freshrss_extensions:/var/www/FreshRSS/extensions
labels:
- traefik.enable=true
- traefik.http.routers.freshrss.rule=Host(`${DOMAIN}`)
- traefik.http.routers.freshrss.entrypoints=websecure
- traefik.http.routers.freshrss.tls.certresolver=letsencrypt
- traefik.http.services.freshrss.loadbalancer.server.port=80
restart: unless-stopped
networks: [frontend, backend]
networks:
frontend:
backend:
volumes:
pgdata:
freshrss_data:
freshrss_extensions:
YAML
If the copy button does not work in your browser/editor view, manually select the command block and copy it.
4) Start services and verify health
Bring the stack up in detached mode, then confirm health from Compose, container logs, and endpoint checks. Early verification avoids silently carrying a misconfigured deployment into production. Watch for DB readiness and initial certificate issuance as the two most common first-run delay points.
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail=80 freshrss
docker compose logs --tail=80 db
docker compose logs --tail=80 traefik
If the copy button does not work in your browser/editor view, manually select the command block and copy it.
5) Post-deployment hardening
Operational hardening should happen immediately after first successful boot:
- Restrict SSH ingress to admin CIDRs.
- Enable unattended security updates for Ubuntu packages.
- Keep Docker daemon and Compose plugin patched monthly.
- Disable unused Traefik dashboard exposure or protect it behind auth/network controls.
- Document and test restart/recovery procedure before onboarding users.
Also define ownership boundaries: who rotates secrets, who approves version upgrades, and who validates backup restores. Clear ownership reduces mean time to recovery during outages.
Configuration and secret-handling best practices
For production, treat all values in .env as sensitive operational secrets. Keep a secure source of truth and avoid reusing credentials from unrelated services. Rotate database and admin credentials on a schedule, and always after incident response events. If your organization uses a vault platform, template the environment file at deploy time so no long-lived plaintext remains on operator workstations.
Keep secret scope narrow: FreshRSS needs only its app DB credentials, while Traefik only needs ACME storage and an email identity. The fewer secrets each service knows, the smaller your blast radius. Finally, establish auditable change management: when credentials rotate, record who changed them, why, and whether post-rotation verification passed.
For teams with compliance obligations, pair this with routine access reviews and immutable audit logs. Even for small teams, a lightweight checklist before each release can prevent risky shortcuts from becoming recurring practice.
cp .env .env.bak.$(date +%F-%H%M)
# Update secrets in .env
docker compose up -d
# Validate login + refresh + DB connectivity
If the copy button does not work in your browser/editor view, manually select the command block and copy it.
Verification checklist
- HTTPS certificate is valid and auto-renewable.
- Admin login works and API is enabled as intended.
- Feed refresh jobs execute without DB timeout errors.
- Container restart survives host reboot with data intact.
- Backup and restore test has been executed at least once.
Do not skip restore verification. Many teams run successful backups for months only to discover restore gaps during an incident. A monthly restore drill to a disposable environment is usually enough to keep this risk low.
curl -I https://${DOMAIN}
openssl s_client -connect ${DOMAIN}:443 -servername ${DOMAIN} < /dev/null | openssl x509 -noout -dates -issuer -subject
mkdir -p backups
docker compose exec -T db pg_dump -U ${POSTGRES_USER} ${POSTGRES_DB} > backups/freshrss_$(date +%F).sql
If the copy button does not work in your browser/editor view, manually select the command block and copy it.
Common issues and fixes
Issue: Traefik serves 404 for the FreshRSS host
Cause: Host label mismatch or DNS not pointing to the active node. Fix: Confirm DOMAIN, Traefik router rule, and public DNS record. Recreate containers after correcting env values.
Issue: FreshRSS loads but cannot refresh feeds
Cause: Cron worker disabled or network egress restrictions. Fix: Ensure CRON_MIN is set, inspect FreshRSS logs, and validate outbound HTTP/HTTPS connectivity from the container namespace.
Issue: Database connection failures after restart
Cause: Password drift between .env and persisted DB roles. Fix: Reset DB user password in PostgreSQL to match current env, then redeploy and verify health checks pass.
Issue: Certificate renewal problems
Cause: ACME challenge blocked by firewall/proxy or stale ACME storage permissions. Fix: Keep port 80 reachable for HTTP challenge, ensure ACME storage is writable only by Traefik, and review Traefik ACME logs.
Issue: Slow UI when feed count grows
Cause: Under-provisioned database resources or insufficient vacuum/analyze cadence. Fix: Increase DB memory limits, monitor query latency, and tune maintenance windows for housekeeping tasks.
Issue: Unexpected logout sessions
Cause: App restarts during edits or mismatched salt/session settings after manual changes. Fix: Keep salt stable unless intentionally rotating and document restart windows for users.
FAQ
Can I run FreshRSS without PostgreSQL?
Yes, but for production reliability and operational visibility, PostgreSQL is the better default. It handles larger datasets and backup/restore workflows more predictably than lightweight single-file alternatives.
How often should I back up the database?
At least daily for small teams, and more frequently if feed state is operationally important. Pair scheduled DB dumps with retention policy and periodic restore drills.
Should I expose the Traefik dashboard publicly?
No. Keep it private behind network controls or an authenticated route. Public dashboards become reconnaissance targets and increase risk during internet-wide scans.
What is the safest way to handle admin password rotation?
Rotate in a maintenance window, update secret storage and environment values, redeploy, validate login and refresh jobs, and invalidate old credentials everywhere they were copied.
Can I scale this setup to multiple FreshRSS replicas?
You can, but ensure shared session/data behavior is understood. Start with single replica plus strong backups, then load-test before adding horizontal complexity.
How do I update FreshRSS with minimal risk?
Pin image tags, pull new version in staging first, verify migrations and feed refresh behavior, then deploy to production with rollback-ready backups.
What monitoring should I add first?
Track container restart counts, DB health, certificate expiry windows, and feed refresh latency. These four indicators detect most real-world failures early.
Related guides
If you are building a broader self-hosted platform, these guides are useful next steps:
- Production Guide: Deploy Kestra with Docker Compose + Nginx + PostgreSQL on Ubuntu
- Production Guide: Deploy Meilisearch with Kubernetes + Helm + ingress-nginx on Ubuntu
- How to Deploy Vaultwarden with Docker Compose and Traefik (Production Guide)
Talk to us
If you want this implemented with hardened defaults, observability, and tested recovery playbooks, our team can help.