Problem
I wanted a public portfolio that shows off the homelab (live status, a read-only terminal, the whole thing), but my ISP blocks inbound ports. I can't just point a domain at my house and call it a day.
christopher@homelab:~$ whoami
christopher. I build and run a homelab at home
christopher@homelab:~$ cat mission.txt
christopher@homelab:~$ _
How traffic gets to the static site, the live terminal, and everything running on the box at home.
A quick case study on shipping a public portfolio when your ISP won't let inbound ports through.
I wanted a public portfolio that shows off the homelab (live status, a read-only terminal, the whole thing), but my ISP blocks inbound ports. I can't just point a domain at my house and call it a day.
I put the static site on Cloudflare Pages (free, fast, no server to babysit). My afraid.org subdomain forwards to Pages so I get a clean URL. For the live terminal, a Cloudflare quick tunnel on the homelab exposes a WebSocket to a read-only gateway with sensitive paths blocked and output redacted. Status syncs from Uptime Kuma on a cron job.
Public site with zero open inbound ports. Visitors get HTTPS, live monitoring, and a sandboxed shell peek at the stack. Deploys are a script on the homelab that pushes to Pages. When the box reboots, Docker brings everything back in about two minutes.
All of this runs in Docker on one machine at my place. Caddy sits in front and handles HTTPS.
I use AdGuard Home with Unbound for network-wide blocking and recursive DNS. Queries go to the root servers, not some upstream resolver.
DoH, DoT, and DoQ endpoints so devices can resolve DNS privately. Works off-network too when I'm on the VPN.
Tailscale mesh VPN with an exit node and MagicDNS. I can reach everything at home from anywhere without opening ports.
Self-hosted SearXNG for search. No tracking, no profile building, just aggregated results.
Caddy as reverse proxy and TLS. Let's Encrypt certs via DNS-01, so every service gets a clean HTTPS URL.
Uptime Kuma for uptime, Beszel for metrics, Dozzle for container logs, and a speedtest tracker so I know when the ISP is acting up.
Homepage dashboard: one page with links and widgets for everything on the network.
Fabric Minecraft server (The Boys) with Chunky pregen and BlueMap for a live web map of the world.
Read-only shell into /opt/stacks and Minecraft config over a Cloudflare Tunnel. You can't edit anything. Sensitive paths are blocked and output gets redacted.
Pulled live from Uptime Kuma and the homelab host. Updates every minute.
What happens when the box reboots, how status stays fresh, and what I actually monitor.
fetch_status.py runs every minute via cron. It pulls Uptime Kuma heartbeats, host CPU/RAM/disk, deploy timestamp, and incident log into status.json, then deploys if the hash changed./opt/stacks. I keep copies before changes (timestamped .bak files on deploy).How I limit blast radius on a box that is partly public-facing.
ISP blocks inbound anyway. Public site on Cloudflare Pages. Homelab services reach the internet only through outbound tunnels and Tailscale.
Path jail to /opt/stacks and Minecraft config only. Blocked dotfiles, keys, env files, and databases. Output redacted. Commands audit-logged.
Terminal gateway runs read-only rootfs, non-root user, cap_drop: ALL, and no-new-privileges. Secrets stay outside deploy paths.
Caddy terminates HTTPS with Let's Encrypt via DNS-01. HSTS on proxied services. Encrypted DNS (DoH/DoT/DoQ) for clients that support it.
No datacenter required. This whole stack runs on a recycled office desktop in my house.
I run a homelab at home: DNS, VPN, reverse proxy, monitoring, Docker, and a Minecraft server. I built this portfolio site and the automation around it myself.
Building toward systems administration. The lab is where I practice what I do at work: monitoring, DNS, Linux, documentation, and recovery.