I Rent a Server and Run My Own Website. Here's the Stack.
Every few months, a fresh crop of screenshots makes the rounds. Someone's side project got a moment of traffic, or a bot decided to hammer it, and the hosting platform that felt free at signup handed them a four-figure bill.
A portfolio site that ate 1.1TB of transfer and returned an $1,100 invoice. A hobby app that went from a $20 plan to $700 overnight. Nobody did anything wrong. They just built on a meter they couldn't see and couldn't cap.
I've watched that anxiety push a lot of developers back toward something that felt old-fashioned two years ago: renting an actual server and running your own thing on it. Not serverless, not a per-seat dashboard, just a box you pay a flat fee for and control completely.
That's how my own site runs, and after twenty-plus years of building software I have opinions about why. This is the whole setup, the pieces, the reasoning, and the afternoon I lost to a single missing environment variable.
The month everyone got a scary bill
The pitch for platforms like Vercel and Netlify was always convenience, and it's real. Push to git, get a URL, never think about a server. For a long time that trade looked obviously correct.
What changed is that the bill stopped being predictable. Vercel's bandwidth overage sits at $0.15 per gigabyte with no default spending cap, which is fine right up until it isn't. Netlify has reshuffled its pricing model twice in under a year, and if there's one thing worse than a high price it's a price you can't forecast.
I'm not here to dunk on either. They solve a genuine problem, and for a team shipping fast they can absolutely be worth it. But the calculation flipped for a lot of people, myself included.
A small server from Hetzner or a similar host runs a dozen containers for the price of a couple of coffees a month, and the number on the invoice is the same every single month whether ten people visit or ten thousand. Predictability turns out to be a feature you appreciate more the longer you've been doing this.
Shopping for a box: Hetzner, Contabo, Hostinger, OpenMetal
Before I landed on Hetzner I actually shopped around, because "cheap VPS" is a crowded market and the sticker price is rarely the real price. Four hosts made my shortlist, and they taught me something about how differently these companies think.
Contabo is the one that looks unbeatable on paper. You get an absurd amount of RAM and storage per euro, roughly four vCPUs and 8GB for around the price of a Hetzner box half its size, and crucially no renewal trap where year two secretly doubles.
The catch shows up in the benchmarks and the support forums. Network throughput and disk IO are inconsistent, and the stories about slow ticket responses are common enough that I filed Contabo under "great second box for backups, risky for anything a customer touches."
Hostinger plays a different game. The intro price is genuinely tempting, until you notice it's roughly half the renewal price and only applies if you prepay a year or two up front. That's not a scam, it's just marketing, but it means the sticker number is fiction. You're really signing up for the renewal rate, so compare on that.
OpenMetal was the interesting outlier. I looked at it and quickly realised it isn't in the same category at all. They sell managed private cloud and bare metal, real dedicated hardware running OpenStack, with entry pricing that starts north of $700 a month.
It's a serious product for companies fleeing a $20,000 AWS bill or carrying compliance requirements. For one person's website it would be like renting a warehouse to store a bicycle.
Which left Hetzner, and the deciding factor for me wasn't even price. It was location. My site's data sits in the EU because I'm in the EU, and Hetzner's data centres in Falkenstein, Nuremberg, and Helsinki mean data residency is a non-question.
Yes, they raised prices twice in 2026 and the "too cheap to think about" era is over. A four vCPU, 8GB ARM instance still lands near €10 a month with solid IO and a platform I don't have to babysit. For a solo developer who wants cheap plus EU residency, it's still the default answer, and I say that having genuinely looked at the alternatives.
The stack, and why each piece earns its place
The site itself is a Next.js application on the App Router, backed by a Postgres database that I talk to through Prisma. One deliberate choice worth calling out: the articles you're reading live as rows in that database, not as Markdown files on disk.
That means the content has structure I can query, relate, and evolve, and the site is a real application rather than a folder of files pretending to be one.
All of it runs in Docker containers on that Hetzner server. In front of them sits Traefik, acting as the reverse proxy that handles routing and TLS certificates. Deployments go out through GitHub Actions, and every night the database backs itself up to Cloudflare R2.
None of these are exotic choices. The point isn't novelty, it's that I understand every layer and can open any of them when something misbehaves.
A container built like it's going to production
The Dockerfile is a multi-stage build, and the stages exist for a reason. The first installs dependencies, the second builds the app, and the third is a lean production image that carries only what it needs to run.
Nothing from the build toolchain rides along into production. The container runs as a non-root user, because a web-facing process has no business running as root.
The part I'm most fond of is the entrypoint. Before the app starts, a small script waits for the database to actually accept connections, then runs the migrations, then hands off to Next.js:
#!/bin/sh
set -e
# Wait for the database to be reachable before doing anything.
until nc -z "$DB_HOST" "$DB_PORT"; do
echo "Waiting for database..."
sleep 1
done
# Apply any pending migrations. Safe to run on every boot.
npx prisma migrate deploy
exec node server.jsThat prisma migrate deploy line is the quiet hero. It only applies migrations that haven't been recorded yet, so running it on every single container boot is a no-op when there's nothing to do. The container is safe to restart a hundred times.
The one caveat, if you ever scale to several instances starting at once, is that they can race on the migration step, at which point you want a single instance to own migrations. For one box, it's a non-issue.
The one-line bug that took down TLS
Here's the war story, and it's a good one, because the symptom lived four layers away from the cause.
One day the site started returning a Cloudflare 526 error. That's Cloudflare telling you it can't validate the SSL certificate on your origin server. So naturally I went hunting for a certificate problem, which is exactly the wrong place, because the certificate was never the problem.
The chain, unwound, went like this. Next.js in standalone mode reads an environment variable called HOSTNAME to decide what address to bind to. Docker, helpfully, sets HOSTNAME to the container's ID. So instead of binding to 0.0.0.0 and listening on every interface, my app bound to an address that only existed inside its own head.
The container's healthcheck, hitting 127.0.0.1:3000, got nothing back and marked the container unhealthy. Traefik saw an unhealthy container and quietly refused to route to it. With no real backend to serve, Traefik fell back to its default self-signed certificate. Cloudflare, set to validate origin certificates strictly, took one look at that self-signed cert and threw the 526.
A TLS error, four layers downstream of an environment variable I didn't know Docker was setting. The fix was one line in the Dockerfile:
ENV HOSTNAME=0.0.0.0That's it. Bind to everything, healthcheck passes, Traefik routes, real certificate, no more 526.
I lost an afternoon to it, and I'd lose it again happily, because now I understand a corner of the stack I'd never had a reason to look at. That's the tax you pay for owning the whole thing, and it's a tax I'm fine with.
One box, many apps, no shared config
The nice thing about Traefik is that it discovers what to route from the containers themselves. Each app declares its own routing in Docker labels, so there's no central config file that every service has to be registered in. A new app arrives with its labels and Traefik just picks it up.
There's a networking detail that matters here. The setup uses two networks: a public one that Traefik lives on, and a private internal one where the database sits. When a container is attached to more than one network, Traefik doesn't know which to use and will pick one at random, which is a fun way to create an intermittent bug. The fix is to tell it explicitly with a traefik.docker.network=web label.
The database, meanwhile, never touches the public network at all. It's reachable only from the internal one, so it's simply not exposed to the internet. Its data lives on a bind mount on the host, so it survives any container coming and going.
Deploying without ever SSHing in to poke around
I don't deploy by logging into the server and running commands by hand. That path leads to a server whose state nobody can reconstruct. Instead, GitHub Actions does the work.
On a push, it builds the Docker image for the server's architecture and tags it twice, once as latest and once with the exact commit SHA. It copies the compose file up to the server, then runs three commands over SSH: pull the new image, bring the stack up, prune the old images.
The compose file is the single source of truth for what's running. And tagging every image with its commit SHA means a rollback is trivial. If a deploy goes wrong, I point the compose file at the previous SHA and bring it up.
No rebuild, no scramble, just the exact image that was working ten minutes ago. That's the kind of boring reliability I've learned to value.
Backups I genuinely never think about
There's a small dedicated container whose only job is backups. At 3am it dumps the database, compresses the dump, and pushes it to Cloudflare R2 over a verified TLS connection.
It keeps recent copies locally too, so a restore is fast when I need one, and it prunes anything older than thirty days so the disk never fills.
I set it up once. I have not thought about it since, which is the highest compliment you can pay a backup system. The day I need it, it'll be there, and until then it's invisible.
When you should absolutely not do this
I'd be doing you a disservice if I made this sound like the obvious choice for everyone. It isn't. When you own the server, you own the whole job. Security patches are yours. Uptime is yours. There's no managed edge network absorbing a traffic spike or a denial-of-service attempt for you.
The honest failure mode of self-hosting isn't a dramatic breach, it's the slow slide where updates quietly stop happening a few months in because life got busy and nobody's paged when a cert is about to expire.
If that responsibility sounds like a chore rather than a thing you'd enjoy, the platforms exist for a reason and you should use them.
And if you want most of the ownership without hand-rolling every script, there's a healthy middle ground now. Kamal, the tool 37signals built to pull Basecamp and HEY off the cloud and back onto their own hardware, deploys containers to your servers with a single config file. Coolify and Dokploy give you a Vercel-like dashboard, preview deploys and all, running on a box you own.
Any of them will get you the flat bill and the control without asking you to become a part-time sysadmin. I hand-rolled mine because I wanted to understand every moving part, which is a preference, not a recommendation.
Why I still do it
Strip away the specifics and what's left is ownership. When something breaks on my site, I can follow it all the way down, from Cloudflare to Traefik to the container to the bind address the app chose, and I can fix it at whatever layer it actually lives.
That afternoon I lost to the 526 wasn't wasted time. It was me learning my own stack a little deeper, which is the thing that keeps this work interesting after two decades.
One server I understand completely. No per-seat dashboard, no meter I can't see, no invoice that surprises me.
If that trade appeals to you, rent a small box and put something on it. Worst case, you lose an afternoon to a missing environment variable and come out understanding a bit more than you did. That's not the worst way to spend a day.

