engineer's notebook ÷ internet homepage
est. somewhere on the old web

← notes / 2026.07.11

build log: homelab

homelabself-hostingnetworkingtailscalepi-holefreshtomatodockerproject-log

the goal is to break into server hardware, software, and architecture by building a system i actually live on, not a toy that idles in a closet. here’s the target stack.

  • pi-hole, network-wide ad and tracker blocking for every device in the house at once
  • jellyfin, self-hosted media server so my library streams on my terms
  • immich, photo backup and library, my self-hosted answer to google photos
  • nextcloud, files, sync, and sharing on hardware i own
  • minecraft servers, because infrastructure should also be fun

that list is basically my definition of “viable” for this project. when all of it runs reliably enough that i stop thinking about it, the homelab is real.

the router (the plan at the time)

separate but related, i want to build my own router out of some old minipc, probably with openwrt or something similar handling the configuration. owning the network edge is the part most people skip, and it’s arguably the most interesting layer, since it’s where the security and privacy goals actually get enforced instead of just intended.

why

maximize security, increase privacy, and mostly just have control over my own digital utilities instead of renting them. every service above replaces a subscription or a company’s server with hardware i can physically point at. when it breaks, it’s my fault, and i find that genuinely comforting.

the server is real now (2026.07.18)

planning phase is over. the machine is an hp 280 g1 mt business, and it got a couple upgrades on the way in.

  • a second 500gb ssd on top of the existing 500gb
  • an nvidia t600 for gpu encoding, in support of jellyfin transcodes

then debian went on, with the server environment. the install itself was a whole saga though.

the display saga

i encountered some errors with the display which was really annoying during the install process. when trying to fiddle with the bios and when entering the debian setup screen, my display for some reason would cut in and out. i tried different cables, vga → vga, display port from gpu → vga, etc. and none of them worked. eventually just started messing with the connection at the monitor, and it would cut in and out intermittently which made for a challenge to install. once i got to the IP after installing debian, very painfully but successfully, i was able to just ssh in from my thinkpad x220. ended up discovering it was just the monitor from the start, so there goes that…

the stack is up (2026.08.03)

jellyfin, immich, nextcloud, and a minecraft server are all installed and running now. pi-hole is the only thing left on the original list.

where each one actually stands:

  • jellyfin is up but empty. no media in it yet, so there’s nothing to stream until i start filling it.
  • immich has pictures in it and is doing the job i wanted it for.
  • nextcloud already has files uploaded to it.
  • minecraft is up and running and my friends are playing on it.

tailscale is what made it useful

a server that only works on my own wifi isn’t really replacing anything. i could reach jellyfin and immich and nextcloud from a couple of machines at home and that was it, which is not what “control over my own digital utilities” is supposed to mean. and my friends obviously couldn’t get on the minecraft server at all.

tailscale fixed both. it puts all my devices on one private network no matter what wifi they’re sitting on, so anything with tailscale installed can reach the server from anywhere. my friends installed it and joined, and the minecraft server works for them now. none of it required exposing the box to the open internet, which is the whole reason i wanted to build this myself in the first place.

the minecraft server was stuttering and the numbers said it wasn’t (2026.08.09)

friends reported stutters. i checked tps and it read 20.000 in every sampling window, which is a perfect score. two sources disagreeing like that usually means one of them is measuring the wrong thing, and tps is an average, so it can sit at a clean 20 while individual ticks blow straight through their budget.

so i profiled it with spark instead of guessing. a tick gets 50 ms. the median came in around 19 ms, which is fine, but the maximums were 68 to 78 ms across three windows, so ticks really were overrunning and the average was hiding it. ServerChunkCache.tickChunks accounted for 79% of all tick time, 152,196 ms out of 193,188 ms, with roughly 6,900 chunks loaded and 13,300 block entities for two players. cpu sat at 12%, so the hardware was never the constraint.

the cause was simulation distance, set to 32. that’s the radius the server actually ticks every entity and block inside of, and it doesn’t need to match how far players can see. dropped simulation to 10 and left view distance at 32, so the expensive work shrank and the view out the window didn’t change. holds 20 tps with people on it now.

port forwarding, once i own the router

tailscale works, but it asks something of everybody else. anyone who wants on the minecraft server has to install it first, which is a real ask for people who just want to join a server and play. port forwarding drops that entirely and lets them connect the normal way, so that’s where this is going for the friends side of it. i’ll almost certainly keep tailscale for my own devices either way, since it’s still the better answer for reaching immich and nextcloud from wherever i happen to be.

the holdup is that port forwarding means opening the box up to the internet, and i’d rather own the network edge before i do that. so it waits on the router build, which is its own separate project, and that one waits until i move into a new apartment.

the router is mine now (2026.08.22)

moved into the apartment, which finally unblocked the router build. it didn’t end up being the minipc with openwrt i planned at the top of this log. my dad had a netgear r7000 sitting unused and handed it over, and once i actually read into it, openwrt was the wrong call anyway. the r7000 is broadcom based, and openwrt can’t properly support broadcom wireless, so i’d have ended up with a router that routes and a radio that doesn’t. freshtomato supports this model specifically and is still actively maintained, so that’s what went on it.

the flash was three stages, stock netgear firmware to an initial .chk image, then the full .trx, with an nvram erase in between. netgear’s 1.0.7.10 firmware also blocks downgrades through the web gui, so before touching anything i set up nmrpflash and npcap as a bootloader level recovery path and verified md5 on all three images. the gui path went through and the recovery tooling never got used, which is the correct outcome for having prepared it.

then the config. lan moved to 192.168.2.0/24 so it can’t collide with whatever the building is running upstream, and the server came off wifi and onto ethernet for the first time in its life.

the move broke the server in a way i didn’t see coming

plugging the ethernet in did nothing. the interface was sitting state DOWN with qdisc noop, because the original debian install only ever configured wireless, so enp4s0 had never been set up at all. i’d been running on wifi the entire time and never noticed the gap existed. configured it at the console in /etc/network/interfaces and it came up gigabit full duplex.

that fixed the link and immediately broke nextcloud, which started throwing http 400 at everything. the subnet had changed with the move and nextcloud’s trusted_domains still listed the old ip, so it was refusing requests from an address it no longer recognized. fixed with occ config:system:set. the lesson i’m keeping is that any service holding a hardcoded ip is a landmine you plant yourself and step on months later.

pi-hole is up, and the original list is done

pi-hole was the last item and it needed the router to exist first, because it does nothing until something points dns at it. now the r7000 hands it out over dhcp, so every device in the apartment gets filtered dns without installing anything or even knowing it’s happening. it’s blocking around 6% of queries in normal browsing off a list of about 82,000 domains.

that closes out the stack i wrote at the top of this page. pi-hole, jellyfin, immich, nextcloud, and minecraft are all running, with uptime kuma watching them.

the ip was never mine (2026.08.25)

with the router flashed and configured, port forwarding was finally supposed to be on the table. it isn’t, and working out why is probably the most useful thing i learned in this entire build.

ask any “what’s my ip” service from in here and it hands back a normal looking public address. that reads like a green light. but the router’s own wan interface is holding 10.20.154.7, and 10.x.x.x is a private range. that isn’t a public address, it’s an address the building handed my router from inside its own network.

so there are two layers of nat between me and the internet. mine, which i now control completely, and the building’s, which i will never touch. a port forward on my router only tells my router where to send traffic that already reached it, and nothing from outside can reach it, because the building’s nat has no rule pointing anything at me. i could forward every port on the box and it would change nothing. that isn’t a config problem i can fix, it’s the shape of the network i live in.

worth saying how i found it, because i nearly didn’t. one source said public, one said private, and if i’d trusted either one on its own i’d have concluded the wrong thing and then burned a weekend debugging a port forward that was never going to work regardless. comparing the router’s own wan status against an external reporting service is the only reason it was obvious.

so tailscale stays, permanently. it started as a workaround for a dynamic ip and a router i didn’t own, and now it’s just the architecture. my friends install it once and they’re on the minecraft server, which is a real ask but a one time one, and it’s the only path into here that exists. everything else i wanted out of port forwarding i already have. nothing on this box is exposed to the open internet, which was the point from the first paragraph of this log.

where it stands

about ten containers, total system cpu under 10% at idle, roughly 10 gb of the 15.6 gb of ram in use. wired link is gigabit full duplex and the apartment connection measures 139 down / 43 up. minecraft holds 20.000 tps with multiple players.

it also survived an unplanned power cut. both postgres databases, immich’s and nextcloud’s, replayed their write ahead logs cleanly and every container came back on its own. i went and read the logs to confirm that rather than assuming it. nothing was lost, but that was luck rather than design, so everything runs off a ups now. the next outage shouldn’t even be a story.

what’s left

the build is done. the list at the top of this page is fully checked off and the router underneath it is mine, so from here it’s maintenance instead of construction.

  • storage, if i decide i want it. everything is one copy on one 480 gb ssd. there’s a third ssd i can’t install because the case has no free sata port or power connector for it, so adding capacity means solving that first. this is the one real gap, and until it’s closed the plan is manual copies of the film scans, which are the only genuinely irreplaceable thing on there.
  • upkeep. debian stable, pinned versions, unattended-upgrades, and uptime kuma watching everything. the whole stack got chosen to run untended through a five course semester, so the ongoing job is patching, the occasional restart, and keeping the compose repo current.

the honest summary is that it works and i’ve stopped thinking about it, which is exactly the bar i set for myself at the top of this page.


← back to all notes