The stack behind this blog (and everything else I self-host).

Share
The stack behind this blog (and everything else I self-host).
Photo by Mahmudul Hasan / Unsplash

This post is served from a Mac Mini sitting on my desk, not from a hosting provider. I recently moved this blog off Ghost's managed platform and, at the same time, consolidated the various self-hosted services I already ran into a single, documented infrastructure-as-code setup. This is a write-up of the architecture, the tools involved, and the reasoning behind each choice, aimed at anyone with a similar itch.

Motivation

Three factors drove this. First, cost: a managed Ghost subscription for a low-traffic personal blog is hard to justify against hardware I already own and leave running. Second, data locality: some of what I host relates to my Prospect trade-union representative work, and I'd rather that data sit on infrastructure I control end-to-end than on a third party's platform, however reputable. Third, and I won't pretend otherwise, there's genuine value in understanding a system by building it — reading about reverse proxies is not the same as debugging one at 11pm.

Architecture

The setup is two machines behind a router, not one box doing everything. A Mac runs the majority of the actual applications, each in its own container. A Raspberry Pi sits in front as the only internet-facing host — it's the sole target of port forwarding on the router, terminates TLS for every service, and is the only machine with anything resembling a public attack surface. Nothing on the Mac accepts a connection that didn't first pass through the Pi.

This is a conventional reverse-proxy topology, but the reasoning is worth stating explicitly: it collapses "keep N services patched and hardened" into "keep one reverse proxy and N backend services patched," while ensuring a compromise or misconfiguration in any single backend application doesn't expand the network's actual attack surface. The router itself does its own filtering in front of both machines, so the Pi and Mac aren't even fully exposed to each other by default, only to what the router explicitly allows through.

The core tooling

Containers, not host installs, throughout. Every application runs isolated, with its own dependency set and no ability to interfere with anything alongside it. On the Mac, that means Podman rather than Docker — specifically for its rootless-by-default model, which is a better fit for a machine that also does ordinary desktop work. The trade-off is a genuine one: Podman's macOS implementation runs containers inside a lightweight Linux VM, and published ports have to bind to the Mac's real LAN interface rather than a wildcard address, which is a non-obvious gotcha the first time you hit it.

The Pi runs Caddy as the reverse proxy. Caddy's automatic HTTPS, backed by Let's Encrypt, removes an entire category of operational toil — certificate issuance and renewal are handled transparently, with no cron jobs or manual intervention. Routing for every service in the stack is defined declaratively in a single Caddyfile, along with per-service hardening: security headers, and rate limiting on authentication endpoints for the applications that don't implement their own brute-force protection (several don't, which was a genuine surprise).

The router is a DrayTek Vigor unit, configured to forward only the ports Caddy needs and nothing else.

The applications

This blog runs on Ghost, self-hosted via its official Docker image against a MySQL backend (Ghost's documentation is explicit that MySQL, not SQLite, is the only supported production database). Its native web analytics — the Stats page in the admin panel — is powered by Tinybird, a real-time analytics platform Ghost adopted as its official backend as of Ghost 6.0.

Alongside it:

  • copyparty — a single-binary file server I use for ad hoc file transfer and a small public read-only share, plus a password-gated write-only drop box for anyone who needs to send me something.
  • Memos — a lightweight self-hosted note-taking tool, which I use specifically for Prospect-related writing.
  • Vikunja — open-source project and task management, used for anything with real structure (as opposed to recurring chores).
  • Nimbus — a homelab monitoring dashboard, running on the Pi rather than the Mac deliberately, so it stays up to report a problem even if the Mac itself is the thing having the problem.
  • Speedtest Tracker — periodic connection speed tests, logged over time, mostly useful as evidence when an ISP's service degrades.

Not everything survived. I evaluated Donetick, a lighter chore-tracking tool, as a companion to Vikunja for recurring household tasks. Getting a working avatar-upload feature required standing up a self-hosted S3-compatible object store as a dependency for what amounted to a minor feature. When the first candidate for that role, MinIO, turned out to have had its open-source edition effectively discontinued upstream, and the replacement introduced its own integration issues, I removed Donetick entirely rather than keep expanding the dependency graph to support it. Vikunja alone covers the requirement adequately.

Where the actual time went

Standing up a new service is rarely the hard part — that's a Compose file and a domain entry, typically under fifteen minutes. The time sink is always the integration detail that doesn't match your specific environment.

The clearest example was wiring up Ghost's Tinybird-backed analytics. The documented setup flow authenticates the Tinybird CLI via an interactive OAuth device-code or browser-callback flow, which assumes the CLI is running on the same machine as the browser completing the login. Mine was running inside a container, reached over SSH, which broke that assumption in two different ways depending on which auth method I tried — a callback to a `localhost` port that isn't reachable from inside the container's own network namespace, and a device-code flow that never picked up the browser-side approval regardless of where the CLI itself ran. The eventual fix was to skip interactive login entirely: Tinybird's CLI supports non-interactive authentication via a `--token` flag against a workspace admin token, which is fully scriptable and, in hindsight, the option I should have reached for first.

The second recurring issue was image freshness. Several of these projects publish to a well-known registry under a tag that lags meaningfully behind the project's actual current release — in one case, comparing the "official" Docker Hub tag against the project's real GitHub releases showed it was over two major versions stale, missing several disclosed CVEs in the process. "Widely-used Docker tag" and "current, patched software" are not the same claim, and I now verify both independently before deploying anything, rather than treating a Docker Hub listing as authoritative.

Operational baseline

A few practices apply uniformly across the stack, regardless of how minor a given service seems:

  • Secrets are never stored alongside configuration. Every application's compose definition reads credentials from a gitignored environment file; the tracked repository holds only infrastructure-as-code and template files with placeholder values.
  • Images are pinned to specific, verified versions rather than floating tags, checked against the upstream project's actual release history and security advisories before deployment.
  • Registration is closed by default on anything holding data with no sharing intent, with accounts provisioned deliberately via CLI rather than a public sign-up form. It's left open only where sharing is the actual point.
  • Rate limiting is applied at the reverse proxy for every authentication-adjacent endpoint, independent of whether the backend application implements its own — several of the ones I run don't.

Would I recommend it

If the appeal of understanding a system by operating it resonates, yes, without reservation — accepting that a nontrivial fraction of the time investment goes into integration problems that have nothing to do with the core competence of any individual tool. If the goal is simply a blog or a notes app with minimal ongoing maintenance, the hosted equivalents of most of what's listed here are genuinely good products, and I'd not try to talk anyone out of using them.

For me, the case was never primarily financial, even though the numbers work out favourably. It's that I can now state precisely what happens to data I care about, end to end, and that's worth more to me than the hours it cost to get here.