This assumes you already run your own small server: SSH access, a domain pointed at it, TLS working, a web server (OpenBSD's httpd, in my case) already serving a site. If you don't have that yet, Derek Sivers' Tech Independence guide and its script get you there. The goal here is a self-hosted RSS reader, reachable only from my own devices over Tailscale, never the public internet.
Reasoning: an RSS reader is personal. There is no reason for it to be reachable by anyone but me. Tailscale gives every device I own a private network only those devices can join. The reader is never exposed publicly, regardless of how strong its own login is.
A script at the end does all of this. Read it before running it.
1. Install PHP
2. Get FreshRSS
3. Configure the web server
4. Fix outbound HTTPS fetching
5. Enable the API
6. Install Tailscale
7. Publish it privately
8. Automate refreshing
9. Read it on your phone
10. The script
Log in, become root:
📋
doas su
Install PHP. Pick the highest version offered:
📋
pkg_add php
Install the extensions FreshRSS needs:
📋
pkg_add php-curl php-gd php-intl php-mbstring php-pdo_sqlite php-sqlite3 php-zip
Match each prompt to the version you picked above.
OpenBSD installs extensions disabled by default. The config files land in a "sample" folder and need to be linked into the active one before PHP loads them:
📋
for x in /etc/php-8.2.sample/*; do ln -sf "$x" /etc/php-8.2/; done
(Replace 8.2 with your installed version throughout.)
Turn PHP on:
📋
rcctl enable php82_fpm
rcctl start php82_fpm
Not packaged for OpenBSD. Download the release directly:
📋
cd /var/www
ftp https://github.com/FreshRSS/FreshRSS/archive/refs/tags/1.29.1.tar.gz
tar -xzf 1.29.1.tar.gz
mv FreshRSS-1.29.1 freshrss
rm 1.29.1.tar.gz
chown -R www:www /var/www/freshrss
Check the releases page for the current version before running this. Adjust the version number in each line.
FreshRSS's own documentation shows an OpenBSD httpd config using regex-style location match patterns. OpenBSD's pattern matching is not regex — it is Lua-style, with different rules. That example parses without error and then routes nothing.
Plain glob-style location blocks work correctly. Add this as its own server block in /etc/httpd.conf, alongside your existing one. Pick a subdomain and an unused local port:
📋
server "rss.yourdomain.com" {
listen on 127.0.0.1 port 8090
root "/freshrss/p"
directory index "index.php"
location "/*.php" {
fastcgi socket "/run/php-fpm.sock"
}
location "/*.php[/?]*" {
fastcgi socket "/run/php-fpm.sock"
}
}
The second line matters. It catches the Reader API's URLs, which carry extra path segments after .php — for example /api/greader.php/reader/api/0/.... Without it, the API returns 404 while the rest of FreshRSS works fine, which is a confusing failure to debug.
Check and reload:
📋
httpd -n
rcctl reload httpd
Without this, every feed shows as broken. PHP's curl extension does not know where OpenBSD keeps its trusted certificates:
📋
echo 'curl.cainfo = "/etc/ssl/cert.pem"' > /etc/php-8.2/zzz-cainfo.ini
echo 'openssl.cafile = "/etc/ssl/cert.pem"' >> /etc/php-8.2/zzz-cainfo.ini
rcctl restart php82_fpm
Visit https://rss.yourdomain.com/ and finish the setup wizard. Choose SQLite — no separate database server needed.
Log in. Gear icon → Authentication → check "Allow API access (required for mobile apps)". Gear icon → your profile → set an API password, separate from your login password. Mobile apps use this one.
📋
pkg_add tailscale
rcctl enable tailscaled
rcctl start tailscaled
tailscale up
It prints a login URL. Open it, approve the device.
In the Tailscale admin console's DNS page, confirm MagicDNS is on. If you use a filtering DNS provider on your own devices, add it there as a global nameserver and enable "Override DNS servers". Skip this and any device that adopts Tailscale's DNS loses the ability to resolve ordinary websites.
This does not touch your public web server, port 443, or your certificate setup:
📋
tailscale serve --bg --https=443 http://127.0.0.1:8090
It prints a .ts.net address. That address is unreachable from any device outside your tailnet.
FreshRSS needs a scheduled trigger to fetch new articles. Add a cron job. Use the full path to the PHP binary — cron's environment omits /usr/local/bin and fails silently otherwise:
📋
(crontab -l 2>/dev/null; echo "*/15 * * * * /usr/local/bin/php-8.2 -f /var/www/freshrss/app/actualize_script.php > /dev/null 2>&1") | crontab -
Install Tailscale, sign in with the same account, confirm normal browsing still works once connected. If it does not, check the DNS step above.
Install an RSS app that speaks FreshRSS's Google Reader-compatible API. Server address is the .ts.net URL from step 7, username is your FreshRSS username, password is the API password from step 5.
All of the steps above, automated where that's possible. Run as root on a fresh server:
📋
ftp https://nevrast.xyz/rss.sh
sh rss.sh
It asks for your subdomain and pauses where a step needs a browser: the FreshRSS setup wizard, the Tailscale login. Read it before running it, the same as anything else you pipe into a shell.
Source is also on GitHub, alongside the ntfy script.
Email me with questions or fixes.