SearXNG, LinkWarden, and the RSS Reality Check — Part 10 of Building a Resilient Home Server Series
Where We Left Off
By the end of Part 9, I had automated backups with Restic, Syncthing replicating everything to the second server, and a monitoring stack that would actually tell me when things went sideways. The server was doing what a server should do — running quietly in the background while I forgot about it.
Naturally, I decided to break that streak by adding more services.
This time around I added a self-hosted search engine, a bookmark manager to replace yet another SaaS subscription, and went down an RSS rabbit hole that taught me a painful lesson about Meta's walled garden. Not everything worked out, but that's kind of the point of documenting this stuff.
SearXNG — Your Own Private Search Engine
What Is It and Why Bother
SearXNG is a privacy-focused metasearch engine. It takes your query, fans it out to Google, Bing, DuckDuckGo, and whatever other engines you configure, strips out all the tracking garbage, and gives you combined results. No logging, no ads, no cookies, no profile building. Just search results.
Now, full disclosure — I'm a Kagi subscriber and I love it. Their quick answer feature alone is worth the subscription to me. So why bother self-hosting a search engine?
Insurance.
If Kagi ever goes down, jacks up their prices, or decides to pivot into something I don't want to be part of, I have search.home sitting right there on my network ready to go. It's my backup plan for a service I can't afford to not have access to. And for basic searching around the house, it's perfectly solid.
The NixOS Config
One of the things I genuinely love about NixOS is how anticlimactic adding a new service can be. After the battles I fought getting Nextcloud and Vaultwarden running, SearXNG was almost disappointing in how easy it was.
Here's the full service config from services.nix:
# SearX - Self-Hosted Search
services.searx = {
enable = true;
settings = {
general = {
instance_name = "ppb1701 Search";
contact_url = false;
};
server = {
port = 8888;
bind_address = "0.0.0.0";
secret_key = secrets.searxSecret;
image_proxy = true;
};
search = {
safe_search = 0;
autocomplete = "google";
default_lang = "en";
};
ui = {
infinite_scroll = true;
theme_args.simple_style = "dark";
};
};
};
NIX
That's it. Enable the service, set a secret key, pick your preferences. Dark mode, infinite scroll, image proxy on so images actually load in results. The secret key lives in my secrets.nix file that gets imported at the top of the module — same pattern I've been using for every service.
Nginx Virtual Host
And the reverse proxy to give it a clean URL:
"search.home" = {
locations."/" = {
proxyPass = "http://127.0.0.1:8888";
proxyWebsockets = true;
extraConfig = ''
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
'';
};
};
NIXAdd a DNS rewrite in AdGuard Home pointing search.home to the server's IP, sudo nixos-rebuild switch, and you've got a private search engine. The whole thing took maybe fifteen minutes, and ten of those were me deciding what to name the instance.
Failover-Ready on nixos2
And because this is a pattern I've been following for every service, the same config exists on nixos2 with enable = false. If the main server catches fire — metaphorically, hopefully — I flip that to true, rebuild, and search is back. One line change.
LinkWarden — Self-Hosted Bookmarks
Why Not Just Use the Browser
I've been paying for a bookmark management service and it was starting to bug me. Not because it was bad, but because bookmarks are not a hard problem. I don't need someone else's servers to store a list of URLs. LinkWarden is an open-source, self-hosted bookmark manager that archives your links, takes screenshots, and lets you organize everything with tags and collections. And it runs on NixOS.
The hosted version of LinkWarden runs about $132 a year. The self-hosted version costs me electricity and a few minutes of config. Easy math.
The PostgreSQL Dependency
Unlike most of my other services that use SQLite, LinkWarden needs a real PostgreSQL database. This was actually my first time setting up Postgres on NixOS, and it turned into a learning experience about how NixOS handles database services differently from a traditional distro.
The database setup is surprisingly clean:
# PostgreSQL - needed by Linkwarden
services.postgresql = {
enable = true;
ensureDatabases = [ "linkwarden" ];
ensureUsers = [{
name = "linkwarden";
ensureDBOwnership = true;
}];
};
NIX
NixOS handles creating the database and user declaratively. ensureDBOwnership = true gives the linkwarden user ownership of the linkwarden database. No manual SQL commands, no remembering to run CREATE USER — it's all in the config.
The Service Config
LinkWarden runs as a systemd service with a dedicated system user and security hardening:
systemd.services.linkwarden = {
description = "Linkwarden Bookmark Manager";
after = [ "network.target" "postgresql.service" ];
wantedBy = [ "multi-user.target" ];
environment = {
DATABASE_URL = "postgresql://linkwarden:${secrets.linkwardenDbPassword}@localhost:5432/linkwarden";
NEXTAUTH_URL = "http://links.home";
NEXTAUTH_URL_INTERNAL = "http://localhost:8230";
NEXTAUTH_SECRET = secrets.linkwardenNextAuthSecret;
NEXT_PUBLIC_DISABLE_REGISTRATION = "true";
STORAGE_FOLDER = "/var/lib/linkwarden/data";
LINKWARDEN_HOST = "0.0.0.0";
LINKWARDEN_PORT = "8230";
NODE_ENV = "production";
};
serviceConfig = {
Type = "simple";
User = "linkwarden";
Group = "linkwarden";
WorkingDirectory = "/var/lib/linkwarden";
ExecStart = "${pkgs.linkwarden}/bin/linkwarden";
Restart = "on-failure";
RestartSec = "10s";
# Security hardening
NoNewPrivileges = true;
PrivateTmp = true;
ProtectSystem = "strict";
ProtectHome = true;
ReadWritePaths = [
"/var/lib/linkwarden"
"/var/cache/linkwarden"
];
};
};
# Create linkwarden user
users.users.linkwarden = {
isSystemUser = true;
group = "linkwarden";
home = "/var/lib/linkwarden";
createHome = true;
};
users.groups.linkwarden = {};
NIXA few things worth calling out:
-
NEXTAUTH_URLvsNEXTAUTH_URL_INTERNAL: The external URL is what users see (http://links.home). The internal URL is what the app uses to talk to itself (http://localhost:8230). Get these wrong and authentication breaks in confusing ways. I learned this the hard way. -
NEXT_PUBLIC_DISABLE_REGISTRATION = "true": This is my server, for me. No open registration. -
Security hardening:
ProtectSystem = "strict"makes the entire filesystem read-only except for the paths I explicitly allow.PrivateTmpgives the service its own/tmp.NoNewPrivilegesprevents privilege escalation. This is the kind of thing that NixOS makes easy that you'd have to manually configure with systemd on other distros.
Nginx and DNS
Same pattern as everything else:
"links.home" = {
locations."/" = {
proxyPass = "http://127.0.0.1:8230";
};
};
NIXAdd the DNS rewrite in AdGuard, rebuild, done. Type links.home in any browser on the network and you're managing bookmarks.
Backing It Up
Since LinkWarden uses PostgreSQL instead of SQLite, the backup strategy is a little different from Vaultwarden. I can't just stop the service and copy a file — I need to dump the database first, then back up both the dump and the data directory (which holds archived pages, screenshots, and uploads).
Here's the Restic backup job from backups.nix:
linkwarden = lib.mkIf enableBackups.linkwarden {
repository = "/var/local/backups/restic";
passwordFile = "/etc/nixos/private/restic-password";
paths = [
"/var/backup/linkwarden-db"
"/var/lib/linkwarden/data" # Archived pages, screenshots, uploads
];
# Create database dump before backup
backupPrepareCommand = ''
mkdir -p /var/backup/linkwarden-db
${pkgs.sudo}/bin/sudo -u postgres ${pkgs.postgresql}/bin/pg_dump linkwarden > /var/backup/linkwarden-db/linkwarden.sql
'';
# Daily at 2:40 AM (offset from Nextcloud at 2:15)
timerConfig = {
OnCalendar = "02:40";
Persistent = true;
};
pruneOpts = [
"--keep-daily 7"
"--keep-weekly 4"
"--keep-monthly 12"
];
};
NIXThe backupPrepareCommand runs pg_dump before Restic starts snapshotting. That gives me a consistent SQL dump I can restore from, plus all the archived content. And since I stagger my backups (Nextcloud at 2:15, LinkWarden at 2:40), they're not all fighting for disk I/O at the same time.
Same as every other backup job, the post-backup step makes the Restic repository readable by the Syncthing group so it gets replicated to nixos2.
The Backup Toggle Pattern
One thing I've been evolving across this series is how backups are enabled. Instead of commenting things out or maintaining separate configs, I use a simple toggle at the top of backups.nix:
enableBackups = {
vaultwarden = true;
nextcloud = true;
linkwarden = true;
gitea = false;
private = true;
};
NIXEach backup job wraps itself in lib.mkIf enableBackups.serviceName { ... }. Want to disable LinkWarden backups? Change true to false. Want to enable Gitea backups? Change false to true. No commenting out blocks of config, no risk of syntax errors from a misplaced bracket. NixOS just skips the disabled jobs entirely.
The RSS Reality Check
The Problem
I use RSS.app to pull feeds from Facebook and Instagram — specifically local news outlets and city government pages that only post on social media. It's not free ($8/month at the cheapest plan), and that bugged me. I'm self-hosting everything else; surely I can self-host RSS scraping, right?
What I Tried
I looked at RSSHub and RSS-Bridge, the two most popular self-hosted options for generating RSS feeds from social media. Both have Facebook and Instagram scrapers. Both of those scrapers have been broken for years.
Turns out Meta actively fights this. Authentication requirements, aggressive rate limiting, bot detection, constant HTML and API changes, and occasional legal threats to scraper projects. Every time someone fixes a Facebook scraper, Meta breaks it within weeks. It's a never-ending arms race, and the open-source projects understandably don't want to keep fighting it.
The Pragmatic Decision
Sometimes the self-hosted answer is "don't." RSS.app exists because scraping Meta platforms is a full-time job. They have the resources to keep up with the constant breakage. I don't, and I don't want to.
I downgraded my RSS.app plan to hourly updates instead of real-time (I don't need to know about a city council meeting the instant it's posted), which cut the cost in half. It's $8 a month to avoid maintaining Puppeteer scripts that break every other week. That's a trade I'm willing to make.
The lesson here is the same one that keeps coming up in this series: self-hosting everything isn't the goal. Self-hosting the things that make sense is the goal. My bookmarks, my search engine, my passwords, my files — those make sense. Scraping Facebook's walled garden? That's someone else's problem.
What's Next
In Part 11, I tackle self-hosting Git with Gitea, make my monitoring stack actually smart about which services exist, and fight a particularly annoying battle with Syncthing file permissions. Because apparently every service on my network has opinions about who owns what directory.
Have you ditched any SaaS subscriptions for self-hosted alternatives? Found any services that just aren't worth self-hosting? Let me know! Find me at @ppb1701@ppb.social on Mastodon.
Main Server (nixos): Codeberg
Second Server (nixos2): Codeberg
ISO can be gotten here.
Next: Part 11: Gitea, Smarter Monitoring, and Syncthing Permissions
The full configs are available in my NixOS config repo and nixos2 config repo.