This site

Astro, built to static files. No runtime, no database behind it, no framework shipped to the browser, no tracking and no analytics. Every colour, size and duration resolves from one token file, and seventeen render gates — contrast, keyboard operation, target size, reduced motion, RTL mirroring, silent overflow — run over every page in both themes before anything ships.

The edge

Caddy terminates TLS with certificates it obtains and renews on its own, serves over HTTP/2, redirects www to the apex and strips trailing slashes so each page has one URL, and returns the Spanish 404 to Spanish paths. Content is read from disk per request, so publishing restarts nothing.

Every response carries

Strict-Transport-Security
max-age=31536000; includeSubDomains
X-Content-Type-Options
nosniff
Referrer-Policy
strict-origin-when-cross-origin
Permissions-Policy
geolocation=(), camera=(), microphone=(), payment=(), usb=()
Content-Security-Policy
default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; font-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'

No third-party origin is permitted at all. Fonts, styles and scripts are served from this host, so nothing on the page can call anywhere else.

Independent scanners, so this is checkable rather than claimed:

Getting a change out

  1. Gate and pushseventeen gates, then origin
  2. Pullon the host, via a deploy key
  3. Build besideinto a directory nobody is serving
  4. Swaptwo renames
The gates run before the push, on my own machine. They call scripts that are not part of the deployed repository, so the server could not run them even if it tried.

The swap is the part that matters. Astro clears its output directory when a build starts, so building straight into the directory Caddy serves would empty the site for the length of every build, and leave it empty if that build failed. Building beside it and renaming makes the change effectively instant and leaves the previous build in place, so a rollback is the same two renames in the other order — no reload, nothing to reconfigure.

The host

One bare-metal Debian machine that I install, patch and answer for. It runs this site alongside a number of other services, described here by what they do rather than by where they are.

Reverse proxy and TLS
Caddy, in front of every site and service on the box.
Relational database
PostgreSQL, on the fastest disk and deliberately off the media pools.
Object storage
An S3-compatible store for application media.
Containers
Docker, with persistent volumes pinned to a dedicated disk so a full root partition cannot take the daemon down with it.
Source control and delivery
A self-hosted Git forge with push-to-deploy pipelines. Each repository gets its own key, so a compromised pipeline reaches one project rather than the host.
Local model inference
For work where the data cannot leave the machine it is already on.
Monitoring and hardening
Per-host metrics with alerting rules, SMART disk monitoring, and fail2ban on the exposed surface.

How it fails

It is one machine on a residential connection with a dynamic address, which is worth saying plainly on a page about reliability. A dynamic DNS updater watches that address and republishes it when it changes, so an address change costs the poll interval plus the record TTL. There is no second machine, so a kernel update takes everything down together — deliberately, at a time I pick. That is the right trade for a personal host and the wrong one for a company, which is why the production systems in the case studies are built differently.