A personal developer blog built to be fast, secure, and entirely self-hosted — no platform lock-in, no third-party runtime dependencies.
- Astro v6 — generates static HTML at build time, served via
astro preview. Zero client-side JavaScript by default. - Markdown & MDX — posts live in
src/content/blog/as typed Content Collections with frontmatter validation. - RSS feed + sitemap — auto-generated via
@astrojs/rssand@astrojs/sitemap. - Local fonts — Atkinson Hyperlegible served from
src/assets/fonts/, no external font requests. - pnpm — fast, disk-efficient package management. Requires Node ≥ 22.
- Vitest — unit and integration tests with v8 coverage.
- Playwright — end-to-end tests against the running site.
The entire runtime is two containers communicating over a private bridge network.
Internet ──► Caddy :443 ──► Astro app :4321
A two-stage Containerfile (Node 24 on Debian slim):
- Build stage — installs deps and runs
astro build. - Runtime stage — copies only
dist/,node_modules, and config. Runs as a non-rootastrouser (UID 1001) with a read-only filesystem, all Linux capabilities dropped, andno-new-privilegesenforced.
A custom Caddy build compiled with xcaddy, adding two plugins on top of the official image:
- coraza-caddy — the Coraza WAF with the OWASP Core Rule Set (CRS v4.7.0) baked into the image. All traffic is inspected before it reaches the app.
- caddy-dns/googleclouddns — ACME DNS-01 challenge provider, so TLS certificates are issued and renewed without opening port 80 or requiring a webroot.
Caddy also sets hardened response headers (HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy) and compresses responses with zstd and gzip.
- Local / CI:
compose.yaml(+compose.override.yaml) spins up the full stack withpodman compose up --build. - Production: Quadlet units in
quadlet/integrate the containers directly with systemd — no compose daemon required.
All infrastructure is version-controlled and reproducible.
| Layer | Tool | Details |
|---|---|---|
| Hosting | Vultr | VPS — 1 vCPU / 2 GB RAM, AlmaLinux 10, Seattle (sea) region. Reserved IPv4 and IPv6 addresses survive instance replacement. Daily automated backups. |
| DNS | Google Cloud DNS | Authoritative DNS for the site's domain. A service-account key is also used by Caddy's caddy-dns/googleclouddns plugin to complete ACME DNS-01 challenges for automatic TLS certificate issuance and renewal. |
| Cloud provisioning | OpenTofu | Manages the Vultr instance and reserved IPs as code. State stored remotely via a Cloudflare R2 backend. |
| Host configuration | Ansible | Roles: common, nftables (firewall), fail2ban (intrusion prevention), registry (private container registry), container_host (Quadlet + Podman setup). |