My home server archives Linux ISOs. Some of them are feature-length images that ship once and never change, like an LTS release. Others are rolling-release distros: a new point release every week, sometimes for fifteen years straight, and completists want the full back-catalogue. The household requests these ISOs by typing a name at a Discord bot, and a chain of services does the rest: the bot hands the request to Seerr, Seerr hands it to the fetchers, the fetchers find a source and download it. All very hands-off, except for the last metre, which was me.
The last metre
The library software organises the collection into eight shelves: feature ISOs and rolling-release ISOs, each split by origin — anime, Asian, English, foreign — plus one hand-curated kids shelf that nobody but me touches. Each shelf watches its own folder. The fetchers, though, drop everything into two landing folders that no shelf watches at all. So every requested ISO downloaded perfectly, then sat invisible until I noticed the household asking where it was, shelled in, and dragged it to the right folder by hand. An automated pipeline with a human as the final conveyor belt.
The obvious fix was the request app's built-in routing rules, and it turned out to be a trap. Route by genre and by language sounds fine until an animated ISO from Japan matches both the Animation rule and the Asian-language rule, and the app's documentation doesn't say which one wins. The only way to find out was to fire real requests and watch where they landed. I'd be debugging a black box with live ammunition.
A small librarian
Instead the server got one more container: forty lines of compose and a Python script that wakes every fifteen minutes, looks at whatever landed in the transit folders, and asks the metadata service what each ISO actually is. The rules are mine, written down where I can read them: anything tagged Animation goes to the anime shelf no matter what language it speaks, live-action in an Asian language goes to the Asian shelf, English goes to English, and the kids shelf is never, ever, touched by a machine. Moves are renames on the same filesystem, so a fifty-gigabyte archive shelves itself instantly instead of copying, and the fetcher's bookkeeping gets updated so it doesn't go looking for files in the old spot.
First live sweep, it found two rolling-release distros stranded in transit, read their metadata, and filed one under English and one under anime. Both appeared on their shelves before I'd finished reading the log line.
The release archives that wouldn't fetch
While I was in there, a longer-standing annoyance: requesting a full season-sized release archive of a rolling distro almost never worked, and single point releases only sometimes. The causes were stacked three deep, which is why it always looked random.
First, a global size cap of five gigabytes, sensible for one point release, meant every full archive was rejected on sight — those run twenty to forty gigabytes. Raised to fifty: big enough for any real archive, still small enough to reject the fake two-hundred-gigabyte "releases" that are never what they claim. Second, quality floors. I set them to ten megabytes a minute deliberately, because I still want the content to be quality; bottom-shelf re-encodes can stay where they are. Third, the actual killer: the busiest indexer was handing out download files through a mirror site that had quietly died. Every grab hung for a hundred and twelve seconds, failed, and each failure put the indexer in a penalty timeout, which made everything downstream look broken in new and confusing ways. The fix was one setting: prefer magnet links, skip the corpse of the mirror entirely. Grabs went from a two-minute timeout to four seconds.
Proof run: asked for a missing point release from a distro nine major versions deep. Found, fetched, and on the shelf inside a minute. There's a back-catalogue of about a thousand missing point releases across the collection that's suddenly within reach.
Housekeeping
The request app also got a rename. It runs Seerr now, the successor to Overseerr, but its public address still said otherwise. The tricky part of renaming a gated service is the gap where the new name exists but the login gate doesn't, so the gate was armed for both names before the new one existed, then the old name was dropped once the switch was verified. Portal tile, uptime monitors, done.
So the pipeline is closed. Type a name into Discord, and a few minutes later the ISO is on the correct shelf, sorted by what it is and where it's from, with nobody dragging files anywhere. The server hands out Linux ISOs like a package manager now. As it always should have.
The reboot nobody watched
The morning's work ended with a deliberate act of violence: reboot the whole server and see what comes back on its own. Everything on this box is supposed to survive that — the containers, the network mounts, the tunnel, and the two Discord bots that let me drive the server from my phone. Supposed to. The only way to know is to pull the trigger with a baseline captured and a verification script waiting.
Verdict: sixteen out of sixteen service probes up, all three network mounts back, and the photo library visible the moment its container started — that last one matters because it used to lose a race against the NAS mount at boot and wake up staring at an empty directory. One systemd drop-in (make Docker wait for remote filesystems) killed that race permanently. The verification script did report one failure, which turned out to be the best kind: a service that looked dead was just socket-activated, dozing until something first knocks. The box now provably returns from a power cut with nobody logged in, and the bots answer from the couch before I've noticed the lights blinked.
Going public
The Discord bridge itself — two small bots, one that carries the conversation into a live Claude Code session and one that only knows four words (!start !stop !restart !status) — had been asked about enough that it deserved to stop being a private pile of scripts. So the evening went to open-sourcing it: disclaude-sesh.
Publishing homelab plumbing is mostly a privacy exercise. The private version had my paths, my Discord IDs, my quirks baked in everywhere. The public one got a single config file, placeholder IDs, and — importantly — different defaults than I run: the risky conveniences (skipping permission prompts, patching the plugin to move approval buttons into a channel) are opt-in flags with their trade-offs documented, not the out-of-the-box behavior. Then an automated sanitizer pass grepped the whole tree for everything from tokens to interface names before the first commit ever existed. Nothing personal survived into history.
The README got two installation paths, because the project is itself about AI-assisted ops: a full manual tutorial for humans who want to understand every step, and a single copy-paste prompt that hands the entire server-side setup to Claude Code while the human only does the two things it can't — click around the Discord Developer Portal and send a pairing DM.
Release buttons
First release means version one point oh, live badges on the README — star count, latest release, how many people are online in the Discord — and a lesson at the very end: one commit message credited another project as the style reference for the page layout. Harmless? On a public repo the commit log is published content, and it should describe the change, not the mood board. Scrubbing two words from an already-pushed message costs a history rewrite, a moved tag, and re-pointed release notes. Cheap on a repo that's three hours old; a good habit to have learned before it wasn't.
One day, one arc: the request pipeline stopped needing me in the morning, the server proved it doesn't need me at boot, and by midnight the whole remote-control rig became something a stranger can install without ever seeing my setup. The couch is now a fully supported deployment target.