Operating manual · Rev. 2026-09-20

Running homie

How to watch things, sync files, get in from outside, tell whether anything is wrong, and restart it without breaking it. Written for the people who use the machine, not the person who built it.

Machine
ThinkPad T470
House address
192.168.1.10
Tailnet name
homie.tailnet-name.ts.net
Library on
the T7 external SSD
  1. Which door you are using
  2. Watching movies & TV
  3. Adding to the library
  4. Syncing your files
  5. Getting in from outside
  6. Checking it is healthy
  7. If the tunnel “works” but nothing connects
  8. What does not work yet
  9. Turning it off and on
  10. Updating without breaking it
  11. Quick reference
01

Which door you are using

Everything makes more sense once you know there are two ways in, and that which one you are using decides what you can reach.

DoorWho gets inWhat it reachesEncrypted?
the LAN
Home WiFi
Anyone on the house WiFi Media only No — plain HTTP
the tailnet
Tailscale
Only devices the owner approved Everything, from anywhere in the world Yes — real HTTPS

Roommates use the first door and never need to install anything. The second exists so the owner can reach the machine from outside the house without opening a single port on the router. Architecture §02 explains why it is built this way.

Why this matters to you

On the home WiFi, your Jellyfin password travels unencrypted. The WiFi password protects it from the street, but not from other people in the house. Use a throwaway password here — never one you use anywhere else.

02

Watching movies & TV

From the house

Jellyfin · home WiFi http://192.168.1.10:8096

Sign in with the account you were given. Everyone gets their own login, so watch history, resume points and preferences stay yours.

On a TV, phone or tablet

The browser works, but the apps are better. Install one, then add the server manually when it asks:

Server address http://192.168.1.10:8096

What to expect while watching

The server converts video on the fly when your device cannot play a file directly. That conversion runs on a dedicated video chip rather than the processor, which is roughly three times faster and leaves the machine responsive for everyone else.

It is not unlimited — this is a laptop. Two people converting at once is fine; four is pushing it. If playback stutters, lower the quality in the player’s gear menu; that often lets your device play the file directly instead, which uses almost no server power at all.

If the library looks empty, check the cable

Everything you can watch lives on an external drive. If that drive is unplugged the media server refuses to start rather than showing an empty library — so “Jellyfin will not load” and “the drive came loose” look like the same problem. The health check tells you which in one line.

03

Adding to the library

Jellyfin works out what a file is almost entirely from its name and folder. Get the name right and the poster, cast and synopsis appear on their own within a minute. Get it wrong and you get a bare filename with no artwork — and no amount of rescanning fixes it.

The naming rules

/mnt/t7/Movies/Title (Year)/Title (Year).mp4
/mnt/t7/TV/Title (Year)/Season 02/Title (Year) - S02E01 - Episode Name.mp4

Season and episode numbers are two digits. Gaps are fine — a missing episode 3 stays missing rather than renumbering everything after it.

The usual way — let it file itself

Add a torrent in qBittorrent, set its category to tv or movies, and that is the whole job. When it finishes, the file is renamed to the pattern above, moved into the library, and removed from the download list.

qBittorrent · Tailscale only https://homie.tailnet-name.ts.net:8444

The category is the only field that matters. Everything else is already configured.

If something does not appear

The filer refuses to guess. If it cannot tell which show a release belongs to, it leaves the file exactly where it is and writes the reason — plus the one line you need to add to series-map.conf — to its log:

~/homeserver/config/qbittorrent/qbt-file.log

To see why it decided what it did — what the name parsed to, whether series-map.conf matched — ask with a few words from the title. It finds the download, works out its category, and replays the filing as a dry run with the reasoning switched on. Nothing moves:

bin/qbt-why big buck bunny
bin/qbt-why --as movies sintel     # for a download added without a category

“0 kept as video” on a folder that is already in the library means it is leftovers — artwork, a .txt, a Subs/ folder — which the filer leaves behind on purpose.

If a filed torrent is still sitting in qBittorrent's list, bin/qbt-forget <hash> takes it off without touching its files.

That is by design. A file that waits is easy to fix; a file misnamed into the wrong show is not.

The manual way

For anything that did not arrive as a torrent — a file from a friend’s drive, or one copied over SFTP — a helper script does the naming so it cannot be got wrong by hand:

addmedia movie ~/inbox/somefilm.mp4 "Some Film" 1954
addmedia show  ~/inbox/ep1.mp4 "Some Show" 1961 2 1 "Episode Name"

It builds the folder structure, moves the file into place, and nudges Jellyfin to scan. It also refuses to run at all if the external drive is unplugged, rather than filing your video somewhere that will vanish when the drive comes back.

Getting files onto the server by hand

SFTP · from the house sftp://admin@192.168.1.10

Any file transfer app works — Cyberduck, FileZilla, or the scp command.

Files cannot be deleted from inside Jellyfin

The media folder is mounted read-only on purpose, so a bug or a mis-set option cannot erase the collection. Deleting something means removing the file directly over SFTP. A little inconvenience, in exchange for a library that cannot delete itself.

04

Syncing your files

Syncthing keeps a folder identical across your devices. Change a file on your laptop and the server has it moments later — no cloud service, no subscription, and the files never touch anyone else’s computer.

FolderOn the serverPaired devices
school-work~/data/school-work homie, admin MacBook

Adding another device

  1. Install Syncthing on the new device and open its web page.

  2. Find Actions → Show ID and copy the long device ID.

  3. On homie’s Syncthing, choose Add Remote Device, paste that ID, then open the Sharing tab and tick school-work.

    Skipping that tick is the single most common mistake — the device pairs but nothing ever syncs.

  4. Back on the new device, accept both prompts: one to trust homie, one to accept the folder. Choose where the folder should live locally.

Pairing is mutual — both sides must agree. That is why the sync port can sit open on the house network safely: an unknown device can knock, but nothing transfers until someone approves it by ID.

Deleted something by accident?

Old versions of every changed or deleted file are kept on the server for 365 days in a hidden .stversions folder inside the synced directory. Deleting a file on your laptop does not destroy it here.

Syncing is not backing up

Versioning protects you from your own mistakes. It does not protect you from a disk failing — that would take every version with it. There is a real backup as well, running nightly (see §06), but read what it does and does not cover before relying on it.

05

Getting in from outside

Tailscale builds a small private network between approved devices. They can talk to each other from anywhere — a café, campus, another country — as if they were on the same WiFi. Nothing is exposed to the public internet and no ports are forwarded on the router.

Once a device is on it, these work from anywhere:

Jellyfinhttps://homie.tailnet-name.ts.net
Syncthing adminhttps://homie.tailnet-name.ts.net:8443
qBittorrenthttps://homie.tailnet-name.ts.net:8444
n8n editorhttps://homie.tailnet-name.ts.net:8445
Terminalssh admin@homie.tailnet-name.ts.net

Those carry genuine certificates issued to the machine’s name and renewed automatically, so browsers show a normal padlock with nothing to click past.

These documentation pages

The docs are the one exception — they are served on 127.0.0.1 only, so they need an SSH tunnel rather than a tailnet address:

ssh -L 8446:localhost:8446 homie-lan
# then open http://localhost:8446

Why it is bound that way. In a terminal, bin/homie-docs renders any page as text instead.

Working from the MacBook since 2026-09-20

These addresses timed out from the Mac for weeks. The cause was never on either machine: the tailnet’s ACL policy granted only exit-node access and nothing device-to-device, so both nodes correctly dropped every real packet while handshakes kept succeeding. Fixed by adding an allow rule in the admin console — see the diagnosis if it ever recurs.

06

Checking it is healthy

One command answers “is anything wrong?”. It only reads — it cannot change or break anything, so run it whenever you are curious.

~/homeserver/bin/homie-check

Thirty-nine assertions, grouped. What it covers:

GroupWhat you want to see
ContainersAll seven up, and the ones with health checks healthy
Port exposureMedia reachable on the house WiFi; every admin page not. Re-tested by connecting, because Docker bypasses the firewall
Memory limitsEvery container capped, and none near its ceiling
DownloaderPassword set, filing hook armed, and qBittorrent’s egress is the VPN, never the home address
Jellyfin configHardware acceleration on, throttling on, proxies declared
Library & diskT7 mounted with free space; more than zero items indexed
Live transcodeWhether a playback happening right now is using the video chip or the processor

The live-transcode row is the useful one: start something playing, run the check again, and it tells you whether the conversion actually landed on the video chip. A setting being switched on does not guarantee it is being used.

The automation stack has its own check

~/homeserver/bin/homie-automation-check

It checks n8n and ollama, then reads the workflow state database directly rather than asking n8n — so “n8n is dead” is not reported by a dead n8n. It emails on failure once SMTP is configured.

A workflow that has never run reports as information, not as a failure — an absence is not a fault when the thing was never built. Once a workflow has run, it gets a deadline: roughly 1.5× its interval, so one missed run is tolerated and two in a row are not.

What runs on a schedule

TimerWhenDoes
homie-backup.timerNightly, 03:30 (+ up to 15 min jitter) Mirrors ~/data/school-work and archives the repo’s gitignored config onto the T7. Keeps 7 archives
homie-automation-check.timerEvery 6 hours, at :45 The automation check above, emailing if something is stale or down

Both are Persistent=true, so a run missed while the machine was off happens on the next boot rather than being silently skipped. Confirm with systemctl list-timers 'homie-*'.

When something is broken — where the logs are

Health checks tell you that something is wrong. Logs tell you why.

When this misbehavesLook hereWhat you will find
Any service won’t start,
keeps restarting
docker logs NAME --tail 100 Startup errors, crashes, config it refused. NAME is jellyfin, syncthing, caddy, gluetun, qbittorrent, n8n or ollama
Playback stutters,
won’t play, no artwork
docker logs jellyfin --tail 100
config/jellyfin/config/log/
Docker shows the recent picture; the log folder keeps a dated file per day, which is what you want for something that happened yesterday
A web address times out,
or 502s
docker logs caddy --tail 50 Every request and every certificate renewal. A 502 means Caddy is fine and the service behind it is not — check that service next
The VPN, downloads stalled docker logs gluetun --tail 50 Tunnel connects, healthcheck failures, which server it picked. First place to look when downloads stop for no reason
Torrents won’t connect docker logs qbittorrent --tail 50
config/qbittorrent/qBittorrent/logs/
qBittorrent keeps and rotates its own log; the real reason for a refused connection is usually only in there, not in the web page
A download didn’t appear
in the library
config/qbittorrent/qbt-file.log The filing script writes here every run, including the reason it refused to guess and the line to add to series-map.conf
A device won’t pair docker logs syncthing --tail 50 Connection attempts and rejections, by device ID
An automation didn’t run docker compose logs -f n8n
bin/homie-automation-check
n8n’s own execution errors; the check reports which workflow is stale
The whole machine —
drive vanished, boot problem
journalctl -b -p err
dmesg | tail -30
Kernel and system errors. This is where a disconnected T7 shows up
Two flags worth knowing

--tail 50 shows the last 50 lines instead of the entire history. -f follows the log live, so you can watch what happens while you reproduce the problem — docker logs -f caddy, then load the page that fails. Ctrl+C stops watching.

06b

If the tunnel “works” but nothing connects

This cost three wrong diagnoses across three sessions, so the test that actually settles it is written down here.

tailscale ping is not ping. A tailscale ping is answered by the Tailscale daemon itself, in userspace, before the packet ever reaches the operating system or the tailnet’s own packet filter. It proves the two machines have negotiated a WireGuard session — and nothing more. Ordinary ping has to survive the whole path, so it is the honest test:

tailscale ping stevens-macbook-air   # succeeds even when everything real is broken
ping -c3 100.64.0.20              # this is the one that tells you the truth
SymptomWhere to look
Both succeedNetworking is fine. Look at the service and its port.
tailscale ping works, ping does not, in both directions The tailnet ACL. Run tailscale debug netmap and read PacketFilterRules — if it is empty, this node accepts nothing from any peer, and no amount of poking at either machine will help. Fix it at login.tailscale.com/admin/acls.
Fails one direction onlyA host firewall. On homie that is ufw; on macOS, System Settings → Network → Firewall, or Tailscale’s own “allow incoming connections”.
Name fails, IP worksMagicDNS. Check the route to 100.100.100.100.
The trap that caused the wrong diagnoses

autogroup:internet in an ACL does not mean “the internet is allowed”. It means “may route out through an exit node”. A policy containing only autogroup:internet destinations grants no device-to-device access at all, which is exactly what had happened here — and it compiles to an empty packet filter rather than to an error.

07

What does not work yet

Stated plainly, so nobody wastes an afternoon on a known gap. This is the one place in the documentation where these are listed; the other pages link here.

ItemStatusWhat it means for you
Backups share one drive Real risk Backups sit on the same external SSD as the media. That protects against deleting a file by accident — not against the drive failing, which would take both copies at once. The biggest open gap here.
Everything depends on one cable Single point Media and backups both live on the T7. Unplug it and there is nothing to watch and nowhere to back up to.
The n8n encryption key exists in one place Back it up It encrypts every stored credential. Lose it and they are not “log in again”, they are unrecoverable. It is in .env, captured by the nightly backup — which is on the same drive. Keep a copy off-box.
School automations Zero workflows n8n and ollama run and are healthy, but no workflow is built yet; each needs a credential fetched by hand.
Roommate accounts One made Two Jellyfin accounts exist: the owner’s and one roommate’s. Any other roommate still needs one created.
Most folders have messy names Cosmetic Most shows and films still sit in raw release-name folders. Newly filed episodes use the clean naming, so a show can briefly appear twice in Jellyfin until the old folder is renamed.
Media passwords on WiFi Accepted trade Unencrypted inside the house, so roommates need no setup. Use throwaway passwords.
Newest video format (AV1) No hardware support This chip predates it. AV1 files fall back to the processor and may stutter.
Searching for things to download By hand Downloading and filing are automatic; finding a release still means going and looking for one.
New episodes are not fetched on their own Not built Nothing watches your shows for you. When a new episode airs, you go and get it.
Downloads cannot accept incoming peers By design The VPN hides the home address and offers no way for strangers to connect in. Downloads are unaffected; you just share back to fewer people. qBittorrent shows a red “not connectable” dot, and that is expected — why.
08

Turning it off and on

homie is a laptop that runs all the time. Mostly you never touch this — services restart themselves after a power cut, and the whole stack comes back when the machine boots. You need this section for three situations: the machine has to move, one service is misbehaving, or you changed something and need it to take effect.

The one rule that matters

Never yank the power. Downloads are mid-write and the media drive is exFAT, which is unforgiving about interrupted writes. Always shut down properly and wait for the machine to go quiet before unplugging anything.

Restarting one service

Almost always the right move — it leaves everything else running. From ~/homeserver:

docker compose restart jellyfin     # or syncthing, caddy, n8n, ollama
sleep 10 && bin/homie-check
The VPN is the exception — two steps, always, in this order

qBittorrent has no network of its own; it lives inside the VPN container. Restart or update gluetun and qBittorrent is left connected to nothing — it looks fine and can reach nothing at all. It must be rebuilt afterwards, not merely restarted:

docker compose up -d --force-recreate gluetun
docker compose up -d --force-recreate qbittorrent

Always both, always in that order. homie-check catches it if you forget: it will say qBittorrent has no egress.

Restarting everything

  1. cd ~/homeserver

  2. docker compose down — stops all seven containers. Data, settings and library are untouched; only the running containers go away.

  3. docker compose up -d — brings them back, in dependency order.

  4. bin/homie-check — expect every check to pass.

Because down removes the containers and up creates fresh ones, the VPN two-step is handled automatically here. That is why a full cycle is sometimes simpler than fixing one service.

Shutting the machine down

  1. If anything is downloading, pause it in qBittorrent and let the current pieces finish writing.

  2. cd ~/homeserver && docker compose down — stops services cleanly rather than having them killed mid-write by the shutdown.

  3. sudo shutdown -h now — needs your password.

  4. Wait until the power light is off before moving or unplugging it. If the T7 is coming too, unplug it only after the machine is fully off.

Starting it back up

  1. Plug the T7 in first, then power on the laptop. This order matters: the media containers deliberately refuse to start if the drive is missing, so a missing drive is a loud failure instead of an empty library.

  2. Everything starts by itself — Docker restores all seven containers. Nothing to type.

  3. bin/homie-check to confirm, once you can reach it.

If containers did not come back

Nearly always the T7 was not mounted at boot. Check ls /mnt/t7 — if it is empty, mount the drive, then cd ~/homeserver && docker compose up -d.

09

Updating without breaking it

Every service is pinned to an exact version on purpose. Nothing updates itself and nothing changes underneath you, which means an update is always something you chose, on a day you chose it, and can undo.

Why not just use “latest”?

latest is a moving pointer, not a version. With it, the next restart could quietly install a different program, and a break would arrive at random with no record of what changed. Pinned versions turn upgrades into a decision with a date attached.

Updating one service

  1. Check it is healthy first. Never start an update from a broken state — you will not know afterwards which problem you were looking at.

    bin/homie-check
  2. Pick a real version. Look up the image’s releases and read what changed. A version number, never latest.

  3. Edit one line. In docker-compose.yml, change that service’s image: tag. Leave the comment recording the old digest — that is your way back.

  4. Apply it to that service only.

    docker compose up -d jellyfin
  5. Verify twice. Run the health check, then actually use the thing — play a video, open the page. A passing check is not the same as a working app.

    bin/homie-check
  6. Commit it, saying what moved from what to what. This is what makes rolling back possible.

    git add -A && git commit

Undoing an update

Because the version lives in a committed file, rolling back is editing that line back and re-running docker compose up -d. Or, if the commit contained nothing else, git revert it and apply. Config and data are separate from the container, so downgrading does not lose anything — with one caveat.

Read this before a big version jump

Some apps upgrade their own settings or database on first start, and once that has happened the old version may refuse to read them. Jellyfin especially. Before a jump like 10.11 → 10.12, run bin/homie-backup and skim the release notes for anything about migrations. Small point releases are safe; major ones deserve five minutes of reading.

Updating the VPN container

Same as above, plus the two-step from §08 — a rebuilt gluetun always needs qBittorrent rebuilt after it:

docker compose up -d --force-recreate gluetun
docker compose up -d --force-recreate qbittorrent
bin/homie-check

The line to look for reads qBittorrent egress is the VPN. If it names the home address instead, stop and fix it before downloading anything.

Updating n8n

Same procedure, with one thing to check first: never change N8N_ENCRYPTION_KEY. Every stored credential is encrypted with it, and a new key turns them into unrecoverable ciphertext rather than a re-login. n8n also occasionally changes defaults between versions and warns at startup; the compose file pins those explicitly, so read its startup log after an upgrade.

Updating Ubuntu itself

sudo apt update && sudo apt upgrade

Separate from the services, which carry their own dependencies inside their containers. If it installs a new kernel it will want a reboot — use the shutdown steps in §08, and remember everything comes back on its own afterwards.

A sane rhythm

There is no schedule to keep. Patch the operating system when you think of it, update a service when you want something it fixed or when a security note says to, and do one thing at a time — so that if something breaks, you know what did it.

10

Quick reference

Addresses

WhatAddressWorks from
Watch mediahttp://192.168.1.10:8096House WiFi
Watch media (secure)https://homie.tailnet-name.ts.netTailnet
Syncthing adminhttps://homie.tailnet-name.ts.net:8443Tailnet
Downloadshttps://homie.tailnet-name.ts.net:8444Tailnet
n8n editorhttps://homie.tailnet-name.ts.net:8445Tailnet
These docshttp://localhost:8446An SSH tunnel only
Terminalssh admin@192.168.1.10House WiFi
File transfersftp://admin@192.168.1.10House WiFi

The full reachability table, including what the public internet can reach (nothing).

Where things live

ContentsPath on the server
Films/mnt/t7/Movies
TV/mnt/t7/TV
Downloads in progress/mnt/t7/downloads
Backups/mnt/t7/backups
Which show goes in which folder~/homeserver/hooks/series-map.conf
Filing log — read when a download does not appear~/homeserver/config/qbittorrent/qbt-file.log
Synced school & work files~/data/school-work
Recovered old file versions~/data/school-work/.stversions
Service configuration~/homeserver

Commands

Run these from ~/homeserver. Anything taking arguments shows them; the rest take none.

CommandDoes
bin/homie-checkFull health check — read-only, safe any time
bin/homie-automation-checkJust the automation stack and workflow freshness
bin/homie-docsRead any of these pages as text. --search TERM greps them all
bin/homie-backupBack up synced files and server settings to the T7. Also runs nightly
bin/canvas-previewShow what a Canvas feed would sync. Changes nothing
bin/addmedia movie FILE "Title" YEAR File one film by hand
bin/addmedia movie ~/inbox/f.mkv "Some Film" 1954
bin/addmedia show FILE "Title" YEAR S E "Ep" File one episode
bin/addmedia show ~/inbox/e.mkv "Some Show" 1961 2 1 "Ep Name"
docker psWhich services are running
docker compose downStop everything. Data is untouched
docker compose up -dStart everything back up
docker compose restart NAMERestart one — but see §08 for the VPN exception
docker logs NAME --tail 50Recent output from one service
systemctl list-timers 'homie-*'When the scheduled jobs last ran and next will
sudo shutdown -h nowPower the machine off properly

Secrets, and what git never sees

There is no password manager here — there is a placement rule. Anything secret lives in a file git is told to ignore, and the repo carries a template naming what is needed without containing it.

PathHoldsIn git?
.env The real values: user IDs, timezone, the tailnet address, the qBittorrent login, the NordVPN key, n8n’s encryption key, and the automation credentials Ignored
.env.example The same variable names with empty values, so the repo says what is needed without leaking it Committed
config/ Live state: Jellyfin’s user database, Syncthing’s private key, Caddy’s certificates, qBittorrent’s password hash, n8n’s encrypted credentials Ignored
data/, backups/ Your files and the backup archives Ignored

Passwords are stored by each service rather than by us: Jellyfin keeps its own in its database, qBittorrent keeps a hash, Syncthing hashes its web login. Nothing here stores a plaintext password except .env, which holds the qBittorrent login because the filing script has to log in as you.