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;
'';
};
};


NIX

Add 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 = {};


NIX

A few things worth calling out:

  • NEXTAUTH_URL vs NEXTAUTH_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 hardeningProtectSystem = "strict" makes the entire filesystem read-only except for the paths I explicitly allow. PrivateTmp gives the service its own /tmpNoNewPrivileges prevents 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";
};
};


NIX

Add 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"
];
};


NIX

The 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;
};


NIX

Each 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.

← Back to Series

Next: Part 11: Gitea, Smarter Monitoring, and Syncthing Permissions

The full configs are available in my NixOS config repo and nixos2 config repo.