claw

a home server in my dorm. it logs itself into campus wi-fi and serves fc-os.tech to the internet.

2026-10-01 | live

i wanted fc-os running on my own hardware, not on a cloud bill. the catch: i live in a dorm, and campus wi-fi logs you out every day.

the box

partspec
machinehp proone 400 g6, an all-in-one
cpuintel i5-10500
memory16gb
storage1tb ssd
osubuntu server 26.04
appscoolify

staying online

the campus portal logs you out after about a day, and again every time the wi-fi reconnects. so claw logs itself back in. nothing clever: a python script that does what the portal's login page does (including rsa-encrypting the password), plus a few shell scripts and systemd timers. four things call it:

whenwhat happens
wi-fi joins or renewslog in to the portal
every 5 minuteslog in again if it's offline
every 20 hoursforce a fresh login, before the day runs out
every 5 minutescheck fc-os.tech, and repair the tunnel if it's down

getting in

campus only lets tcp port 443 out, so nothing on the internet can reach the box directly. a request to fc-os.tech goes through:

  1. a cloudflare worker, which caches static files at the edge1
  2. a tailscale funnel into the box
  3. a tiny caddy shim that puts the real hostname back
  4. coolify's router, then the app

the hard parts

my first version checked if it was online using a hostname. but while the portal is closed, the bring-up turns off warp, and warp is the box's dns. no dns meant the check never passed, so warp never came back, and the whole box sat offline. now everything checks by ip, and i write the runbook before the outage, not after.

the part that's still slow: tailscale's funnel entry point for my box is in japan, so every page goes mumbai → tokyo → chennai and back.

measuredtime to first byte
on the box~6 ms
on campus, median~500 ms
on campus, 1 in 3 requests1.5–2.1 s

cloudflare tunnel would skip japan, but campus blocks it on every transport i tried. the fix left is a small vps in india on my tailnet, so the traffic never leaves the country.

Footnotes

  1. funnel only answers to its own hostname, so a plain dns record can't point at it. the worker's fetch() sends the right one. ↩

projects