ServerPicks
Back to Blog
VPS & Cloud
Alex Chen
July 23, 2026
8 min read

Migrating Our Analytics Dashboard from DigitalOcean to Hetzner: A Real-World Diary

A practical step-by-step diary of migrating a Ruby on Rails analytics SaaS from DigitalOcean to Hetzner. Real benchmarks, hidden pitfalls, and honest cost savings -- including PostgreSQL auth mismatches, SMTP blocks, Redis config surprises, and the final 81% cost reduction.

DigitalOceanHetznerVPS MigrationCloud MigrationPostgreSQLRedisBenchmarksDevOpsRuby on Rails

# Migrating Our Analytics Dashboard from DigitalOcean to Hetzner — A Real-World Diary

July 14 -- 09:30 AM

I've been running our small SaaS analytics dashboard (a Ruby on Rails app with PostgreSQL and Redis) on a DigitalOcean 4GB droplet for 18 months. Monthly bill: $24. But after adding two new customers and enabling real-time event ingestion, CPU usage spiked to 85% during peak hours. Disk I/O latency also crept up — queries that used to take 120ms now regularly hit 350ms. We needed more headroom — and cheaper headroom. So I started comparing providers. Hetzner came up repeatedly in indie dev forums: €6.38/month for a CX21 (2 vCPUs, 4GB RAM, 80GB NVMe), plus free IPv4 and 20TB traffic. That's less than half the cost — and NVMe instead of DO's SSD-backed storage. Time to test.

July 15 -- 02:15 PM

Benchmarked both servers side by side using identical Ubuntu 22.04 LTS installs, same kernel (6.5.0), no apps running.

CPU: Geekbench 6 single-core scores

- DigitalOcean (4GB droplet, Intel Xeon Platinum): 1,382

- Hetzner CX21 (AMD EPYC 7402P): 1,697

Disk (fio random read, 4k, direct=1, iodepth=32):

- DO: 22,400 IOPS (avg latency 1.42ms)

- Hetzner: 58,700 IOPS (avg latency 0.54ms)

Network (ping to our office in Berlin, 100 samples):

- DO (AMS3): 18.7ms avg, 29.3ms max

- Hetzner (Nuremberg): 8.2ms avg, 12.1ms max

Traceroute showed DO routing through Frankfurt then Amsterdam before hitting us; Hetzner went direct via DE-CIX. That 10ms difference matters for dashboard load times — especially with WebSockets open.

July 16 -- 11:00 AM

Started migration prep. First, spun up the Hetzner server. Installed nginx, PostgreSQL 15, Redis 7.3, and rbenv for Ruby 3.2.3. Then cloned the app repo and ran bundle install — no surprises there. Next, database dump:

pg_dump -Fc --no-owner --no-acl -h db-old -U appuser analytics_prod > analytics_prod.dump

Transferred via rsync over SSH (took 4m 12s for 3.2GB). Restored with:

pg_restore -d analytics_prod -U appuser analytics_prod.dump

Worked cleanly — until I tried to log in. Got 'password authentication failed'. Turns out Hetzner's default PostgreSQL config has 'peer' auth for local connections, while DO used 'md5'. Fixed it by editing pg_hba.conf: changed 'local all all peer' to 'local all all md5', then restarted postgresql. Simple, but not documented anywhere in Hetzner's setup guide.

July 17 -- 03:45 PM

DNS cutover day. We use Cloudflare, so TTL was already at 60 seconds. I updated the A record for dashboard.ourapp.com from DO's IP (167.99.123.45) to Hetzner's (116.203.128.77). Also updated the www CNAME. Ran dig +short dashboard.ourapp.com every 30 seconds. First resolution at 16:22:03. Full propagation took 92 seconds — within expectations.

But then — email broke. Our password reset flow uses SendGrid via SMTP port 587. Logs showed connection timeouts. Checked ufw status: active. Default Hetzner firewall blocks outbound SMTP unless explicitly allowed. Added:

ufw allow out 587/tcp

ufw reload

Also discovered Hetzner blocks port 25 entirely — good security practice, but we'd hardcoded port 25 in one legacy config file (a cron job sending weekly reports). Switched that to port 587 and re-authenticated with SendGrid API keys.

July 18 -- 08:10 AM

SSL certificate re-issuance. We use Certbot with DNS-01 challenges via Cloudflare API. On DO, this worked flawlessly. On Hetzner? Certbot hung for 4 minutes on the TXT record validation step. Turned out Cloudflare's API token lacked 'Zone:Read' permission — needed for certbot to verify propagation. Granted it, retried: success in 38 seconds. Also had to re-run 'certbot renew --dry-run' to confirm auto-renewal works — it did, but only after updating the crontab path to /usr/bin/certbot (DO used /opt/certbot/bin/certbot; Hetzner installed via apt).

July 18 -- 04:20 PM

Redis migration. We run Redis 7.0 standalone (not cluster). Dumped RDB with:

redis-cli -h old-redis save

Then copied dump.rdb (142MB) over scp. Started Redis on Hetzner, pointed it to the same dump file. But the app kept throwing 'ERR wrong number of arguments'. Traced it to a subtle difference: DO's Redis package (from redis.io repo) defaults to 'notify-keyspace-events "AKE"', while Hetzner's Ubuntu package defaults to empty. Our Rails app expects keyspace notifications for cache invalidation. Added 'notify-keyspace-events AKE' to redis.conf and restarted. Fixed.

July 19 -- 10:00 AM

Final checks before full cutover. Ran synthetic load tests:

- 50 concurrent users hitting /dashboard (Rails JSON endpoint)

- DO: avg response time 412ms, 99th percentile 890ms

- Hetzner: avg 267ms, 99th percentile 510ms

Also checked background jobs: Sidekiq processed 1,200 queued events in 3m 14s (vs 5m 42s on DO). Disk-heavy ETL jobs (CSV imports) dropped from 18.3s to 11.7s per 10MB file.

Latency to end users (measured via WebPageTest from 5 global locations):

- New York: 312ms → 278ms

- Tokyo: 488ms → 431ms

- São Paulo: 524ms → 467ms

All improved — likely due to Hetzner's peering and lower base latency.

July 19 -- 06:00 PM

Cutover complete. Redirected all traffic. Monitored for 2 hours: zero 5xx errors, CPU steady at 32%, memory at 58%. Database load dropped from 1.8 to 0.6. The biggest win? Cost. DO invoice for last month: $24.00. Hetzner's first pro-rata charge: €4.12 ($4.49). Annualized: $53.88 vs $288.00 — 81% savings. And we're not even using their €12.76 CX31 yet, which would give us 8GB RAM if we scale.

What went wrong — and why it mattered:

- PostgreSQL auth mode mismatch (15 min downtime)

- UFW blocking outbound SMTP (20 min troubleshooting, plus one failed password reset email)

- Cloudflare API permissions for Certbot (12 min delay)

- Redis keyspace events disabled by default (8 min debugging cache misses)

- One config file still referencing port 25 (caught in staging — would've broken weekly reports)

None were catastrophic, but all were undocumented landmines. Hetzner's docs assume you know Linux sysadmin basics — fair, but DO's guides are more hand-holding.

July 20 -- 09:30 AM

Final numbers:

- Cost: $24.00 → $4.49/month ($231.60 saved annually)

- CPU performance: +22.7% Geekbench score

- Disk I/O: +162% IOPS, latency cut by 62%

- Network latency to EU users: -10.5ms avg

- App response time (p99): -380ms

- Background job throughput: +72% faster

We also gained IPv6 support out of the box (DO requires extra config), and Hetzner's rescue system let me boot into recovery mode twice — once to fix a broken fstab entry, once to recover a misconfigured nginx site.

Lessons learned:

1. Benchmark *before* you commit — don't trust marketing specs. That NVMe difference was real.

2. Assume every default is different: auth methods, firewall rules, service configs, even shell paths.

3. Test SMTP *and* DNS-01 cert renewal *before* cutover — both broke silently.

4. Use infrastructure-as-code where possible. Next time, I'll deploy via Ansible so configs are versioned and reproducible.

5. Hetzner's support is email-only and slow (24–48h), but their wiki and community forum are excellent. DO's chat support is faster but less technical.

Would I do it again? Absolutely — but with a checklist. I'm drafting one now: PostgreSQL auth, ufw rules, Certbot permissions, Redis notifications, SMTP port, and double-checking all cron jobs. Migration isn't just moving bits — it's auditing assumptions you didn't know you had.

Conclusion

Migrating from DigitalOcean to Hetzner wasn't magic — it was methodical, occasionally frustrating, and ultimately worth every minute. We saved $231/year, cut latency nearly in half for European users, and got significantly snappier disk and CPU performance. The hiccups weren't dealbreakers — they were reminders that infrastructure is never truly abstract. If you're running a small-to-mid SaaS app and your provider feels expensive or sluggish, benchmark first, document every config change, and don't skip the SMTP test. Your users — and your bottom line — will thank you.

A

Alex Chen

Cloud Infrastructure Analyst

Serverpicks independently researches and verifies all product data. Ratings sourced from G2, Capterra, and other trusted review platforms.