Now that my blog is live, it seems like a good time to talk about how I consume blogs/news from other sources.
I like to use RSS to get updates on content I really care about rather than letting algorithms decide what is best to show me.
FreshRSS
This is the feed aggregator of choice. There are local-only clients which I’ll talk about later, but it’s nice to have some state synced between devices. For this, I use a self-hosted option FreshRSS.
In an attempt to run this on a budget, I’m running this on the cheapest Hetzner VPS available.
While this is configured with the below Terraform, Hetzner doesn’t provide an ArchLinux image, so we mount the ISO and manually do the install.
resource "hcloud_server" "polaris" {
name = "polaris"
server_type = "cx22"
location = "nbg1"
ssh_keys = []
backups = true
# image is a placeholder as we will manually install arch anyway
image = "debian-12"
iso = "archlinux-2025.02.01-x86_64.iso"
# Define firewall rules through Hetzner Cloud Firewall
firewall_ids = [hcloud_firewall.polaris.id]
public_net {
ipv4_enabled = true
ipv6_enabled = true
}
}
resource "hcloud_firewall" "polaris" {
name = "firewall"
# Allow SSH from anywhere
rule {
direction = "in"
protocol = "tcp"
port = "22"
source_ips = ["0.0.0.0/0", "::/0"]
}
# Allow HTTP from anywhere
rule {
direction = "in"
protocol = "tcp"
port = "80"
source_ips = ["0.0.0.0/0", "::/0"]
}
# Allow HTTPS from anywhere
rule {
direction = "in"
protocol = "tcp"
port = "443"
source_ips = ["0.0.0.0/0", "::/0"]
}
# Allow ICMP (ping) from anywhere
rule {
direction = "in"
protocol = "icmp"
source_ips = ["0.0.0.0/0", "::/0"]
}
}
We also do some DNS setup so I can access this with a nice hostname
resource "aws_route53_record" "fresh-rss" {
zone_id = aws_route53_zone.primary.zone_id
name = "fresh-rss"
type = "CNAME"
ttl = 30
records = ["polaris.${aws_route53_zone.primary.name}"]
}
resource "aws_route53_record" "polaris_AAAA" {
zone_id = aws_route53_zone.primary.zone_id
name = "polaris"
type = "AAAA"
ttl = 30
records = [hcloud_server.polaris.ipv6_address]
}
resource "aws_route53_record" "polaris_A" {
zone_id = aws_route53_zone.primary.zone_id
name = "polaris"
type = "A"
ttl = 30
records = [hcloud_server.polaris.ipv4_address]
}
Once that is all done, we actually need to install and set up FreshRSS. I manage this machine with Puppet. The setup is quite simple; we just use Podman to run the official FreshRSS docker image and use an nginx reverse proxy to do TLS termination.
class fresh_rss (
String $base_url = 'https://fresh-rss.danwainwright.com',
) {
require podman
include nginx
include nginx::ssl::certbot
include rss_bridge # Will talk about this later
file { '/opt/fresh-rss':
ensure => directory,
}
-> file { '/opt/fresh-rss/data':
ensure => directory,
group => 'http',
mode => '0775',
}
-> file { '/opt/fresh-rss/extensions':
ensure => directory,
group => 'http',
}
file { '/opt/fresh-rss/data/config.php':
ensure => file,
content => epp('fresh_rss/config.php.epp', { base_url => $base_url }),
require => File['/opt/fresh-rss/data'],
group => 'http',
mode => '0664',
}
podman::image { 'fresh-rss':
image => 'docker.io/freshrss/freshrss:latest',
}
-> podman::container { 'fresh-rss':
image => 'docker.io/freshrss/freshrss:latest',
flags => {
mount => [
'type=bind,source=/opt/fresh-rss/data,destination=/var/www/FreshRSS/data',
'type=bind,source=/opt/fresh-rss/extensions,destination=/var/www/FreshRSS/extensions',
],
publish => [
'4000:80',
],
env => [
'TZ=Europe/Paris',
'CRON_MIN=13,43',
],
},
}
}
The nginx config looks like
server {
listen 80;
server_name fresh-rss.danwainwright.com;
return 301 https://$server_name$request_uri;
}
server {
server_name fresh-rss.danwainwright.com;
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/fresh-rss.danwainwright.com/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/fresh-rss.danwainwright.com/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
location / {
proxy_pass http://localhost:4000;
}
}
Some manual CertBot commands were needed to generate the initial Let’s Encrypt certificate, which I’ll skip over here.
RSS Bridge
So that’s the core RSS client done, which should be usable. Now on to a useful addition: RSS-Bridge. This is a tool that can scrape a standard website or API and generate an RSS feed from that.
For example, I use the Reddit bridge to get notifications for posts with enough upvotes on subreddits I care about. You can quite easily write new bridges, such as the BazarakiBridge I wrote to get notifications for motorbikes for sale matching my filter.
This is also managed by Puppet.
class rss_bridge {
require podman
file { '/opt/rss-bridge':
ensure => directory,
}
-> file { '/opt/rss-bridge/config':
ensure => directory,
}
# Mount in some custom bridges
file { '/opt/rss-bridge/BazarakiBridge.php':
ensure => file,
source => 'puppet:///modules/rss_bridge/BazarakiBridge.php',
}
file { '/opt/rss-bridge/CyprusMailBridge.php':
ensure => file,
source => 'puppet:///modules/rss_bridge/CyprusMailBridge.php',
}
file { '/opt/rss-bridge/BuySellCyprusBridge.php':
ensure => file,
source => 'puppet:///modules/rss_bridge/BuySellCyprusBridge.php',
}
podman::image { 'rss-bridge':
image => 'docker.io/rssbridge/rss-bridge:latest',
}
-> podman::container { 'rss-bridge':
image => 'docker.io/rssbridge/rss-bridge:latest',
flags => {
mount => [
'type=bind,source=/opt/rss-bridge/config,destination=/config',
'type=bind,source=/opt/rss-bridge/BazarakiBridge.php,destination=/app/bridges/BazarakiBridge.php',
'type=bind,source=/opt/rss-bridge/CyprusMailBridge.php,destination=/app/bridges/CyprusMailBridge.php',
'type=bind,source=/opt/rss-bridge/BuySellCyprusBridge.php,destination=/app/bridges/BuySellCyprusBridge.php',
],
publish => [
'3000:80',
],
},
}
}
Issues
Bot blocking is becoming a more common thing around the internet, with many sites using CloudFlare services for this. That’s not something RSS-Bridge attempts to solve and is slowly breaking my custom bridges.
Local Clients
Now all of this runs a self-hosted webpage. I like to use NetNewsWire to connect to FreshRSS, providing a nice interface across iOS and macOS.
Conclusion
It’s really not that hard to set up an RSS client, and once you have it, it makes managing incoming content a lot easier.
If you’re producing content in any form, please consider adding RSS support for users like myself.
This blog has an RSS feed at https://www.danwainwright.com/index.xml