The infrastructure behind this portfolio. A home server that hosts git repos, API backends, and live project demos. Every API call and demo on this site routes through hardware in my house.
The philosophy is no managed services beyond Cloudflare's CDN. Git hosting, CI/CD, push notifications, monitoring, all self-hosted. Not because cloud services are bad, but because running your own infrastructure is the best way to understand it.
Two access tiers: public internet and private mesh.
Public: a Cloudflare Tunnel from the server to Cloudflare's edge. No exposed ports, no static IP, no port forwarding. Requests to kschappell.com hit Cloudflare Pages for static assets; API and demo traffic routes through the tunnel to Nginx on the server, which reverse-proxies to the appropriate Docker container.
Private: a Tailscale mesh VPN for services that shouldn't be public (Home Assistant, Jellyfin, Immich, Vaultwarden). Accessible only from my own devices, regardless of what network I'm on. The combination covers both access patterns without opening a single port on the router.
Each service runs in its own Docker container with two networks: an internal bridge for service-to-service communication and a public-facing network for external access. Container isolation means I can blow away and rebuild any service without affecting the rest. The whole stack rebuilds from a single docker-compose file.
The backend container (FastAPI on Python 3.12) handles the contact form, activity feed, health monitoring, and CV downloads. Ntfy runs alongside it for self-hosted push notifications, no third-party notification service. The backend talks to Ntfy over the internal Docker network; external traffic only reaches services through Nginx.
Gitea Actions with a self-hosted runner on the same server. Path-filtered workflows: frontend changes (anything outside backend/ and deploy/) trigger a Node build and push to Cloudflare Pages via Wrangler. Backend changes (backend/ or deploy/) SSH into the server and rebuild the Docker container.
Frontend and backend deploy independently. A CSS tweak doesn't rebuild the API, and a new endpoint doesn't redeploy the static site. The runner and the deploy target being the same machine simplifies everything: no remote SSH from a cloud CI service, no credentials to manage beyond a Cloudflare API token for the frontend pipeline.
A status API polls each service's health endpoint with a 5-second timeout, caches results for a minute. Services are defined via environment variable, so adding a new one means adding a URL, not writing code. The portfolio's status dashboard reads from this API. If something goes down, I see it on the site before anyone reports it.
Activity tracking pulls recent commits from the Gitea API across all repos, with a 5-minute cache and stale fallback (up to an hour) if the fetch fails.
Public requests to kschappell.com are served by Cloudflare Pages (static assets). API and demo requests route through a Cloudflare Tunnel to an Nginx reverse proxy on the home server, which distributes traffic to Docker containers running FastAPI, Gitea, and project demos. Private services (Home Assistant, Jellyfin, Immich, Vaultwarden) are accessible only via a Tailscale mesh VPN. Deployments run through Gitea Actions - a push to main triggers path-filtered workflows that either deploy the static frontend to Cloudflare Pages or SSH into the server to rebuild the backend container.
Zero-trust networking via Cloudflare Tunnel - no exposed ports or static IP required
Nginx reverse proxy routing to isolated Docker containers per service
Tailscale mesh VPN for secure access to private services (Home Assistant, Jellyfin, Immich, Vaultwarden)
Self-hosted Gitea instance for git hosting and CI/CD
Gitea Actions with path-filtered workflows - frontend and backend deploy independently on push to main
Live status monitoring with health check API
SSL/TLS termination handled at the Cloudflare edge
Home servers need to just work - nobody wants to SSH in at 2am to fix a crashed container
Cloudflare Tunnel + Tailscale covers both public and private access without opening any ports
Docker makes it easy to blow away and rebuild a service without affecting everything else
Health checks catch problems before users do
Public Services
Private Services - Tailscale VPN
CI/CD Pipeline