claw
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
| part | spec |
|---|---|
| machine | hp proone 400 g6, an all-in-one |
| cpu | intel i5-10500 |
| memory | 16gb |
| storage | 1tb ssd |
| os | ubuntu server 26.04 |
| apps | coolify |
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:
| when | what happens |
|---|---|
| wi-fi joins or renews | log in to the portal |
| every 5 minutes | log in again if it's offline |
| every 20 hours | force a fresh login, before the day runs out |
| every 5 minutes | check 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:
- a cloudflare worker, which caches static files at the edge1
- a tailscale funnel into the box
- a tiny caddy shim that puts the real hostname back
- 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.
| measured | time to first byte |
|---|---|
| on the box | ~6 ms |
| on campus, median | ~500 ms |
| on campus, 1 in 3 requests | 1.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
-
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. ↩