My family uses this server every day for movies, music and local AI. I build and run it with Claude Code. Below are the main projects and the decisions behind them.
Claude Code works on this server every day. These are the rules it works under.
It can read logs and configs freely. Any change needs my OK first, and each OK covers one action.
What it will run, why, what could break, and how to undo it.
Read-only sudo, and it's blocked from secret files like .env.
Each session keeps a log with exact errors and commands, so the next one starts from facts, not memory.
Problem, what I did, result.
My family wanted to add music to our Jellyfin server without asking me.
Built a web app that talks to Jellyfin, Lidarr, Soulseek and Spotify through their APIs (Spotify via OAuth). Every change goes issue, branch, PR, CI (tests and secret scanning), then release. The server pulls new releases on its own. Dev and prod run separately, so testing never touches the real library. Before making it public I added login lockout, CSRF protection, hashed session tokens and auto IP bans.
4 releases, 28 merged PRs, used daily. Each request also lands in the requester's own playlist.
I planned a setup where a fast model hands big jobs to a large, slow one. First I needed to know which model was good at what.
Wrote a test harness: 8 research questions, 7 scoring measures, graded blind. While grading I found a bug in my own harness that was dropping one model's answers, and fixed it before trusting any scores. I also flagged every made-up fact.
The large model scored lower (195 vs 248) and ran 2.5x slower than the best small one, so I dropped the plan. The numbers didn't support building it.
I set up fail2ban to block IPs with repeated failed logins. It said it was running.
Tested it from outside instead of trusting the status. The logs showed two problems: the app wraps IPs in quotes, so the match pattern never fired, and Docker traffic skips the firewall chain the bans went into. A later review found that IPv6 visitors would have looked like internal traffic and could never be banned.
Fixed all three and confirmed with a real ban. Lesson: test the outcome, not the status.
The apps keep their data in SQLite DBs, and copying a DB while it's being written can give you a broken backup.
The nightly job (restic) stops the apps for about 35 seconds so every DB is copied cleanly, then keeps 7 daily, 4 weekly and 6 monthly encrypted snapshots. Failures email me. I didn't call it done until I'd restored from it.
Restore test passed: a 382 MB DB came back with a clean integrity check.
Put a public page on a home server without opening anything else up.
Static files only: no login, no DB, no scripts. nginx runs as a non-root user, read-only, with all Linux capabilities dropped. It has no open ports of its own. The only way in is through the reverse proxy (Traefik). I tested it on an internal-only address first, then made it public.
You're reading it. It doesn't list IPs, hostnames or software versions, on purpose.