Skip to content

Migrate www.etiennerobert.com hosting from Vercel to tower #171

Description

@etrobert-bot

Context

www.etiennerobert.com and apex etiennerobert.com are currently on Vercel.
Every other subdomain (creatures/files/adele/umami) is served from tower's
Caddy. Moving these last two to tower removes the final third-party hosting
dependency for the domain.

The test.etiennerobert.com vhost was added earlier as scaffolding to prove
the etiennerobert.com flake's build output served correctly through tower's
Caddy. It is not a permanent fixture — once production cuts over, it
gets torn down along with the Vercel project.

The site's flake (github:etrobert/etiennerobert.com) already exposes:

  • packages.default — the static build (consumed by the existing
    test.etiennerobert.com vhost today)
  • nixosModules.default — stub Caddy vhost for http://etiennerobert.com
    with a TODO to make it HTTPS; not currently imported on tower

This issue plans the migration as two stacked PRs and a manual DNS cutover,
plus a teardown phase that removes both Vercel and test.etiennerobert.com.

PR 1 — Stage /stats ad-blocker-resistant proxy on test.etiennerobert.com

Repo: etrobert/setup

Extend the existing test.etiennerobert.com Caddy block in
modules/server.nix to add a handle_path /stats/* block that
reverse-proxies to https://umami.etiennerobert.com (with a
header_up Host umami.etiennerobert.com line so umami's vhost match works).
This mirrors the Vercel vercel.json rewrites currently in production, on a
hostname that doesn't carry traffic — pure shake-down of the Caddy config.

Verify:

  • curl https://test.etiennerobert.com/stats/script.js → 200,
    application/javascript, ~4595 bytes (same payload as direct umami)
  • curl -X POST https://test.etiennerobert.com/stats/api/send -H 'content-type: application/json' -d '{"type":"event"}'
    → reaches umami (200/400-class, not a Caddy 502 or 404)
  • No Host header confusion in journalctl -u caddy on tower

This is throwaway test-bed work; the block goes away in the cleanup phase.

PR 2 — Production vhosts via flake nixosModules.default

Repo: etrobert/etiennerobert.com

Promote nixosModules.default from stub to production-ready. Design choices:

  • No enable option. This flake's only consumer is tower; importing the
    module is the activation. Adding enable would be ceremony with no
    payoff (the nixpkgs convention exists to guard against thousands of
    modules accidentally activating; here there's exactly one).
  • No domain option. Hostnames are part of the module's identity, not a
    tunable — the module's job is specifically "host www.etiennerobert.com
    and redirect etiennerobert.com to it."
  • One option: umamiUpstream. Default https://umami.etiennerobert.com
    (works from any host that can reach the public URL). Tower overrides to
    http://localhost:3001 to skip a TLS hop and the DNS lookup when umami
    runs locally. header_up Host umami.etiennerobert.com is hardcoded; it's
    the correct value for both upstream modes (umami at :3001 doesn't
    vhost-match on Host, so the header is harmless there).

Sketch:

{ self }: { config, lib, pkgs, ... }:
let cfg = config.services.etiennerobertCom; in {
  options.services.etiennerobertCom.umamiUpstream = lib.mkOption {
    type = lib.types.str;
    default = "https://umami.etiennerobert.com";
    example = "http://localhost:3001";
    description = ''
      reverse_proxy target for /stats/* requests. Default works from
      any host. Override to a same-host URL when umami runs locally.
    '';
  };
  config.services.caddy.virtualHosts = {
    "www.etiennerobert.com".extraConfig = ''
      encode zstd gzip
      handle_path /stats/* {
        reverse_proxy ${cfg.umamiUpstream} {
          header_up Host umami.etiennerobert.com
        }
      }
      handle {
        root * ${self.packages.${pkgs.stdenv.hostPlatform.system}.default}
        try_files {path} /index.html
        file_server
      }
    '';
    "etiennerobert.com".extraConfig = ''
      redir https://www.etiennerobert.com{uri} 308
    '';
  };
}

Drop the TODO http comment.

Repo: etrobert/setup

  • flake.nix: bump etiennerobert-com input to the new commit.
  • Tower's host config: import etiennerobert-com.nixosModules.default and
    set services.etiennerobertCom.umamiUpstream = "http://localhost:3001";
  • modules/server.nix: add @ and www to services.ddclient.domains.
  • modules/lan-dns.nix: add two host-record entries:
    • "www.etiennerobert.com,192.168.0.130"
    • "etiennerobert.com,192.168.0.130"

Verify before DNS flip (still resolving to Vercel publicly):

  • nix eval .#nixosConfigurations.tower.config.system.build.toplevel.drvPath
    succeeds — module assertions OK
  • nix flake check (statix + deadnix) clean

Manual operator steps (not in any PR)

After PR 2 merges and tower is rebuilt:

  1. Verify Namecheap has A records for @ and www. Both should already
    exist (currently pointing at Vercel's IPs). If either is missing for some
    reason, create a placeholder — Namecheap's dynamic DNS protocol can only
    update existing records, not create them (same gotcha hit with the
    umami subdomain).
  2. Trigger the ddclient timer (sudo systemctl start ddclient on tower)
    to push the WAN IP 91.64.99.245 to both records.
  3. Wait for propagation + Caddy ACME. Watch journalctl -u caddy -f
    within ~1 min of DNS resolving correctly, certs for
    www.etiennerobert.com and etiennerobert.com should be obtained.
  4. Verify externally:
    • https://www.etiennerobert.com/ → 200, content from tower
    • https://etiennerobert.com/ → 308 to www variant
    • https://www.etiennerobert.com/stats/script.js → 200
    • Load in browser with uBlock on; new event appears in umami dashboard
  5. Rollback path if Caddy can't obtain certs: revert the @ and www
    records at Namecheap to Vercel's IPs. The setup repo changes are harmless
    on tower without working DNS — the vhosts just fail to acquire certs and
    serve nothing for those names. test.etiennerobert.com keeps working
    independently as a known-good fallback for confirming Caddy/umami
    plumbing on tower while the production vhost is broken.

Cleanup (final tracked step)

Once tower has served www.etiennerobert.com cleanly for a week (no cert
renewal blips, no surprise visitors with broken setups):

Tear down Vercel:

  • Delete vercel.json from etrobert/etiennerobert.com — the rewrites
    are now in Caddy, not Vercel.
  • Decommission the Vercel project at vercel.com.

Tear down test.etiennerobert.com:

  • modules/server.nix: delete the test.etiennerobert.com virtualHost
    block (including the /stats proxy added in PR 1).
  • modules/server.nix: remove "test" from services.ddclient.domains.
  • modules/lan-dns.nix: remove the "test.etiennerobert.com,192.168.0.130"
    host-record entry.
  • Optionally delete the test A record at Namecheap — once it's out of
    ddclient it just won't update; harmless either way.

Out of scope

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions