Hetzner vs DigitalOcean: Same Spend, Different Ceiling
I moved NWOS client workloads from DigitalOcean to Hetzner and got roughly 4x the hardware for the same monthly bill. Here's what actually changed.
I've been a DigitalOcean customer since roughly 2014. I referred clients to them, built deployment pipelines around their API, and generally considered them the sensible default for managed VPS work. Last year I moved a meaningful chunk of NWOS hosting off DO and onto Hetzner Cloud, and the hardware difference was jarring enough that I want to write it down before I forget what surprised me.
The short version: same monthly spend, roughly 4x the CPU and RAM. That's not a rounding error. That changes what you can run.
What Problem I Was Actually Trying to Solve
NWOS runs managed hosting for clients - mostly Laravel applications, Postgres databases, Redis, some queue workers, the occasional background job that crunches biotech data or processes print job files. These aren't giant workloads, but they're not toy apps either. They need to be reliable, they need enough headroom that a spike doesn't take a client down, and they need to be economical enough that I can price hosting competitively.
DigitalOcean's $48/month Droplet gets you 8 GB RAM and 4 vCPUs. That's a reasonable app server. Hetzner's CX32 at roughly $15/month (prices fluctuate slightly with EU/USD exchange) gets you 8 GB RAM and 4 vCPUs too - so right there you're already ahead. But the comparison that actually got my attention was the dedicated CPU line. Hetzner's CCX23, which is around $50/month, gives you 8 dedicated vCPUs and 32 GB RAM. DigitalOcean's closest equivalent - a CPU-optimized Droplet with dedicated cores and 32 GB RAM - runs somewhere north of $200/month.
That's the ceiling I mean. At the $50/month tier, the workloads I can viably host shift considerably.
The Migration Path
Hetzner Cloud has a decent API and a Terraform provider that works. I'm not going to pretend the migration was painless - I'll cover the gotchas below - but the provisioning side is straightforward once you wrap your head around their project and server model.
Here's the basic server provisioning flow I use via their API, called from a simple PHP deployment helper I wrote internally:
<?php
$apiToken = env('HETZNER_API_TOKEN');
$baseUrl = 'https://api.hetzner.cloud/v1';
function hetzner_post(string $endpoint, array $payload): array
{
$apiToken = env('HETZNER_API_TOKEN');
$baseUrl = 'https://api.hetzner.cloud/v1';
$ch = curl_init($baseUrl . $endpoint);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiToken,
'Content-Type: application/json',
],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status >= 400) {
throw new RuntimeException('Hetzner API error ' . $status . ': ' . $response);
}
return json_decode($response, true);
}
function provision_server(string $name, string $serverType, string $image, int $sshKeyId): array
{
return hetzner_post('/servers', [
'name' => $name,
'server_type' => $serverType, // e.g. 'cx32', 'ccx23'
'image' => $image, // e.g. 'ubuntu-24.04'
'location' => 'ash', // Ashburn, VA - their US East datacenter
'ssh_keys' => [$sshKeyId],
'user_data' => cloud_init_script(),
]);
}
function cloud_init_script(): string
{
// Minimal cloud-init: set hostname, install fail2ban, harden SSH
return base64_encode(file_get_contents(__DIR__ . '/cloud-init/base.yaml'));
}
Nothing fancy. The API itself is clean - RESTful, predictable responses, good documentation. Creating a firewall, attaching a floating IP (their equivalent of a reserved IP), adding volumes - all of it follows the same pattern. I haven't hit any API weirdness that made me want to throw something.
For volumes (block storage for Postgres data directories), it's a similar pattern:
function attach_volume(string $volumeName, int $serverId, int $sizeGb): array
{
// Create the volume first
$volume = hetzner_post('/volumes', [
'name' => $volumeName,
'size' => $sizeGb,
'location' => 'ash',
'format' => 'ext4',
]);
$volumeId = $volume['volume']['id'];
// Then attach it to the server
return hetzner_post('/volumes/' . $volumeId . '/actions/attach', [
'server' => $serverId,
'automount' => true,
]);
}
Block storage is $0.052/GB/month. DigitalOcean charges $0.10/GB/month. Same pattern, different ceiling.
The Gotchas That Bit Me
Datacenter locations are limited and the US presence is thin. Hetzner's roots are European - most of their infrastructure is in Nuremberg, Falkenstein, and Helsinki. Their US locations are Ashburn, VA and Hillsboro, OR. That's fine for most of my clients (I'm in Seattle, most clients are US-based, latency to Ashburn is acceptable), but if you have clients who need a specific US region for compliance reasons or who are extremely latency-sensitive on the West Coast, Hillsboro is the better pick and the inventory there is smaller.
Shared vs. dedicated CPU matters more than the label suggests. The CX line (shared vCPU) is genuinely shared. For web application servers handling normal HTTP traffic, it's fine - you get plenty of burst. For anything doing sustained CPU work (data processing, PDF generation at volume, ML inference), you want the CCX dedicated line. I learned this the uncomfortable way when a print management client's job processing started showing inconsistent timing. Moved to CCX, problem gone.
IPv4 costs extra now. Hetzner started charging for primary IPv4 addresses - about $0.50/month per server. It's not a lot, but it surprised me when it showed up. If you're running many small servers, it adds up. IPv6 is still free, and their internal networking between servers in the same project is free too. Worth designing your internal service communication to use private IPs.
Their support tier is slower than DigitalOcean's. I'll be honest about this one. DigitalOcean's support, especially at higher spend levels, is responsive. Hetzner's ticket support is slower and more procedural. I've never had a critical infrastructure failure on Hetzner, so I haven't needed emergency support, but I've had routine questions take a day or two to get answered. If white-glove support matters to you or your clients, that's a real consideration.
The console is functional but not beautiful. DigitalOcean's control panel is genuinely pleasant to use. Hetzner's Cloud Console is fine - it does what you need - but it feels more utilitarian. Honestly this matters zero to me since I'm scripting everything anyway, but clients who want to log in and poke around might feel the difference.
Snapshots are not instant. Powering down a server and taking a snapshot can take several minutes depending on disk size. DigitalOcean handles this similarly, but I've seen Hetzner snapshots take longer on larger disks. Not a dealbreaker, just plan your maintenance windows accordingly.
When I Reach for Hetzner
Hetzner is now my default for new NWOS-managed hosting deployments where the client's compliance requirements don't mandate a specific cloud vendor. That covers most of my clients.
Specifically, I reach for it when:
- The workload is a Laravel app plus Postgres plus Redis and I want to give it real headroom without a painful monthly conversation
- I need a dedicated CPU server and DigitalOcean's pricing for that tier would require justifying a 4x cost bump to a client
- I'm running background workers or queue processors that do real CPU work and need consistency
- I want block storage for a database data directory without paying $0.10/GB
I would not reach for Hetzner when:
- A client has compliance requirements that explicitly name AWS, Azure, or GCP (this happens in healthcare more than you'd expect)
- I need managed Kubernetes with a mature control plane - Hetzner has managed K8s but it's not as polished as DOKS or EKS
- The workload needs object storage with a mature ecosystem - I still use DigitalOcean Spaces or S3 for object storage and just point Hetzner servers at it
- The client needs West Coast US latency and volume guarantees I can contractually stand behind
DigitalOcean isn't bad. I still use it. Their Spaces product is solid, their App Platform is useful for certain things, and their managed databases have saved me maintenance headaches on smaller client projects. But for raw VPS workloads where I control the stack, Hetzner's price-to-hardware ratio is hard to argue with.
The Actual Numbers After a Year
I moved three client environments to Hetzner last year. Across those three, I went from roughly $310/month in DigitalOcean spend to roughly $95/month in Hetzner spend for comparable or better hardware. Some of that delta went back to clients as reduced hosting fees. Some of it went into larger servers than I would have provisioned at DO prices, which means those applications have headroom they didn't have before.
One of those clients is a Seattle-area e-commerce operation that gets seasonal traffic spikes. Last holiday season was the first one where I didn't get an anxious call about the site slowing down. Could be coincidence. But I had twice the RAM available for the same money, and I'll take that.
If you've been on DigitalOcean for years because it's familiar and it works, I get it - I was the same. But it's worth running the comparison on whatever you're currently paying. The gap at the $50-100/month tier is wide enough that it changed how I think about what's affordable to provision for a client.
Need help shipping something like this? Get in touch.