At this point my autonomous stack project had reached a moment where I needed to go from planning to implementation. I'd mapped the dependencies, drawn the architecture, picked the software, gone back and forth on the trade-offs, and ended up with something that looked good on paper (or in a Markdown file).
Yet, as with all well-made plans, they barely survive contact with reality.
I thought the first phase would take the better part of a Saturday.
The beginning of the plan was straightforward and nothing I hadn't dealt with before. Provision a VPS. Harden the OS. Install Docker. Put a reverse proxy in front. Stand up a password manager. Move the credentials across.
What actually happened: I spent an embarrassing stretch of time escaping dollar signs, discovered that a container command which sounds exactly like "reload the configuration" does not reload the configuration, and fell down a DNS hole that had been getting deeper for the better part of a decade.
This is normal and something you should expect if you embark on a similar project. In fact, I would argue that the gap between planning and doing is usually the most interesting part of infrastructure work, where you get to flex your detective and research skills.
Standing up the foundation
For the VPS, the choice was simple: I picked a Hetzner VPS, sized for the workload I had planned. I already used them for a game server I've been running for years, so I knew I could rely on them for this.
Tara from the future: Hetzner raised prices several times through 2026. The April adjustment applied to existing products as well as new orders, so it reached the server I was already renting. The larger June standardisation, which doubled and in places nearly tripled some lines, applied only to new orders and rescales. AI data centres are buying memory faster than anyone can make it, the three big manufacturers have turned most of their output over to the high-bandwidth memory that feeds accelerators, and DRAM prices climbed by something close to ninety percent in a single quarter. The economics that make self-hosting look like a simple saving are quickly changing.
Then I installed the freshest version of Debian I could find before diving into the unglamorous part. Server hardening is a trudge, but given that this infrastructure was going to handle all of my data, it was not to be skipped. I won't bore you with the details, but it was all the standard web server stuff, plus setting up the Hetzner firewall. The one insight was that it's crazy how quickly scanners find new hosts online. Fail2ban was already getting busy long before I had finished the rest of the setup.
Then came the part I was somewhat dreading, because it was new to me. The plan is that for every service I deploy, it needs to be reachable over HTTPS, so rather than have each one juggle its own certificates, Caddy (a reverse proxy) sits at the front, terminates TLS, and routes each request to the right backend. A new service is a few lines of configuration and a certificate that provisions itself. Caddy asks Let's Encrypt, the request goes through, and there is valid HTTPS within seconds of the proxy coming up. Even as someone who's followed this work, I'm still amazed by how well-oiled the automatic certificate machinery has become in the last several years.
Tara from the future: Since I wrote this, Let's Encrypt has started issuing certificates that last six days and is moving its ordinary certificates toward a forty-five-day life. Renewing by hand at that cadence isn't difficult. It's effectively impossible. I'm glad I took the time to set this up.
Vaultwarden: the first real migration
Vaultwarden, a self-hosted re-implementation of the Bitwarden server, was the first service I deployed and the first real data migration. That order was deliberate. The password manager sits at the very bottom of the dependency graph: every other migration involves logging into something, and every login needs credentials.
Deploying it was easy. Configuring it was not. Vaultwarden needs a hash as part of setup, and getting Docker Compose, my terminal, and Vaultwarden to agree on how it should be passed around turned out to be unexpectedly fiddly. Then, when the admin interface finally opened, it again took me several web searches to figure out that, to register my account, I actually had to guess the URL. Once that was out of the way, the software was ready to use.
The password migration itself, the part I had been quietly dreading, was the least eventful thing in the whole exercise. I exported a CSV from Dashlane, pointed Vaultwarden's importer at it, and a decade of accumulated logins, seven-odd email identities, and accounts for services that no longer exist all moved across in a few seconds and a single file upload.
There is something both freeing and faintly unnerving about looking at a decade and a half of my digital identity sitting in a plain-text file that I'm reading through a text editor. I started using Dashlane out of convenience and stuck with it for years because of what I assumed to be real friction in moving all of my data. I appreciate that they made it easy to leave.
DNS archaeology
At this point, I needed to move my DNS zone from my old hosting provider to Hetzner. Frankly, I had not looked at it properly in years, so I took this as a chance to do some digging. Years of accumulated services meant that my DNS zone was less a configuration file than a sedimentary record of my own internet history.
I found subdomains aimed at Bitbucket, which I haven't used in at least ten years, and several A records for Tumblr blogs from a past life. Naturally, there were many verification records for services I no longer have accounts with, some I don't even remember. I had an entire second website on a subdomain that I forgot to retire. Also, a surprising number of ACME challenge records from years of automated renewals, piled up like silt. And then the email records, which at that point were pointing at Google.
Cleaning all of that up is a project of its own, and not for today. For now, I added what Caddy and Vaultwarden needed and started laying the groundwork for the email move: DKIM keys for Migadu, the relay I had chosen to handle outbound mail; an SPF record that authorises the new sending path alongside the old; and a DMARC policy. Migadu was one of the compromises I made in my design, as I didn't want to deal with running an SMTP server and the trials and tribulations that come with it.
Monitoring from day one
With the core pieces in place, I decided to deploy Uptime Kuma the same afternoon as Vaultwarden. You might be asking yourself: monitor what, exactly? Two services and a reverse proxy?
But monitoring is far easier to stand up when nothing is wrong than when something is on fire and you're trying to work out what happened.
Uptime Kuma watches HTTP endpoints, TCP ports, and the health of the individual containers on the host. Building observability in from the start is the infrastructure version of writing the test before the code. It feels like overhead right up until the first time it catches something you would otherwise only have heard about when something stopped working and you needed it most.
What I learned
The technical lessons were interesting, but nothing I didn't know before: shell escaping is hard, container tooling has its quirks, and DNS zones collect debris. The lesson that stuck was about the relationship between planning and doing. I was glad that the plan I had drawn was right about the big things so far: the hosting shape, the choice of software, and where the security boundaries went.
But what a plan can't tell you is the kind of detail work that ends up eating all your hours.
It is not a failure of planning per se. The value of an infrastructure plan is never that it prevents surprises. The real value is that when a surprise arrives, you can often quickly zero in on which part of the system it touches, and it gives you a framework for evaluating remediation options.
The plan largely survived contact with reality. I just needed more time, and considerably more shell escaping, than I had predicted.
This is part of the Autonomous Stack series, documenting my migration from proprietary services to self-hosted infrastructure. Previously: Three Servers and a Tunnel. Next: Eighty-Four Thousand Messages, One DNS Change.