Moving the blog stack to Europe (kind of)

AI-generated, one-shot from the blog title via Gemini (Nano Banana).
Two technicians in workwear pushing a black server rack on wheels from a van labeled Data Center toward a building entrance, with EU member-state flags raised above the entry

There’s a version of this story where I move everything to Europe in one go and write a triumphant post about digital sovereignty. The actual version is less clean. As of this week, the site you’re reading runs from a small Hetzner box in Nuremberg, and most of the supporting services moved with it. Some of the reasoning fed into my latest essay: How DORA Made Sovereignty a Bank Problem.

Why now

Two things converged. First, GitHub Pages started feeling more like a constraint than a freebie: no access logs, a hard cap on file size, no control over response headers without bolting on a Cloudflare Worker, no place to run anything dynamic next to the static site. Second, the EU jurisdictional question stopped feeling academic. Newsletter subscriber records, draft posts, analytics counters, backup archives: all of it sat in US-controlled storage by default.

Self-hosting isn’t a moral position. It’s a series of small choices about where data lives and how much operational overhead you’re willing to absorb in exchange for control. Longer-term I want to be off the major hyperscalers entirely.

Compute: GitHub Pages → Hetzner CPX21

The site moved from GitHub Pages to a Hetzner CPX21 in Nuremberg. Debian, four virtual cores, four gigabytes of RAM, plenty of headroom for a Hugo blog and the supporting services. Hetzner publishes its CO₂ footprint per server class and runs its EU fleet on renewable contracts.

For now, GitHub Pages remains a warm standby. The deploy workflow is still wired up, so if anything goes wrong with the self-hosted setup that I can’t fix in an hour, flipping DNS back gets the site online while I sort it out. Sovereignty is good. A working escape hatch is also good.

Object storage: R2 → R2 EU jurisdiction

The images and newsletter archives that the site loads from static.philippdubach.com used to live in a Cloudflare R2 bucket with no jurisdictional pinning, which in practice meant US-resident metadata. Hetzner Object Storage and Scaleway are both on the table, and I’m still weighing them.

R2 has zero egress fees, the API is S3-compatible, the Workers integration is built in, and the image-resizing pipeline I’d already wired up depends on it. Walking away from all of that to gain a one-step shorter jurisdictional chain felt like the wrong trade, so the bucket moved to Cloudflare’s EU jurisdiction (static-eu) instead.

This part of the story doesn’t fit the clean Europe-versus-US framing. Cloudflare is still US-headquartered. EU jurisdiction means the data stays in EU data centers and the company applies stricter handling, but a US court can still compel disclosure under the CLOUD Act.

Newsletter: nothing at all → self-hosted Listmonk

Moving away from my fully bootstrapped newsletter generator. Listmonk is open source, runs in about 50 MB of RAM, and stores its data in the same Postgres instance I already had running for other services. Subscribers, campaigns, templates: all of it now lives on the same box as the blog itself.

I’m now responsible for deliverability, suppression lists, bounce handling, and the surprisingly large number of small operational decisions that hosted newsletter services hide from you. Sending mail still goes through Resend over standard SMTP. Operating my own outbound mail server is a level of pain I’m not willing to absorb for marginal sovereignty gains, and Hetzner blocks port 25 on new accounts anyway.

Source code: GitHub → Forgejo

This was the move I expected to regret and didn’t. Forgejo is a Gitea fork that runs as a single Go binary, includes a web UI, supports webhooks, and behaves like GitHub for every day-to-day operation. Pushing code to my own remote and watching a webhook fire a Hugo rebuild on the same box has a closed-loop simplicity I didn’t realize I was missing.

I still mirror to GitHub for the public-repo discoverability and the social signal. The self-hosted instance is the source of truth; GitHub is the read replica.

Analytics: already on GoatCounter, still on GoatCounter

Analytics never went through Google. The previous setup used a hosted GoatCounter, which is open source, privacy-preserving, and stores no personal data. The migration moved it onto my own box. Same software, different host, same privacy stance.

Backups: a thing I now have

Before the migration, “backup” meant whatever GitHub kept of my repo and a vague trust that my newsletter provider would still exist next month. After the migration, it means a nightly restic snapshot to a Cloudflare R2 EU bucket, encrypted, with monthly integrity checks that email me if anything is wrong.

What I kept on US infrastructure

Cloudflare. The CDN, the Workers that handle subscribe forms and edge logic, the cache rules, the rate limiting on admin endpoints. Moving off Cloudflare would mean rebuilding the edge layer that makes the site fast everywhere and the spam controls that keep brute-force login attempts away from the self-hosted admin panels. I’m not ready for that trade.

Resend, for outbound mail. They handle SPF, DKIM, DMARC, reputation management, and the dozen other things that determine whether your email lands in the inbox or the spam folder. European alternatives exist, and I’ll look at them next.

GitHub, for the public mirror. The friction cost of asking everyone to discover my code on a self-hosted Forgejo instance is higher than the sovereignty benefit, given the code is open source and the source of truth is on my box anyway.

№ 083 5 min Tech Updated