arthur@homelab:~$ whoami

Arthur Sommer

/home/arthur · homelab operator · software builder · perpetual tinkerer

arthur@homelab:~$ cat ~/posts/why-this-site-moved-home.md

Why This Site Moved Home

How a forgotten CloudFront page became a Hugo site built in Forgejo and served from a dedicated homelab container.

mtime 2026-08-15 · 4 min read

For years, arthursommer.com was a small static page sitting behind AWS CloudFront. It worked, in the narrow sense that it continued returning HTML, but it stopped representing me or the work I was doing.

The replacement now lives at home. Hugo turns Markdown into static files, Caddy serves them, and Forgejo deploys them into a dedicated Incus container. There is no database, application runtime, tracking script, or client-side framework to maintain.

The final path

The public request path is intentionally short:

browser
  -> public DNS
  -> caddy-external in Ashburn
  -> Tailscale
  -> website container at home
  -> static files

The public edge keeps a stable internet address and terminates TLS. It proxies the request over Tailscale to Caddy in the website container at 100.104.12.31:8080.

That address belongs to the container, not the Incus host. This follows one of my homelab conventions: every service container gets its own tailnet identity. If Incus moves the workload between cluster members, the upstream address does not change and the service remains independent of its current host.

The container is deliberately small: unprivileged Debian, one CPU, 256 MiB of memory, Caddy, Tailscale, OpenSSH, and the generated site. The SSH listener is available only on the container’s Tailscale address and accepts one restricted deployment key. It cannot provide an interactive shell or forward traffic.

The repository is the deployment system

The source lives inside my second-brain Forgejo repository beside the homelab documentation it grows from. A change to the website directory on main triggers Forgejo Actions:

  1. Check out second-brain using the Forgejo-hosted checkout action.
  2. Install Hugo Extended from Alpine’s package repository.
  3. Build the site and validate required pages and the health artifact.
  4. Stream the generated archive directly to the container over Tailscale.
  5. Run a forced root-owned deployment command that stages and validates it.
  6. Preserve the previous /srv tree as a timestamped rollback.
  7. Verify both the tailnet origin and public HTTPS endpoint.

The workflow does not depend on GitHub Actions or a GHCR build image. Forgejo is the source, CI service, and deployment control plane.

DNS had one last lesson

Public DNS sends the apex to the Ashburn edge. The existing wildcard record covers www, which Caddy permanently redirects to the apex, so a separate public www record is unnecessary.

My tailnet uses an authoritative split zone for the same domain. That zone cannot fall through to public DNS when a name is missing. The first deployment therefore exposed a mistake: the private zone had neither a usable apex A record nor a www record. Tailnet clients reported that the site did not resolve even though public clients worked.

Adding explicit private apex and www answers fixed the problem. It was a good reminder that split DNS is part of the application path and deserves the same deployment checks as the reverse proxy.

Monitoring the whole path

The deployment workflow checks the origin and public endpoint before it calls a release successful, but that only proves the site worked at deployment time. Prometheus now checks the same path continuously from the homelab’s monitoring container.

The probe exporter connects directly to the public edge address with arthursommer.com as the TLS name, requests /health, and records both latency and certificate expiry. A separate probe bypasses the edge and checks the website container over Tailscale. Two more checks require the tailnet’s apex and www answers to resolve to the expected Caddy address.

Alertmanager receives dedicated alerts when the public path or origin fails, split DNS returns the wrong address, the certificate has fewer than 14 days left, latency remains high, or Incus stops reporting the website container. Together, those checks distinguish an edge or certificate problem from a dead origin or a private DNS mistake instead of reducing every failure to “the site is down.”

A site that looks like where it lives

The first visual draft leaned too far into DOS syntax. The finished design is closer to a restrained early-Unix terminal: an amber phosphor palette, monospace text, home-directory navigation, obvious links, and no JavaScript. The presentation is playful, but the HTML underneath remains responsive and semantic.

The content moved home too. Essays that had been sitting in a general blog folder are now edited Hugo posts. The site has become the public layer of the same second brain that holds private worklogs, configuration snapshots, and incident history.

What remains in AWS

The old S3 and CloudFront resources remain intact for now as a rollback path, but public DNS no longer sends the website to them. The active origin and its deployment system are in the homelab.

That distinction matters to me. The goal was not to remove every cloud component for ideological purity—the public Caddy edge still runs outside my house. The goal was to make the site reflect how I prefer to build: small components, explicit boundaries, infrastructure I understand, and a rollback for every meaningful change.

The migration is essentially complete. What used to be a forgotten page is now a working part of the homelab: built in Forgejo, carried over Tailscale, served by Caddy, monitored, documented, and ready to accumulate the next set of experiments.