Personal infrastructure

Fixed-IP Hosting Runbook

How websites get published to the personal AWS Lightsail host, why every hostname resolves to a single IPv4 address, and how two AI agents share the machine without stepping on each other.

Static IPv4
18.138.156.135
Region
ap-southeast-1
Web server
Caddy + auto TLS
Rollback depth
5 releases

Why one fixed IP

The constraint that shapes everything: the intended audience sits behind a corporate firewall that whitelists destinations by IPv4 address, not by domain name. A page is only genuinely reachable if every resource the browser must fetch lands on the same whitelisted address.

So the topology is deliberately old-fashioned — one Lightsail instance, one attached static IP, many hostnames served by Caddy through SNI virtual hosts. No CDN, no proxy, no Cloudflare orange cloud, because each of those would replace the destination IP and silently break the whitelist.

The rule that follows from it

Critical assets — scripts, stylesheets, fonts, images, media, iframes — must come from the same host or another name resolving to the same IPv4. A single fonts.googleapis.com link is enough to break the page for the people it was built for. Ordinary hyperlinks are fine; anything the browser must load is not.

Topology

ComponentDetail
Instancecodex-static-sites-micro
Static IPcodex-static-sites-ip → 18.138.156.135
DNSWildcard *.falconshire.com A record straight to the static IPv4. No AAAA — an IPv6 answer would divert an IPv4-only client.
Hostnames<slug>.falconshire.com, derived server-side from a validated DNS label
Apexfalconshire.com issues a 301 to the primary library
Releases/srv/static-sites/sites/<slug>/releases/, with an atomic current switch
FirewallTCP 80 and 443 open; TCP 22 public-key only, no root login, no passwords

The publish pipeline

Publishing never touches the web server configuration by hand. The client packages a build; the server validates it independently and installs it.

  1. Build and test locally. Static export is preferred whenever the requirements allow it.
  2. Preflight. The build root must hold a real index.html. Symlinks, device files, .git, .env*, private keys and package caches are rejected outright, as is any critical asset whose origin does not resolve to the whitelisted IPv4.
  3. Package. A UUID-named tarball of regular files and directories only, hashed with SHA-256 locally.
  4. Upload. SCP into the deploy account's fixed incoming directory. That account can do nothing else.
  5. Invoke the wrapper. A single root-owned helper, static-site-admin, is the only privileged entry point.
  6. Server-side re-validation. Owner, size, digest, archive members, slug, mode and destination paths are all re-checked on the server. The client is not trusted.
  7. Atomic switch. An immutable release directory is created, current is flipped, the Caddy config is validated, then gracefully reloaded. A global lock serialises this, so two agents publishing at once is safe.
  8. Verify, or roll back. DNS, certificate, required paths, same-origin assets, and an anonymous external fetch. Anything fails and the release is rolled back — a successful upload is not a successful publication.

Security boundary

The design assumption is that the publishing client may be wrong or compromised, so privilege is kept off it.

The practical consequence: path-based routing such as /some-page under the apex is not available to the publisher, and adding it would mean hand-editing root-owned config. A new subdomain costs nothing instead — the wildcard record already resolves it.

Two agents, one machine

Two AI coding agents publish to this host. The slug namespace is flat and shared, so identical slugs would overwrite each other's sites. That is resolved by reserving a prefix.

NamespaceOwnerIndex
claude*Claude Codeclaude.falconshire.com
everything elseCodexsite-library.falconshire.com

Each agent's catalogue enumerates the server directly and filters to its own namespace, so neither library lists the other's sites and neither needs a hand-maintained list. Each agent is also barred from publishing, rolling back or deleting anything in the other's namespace.

Why enumeration, not a manifest

Both libraries read the live server rather than a checked-in list. A site that exists appears; a site that is removed disappears. There is no state to drift out of sync, and no shared file for two agents to edit concurrently.

Operating notes