There’s a special kind of person who talks to themselves while migrating servers.

I am that person.

This post is basically the black-box recording of me moving two hobby projects-an auth service and an expense tracker called Pennywise-off Hetzner and onto Contabo, while Coolify, Cloudflare, R2, Postgres, and one very stupid vault mistake all tried to teach me humility.

If you’re here for a clean architecture diagram and zero drama: leave now. This is the other kind of migration story.


The setup: two apps, one slightly expensive cloud life

I have (had) two “real” hobby projects living on one machine:

  1. GoAuth-a small authentication service
  2. Pennywise-an expense tracker with monthly/yearly dashboards, category spending, the whole “I built this so I would finally understand where money goes” fantasy

Both ran under Coolify on a Hetzner box.

Coolify is the part of this story that quietly saves the day repeatedly. One UI for apps, databases, backups, env vars, logs, the whole zoo. For a solo hobby setup, it’s less “DevOps platform” and more “remote control for my little cloud animal.”

Architecture was deliberately boring:

  • One Postgres deployment
  • Multiple databases inside it (auth DB, Pennywise DB, random small app DBs)
  • Apps talk to that shared Postgres over Docker networking
  • Domains on Cloudflare
  • Backups pushed to Cloudflare R2 (S3-compatible, without me dating AWS and its billing personality)

Hobby-scale data, hobby-scale ambition. About five users on auth. Expense data for ~3 months. The full Postgres backup? Around 40 KB. Yes, kilobytes. The cloud does not care about your dignity.


Why leave Hetzner at all?

Hetzner is fine. Actually good. Provisioning servers, volumes, firewalls-all solid.

The problem wasn’t “Hetzner bad.”

The problem was my particular setup:

  • Bill hovered around ₹5,000 / ~$47 per month
  • Closest useful region for me was Singapore
  • Even with a small app and ~5–10 users, latency was visible
  • Autocomplete-ish features that bounce request/response a few times felt especially cursed

So picture this mental model:

You (India)  --->  ocean  --->  Singapore server  --->  DB  --->  back across ocean  --->  You
                      ↑
                 "why is this search so slow"

Indexes were fine. App was fine. Geography was not fine.

Also: ₹5,000/month for hobby projects is the kind of number that slowly becomes a personality trait.


The AI part of the migration (the useful part)

I did not carefully spreadsheet twelve providers like a responsible adult.

I screenshotted my Hetzner config, asked ChatGPT for cheaper equivalents, and it pointed me at Contabo.

Contabo’s website looks like it time-traveled from an earlier internet. Their prices, though, are the opposite of nostalgia trauma:

  • Similar machine: roughly €10–15 / month
  • In rupees-brain: maybe ₹1,500–1,800
  • Expected savings: around ₹3,000/month

And-this mattered-Contabo had an India location. Same general shape of machine (around 4 vCPU / 8 GB), less ocean in the hot path.

Decision made. Migration anxiety activated.


What I thought migration would be vs what it actually was

Brain before:

This will be tedious. Fragile. Possibly a weekend funeral for my uptime.

Brain after database restore:

Oh. Coolify already has backup/restore. R2 is already wired. The dump is 40 KB. This took seconds.

The plan was always:

  1. Stand up Coolify on Contabo
  2. Backup Postgres from old Coolify → R2
  3. Restore into new Coolify Postgres
  4. Point apps at new env / new domains
  5. Don’t cry

Steps 1–3 went suspiciously well.

Then came DNS.

DNS is where hobbies go to become character development.


Domain cutover: temporary subdomains vs just yeeting the DNS

ChatGPT suggested the grown-up path:

  • Create temporary subdomains
  • Point those at Contabo
  • Test thoroughly
  • Flip production DNS later

I considered that.

Then I remembered:

  • Only a few meaningful subdomains
  • Only users = family group
  • I already told the family group: please don’t touch the app for a bit

So I chose the plan that feels efficient until it isn’t:

Just change Cloudflare DNS to the new IP and deal with whatever explodes.

Something exploded immediately.


First explosion: I locked myself out of the old Coolify

Once DNS pointed at Contabo, the old Hetzner Coolify became awkward to reach.

Why? Because Hetzner’s firewall didn’t love “just hit random ports directly,” and my muscle memory still wanted the old dashboard for reference.

So I did the thing every migration guide pretends you won’t do:

  1. Screenshot the nice Hetzner firewall rules (HTTP / HTTPS / SSH)
  2. Remove firewall rules
  3. Still can’t get in
  4. Remove the firewall entirely from the instance
  5. Suddenly: access restored
  6. Quiet voice in head: this is not how adults migrate production systems

I logged into old Coolify anyway, updated domain settings for GoAuth and Pennywise on the new side as best I could, and kept going.

Also I forgot a password briefly and had a small existential crisis. Classic.


Second explosion: Coolify is up, but the internet says “SSL handshake failed”

New Coolify domain: something like coolify.deeka.work.

Symptoms:

  • Cloudflare unhappy
  • Browser unhappy
  • Me: “but I switched the proxy to Caddy already??”

I had switched the Coolify proxy from Traefik to Caddy (personal preference; old setup used Caddy). Proxy started. Config looked familiar. Still broken.

So I did what modern people do when reality stops making sense: I asked Codex to dig.

Codex dug. Literally dig. Also crt.sh. Also “may I leave the sandbox.”

What it found, in human language:

  • Port 80: Caddy answers, but with 503
  • Port 443: TLS handshake aborts before a real certificate shows up
  • Cloudflare translates that chaos into errors like 525 (“I tried to talk HTTPS to your origin and your origin ghosted me”)

So the chain was:

Browser → Cloudflare → Contabo IP → Caddy → ??? → sadness

Not:

Browser → Cloudflare → Contabo IP → Caddy → Coolify dashboard → joy

The embarrassing fix

I had never properly set Coolify’s instance domain / public URL in settings.

That field is not cosmetic.

When you save something like https://coolify.deeka.work, Coolify treats it as infrastructure:

  • Creates host routing for that hostname
  • Points it at the dashboard container
  • Reloads Caddy
  • Asks for HTTPS / Let’s Encrypt behavior
  • Fixes redirects, cookies, generated links, the whole “please act like a real site” layer

Before that setting: Caddy listening, nothing correctly claimed as the dashboard host → 503 / handshake weirdness.

After that setting + a little propagation: dashboard loads.

I then almost destroyed my password vault entry by overwriting the old Coolify password instead of creating a new entry. Pure luck: another browser tab still had the previous password. I do not recommend this as a backup strategy, even though it worked once.


Third explosion: auth deploys, then restart-loops on Postgres auth

GoAuth finally built and started… then started restarting like it owed money.

Logs:

password authentication failed for user postgres

I checked env vars. They looked right.

I checked the new Postgres. Data was still there after a restart (good; restore hadn’t evaporated).

I tried connecting via pgAdmin over SSH tunnel. Wrong password. Tried the “new” Coolify Postgres password from the UI. Still wrong.

This is the part where your brain invents eight theories, seven of which are wrong, and the eighth is:

Restore brought the old Postgres password with it

I had restored the old database into the new Postgres deployment.

That restore didn’t just bring tables.

It effectively meant the credentials I needed were the old ones-not the fresh password Coolify showed for the new deployment.

Once I pointed GoAuth (and later Pennywise) env vars at the restored reality:

  • Redeploy
  • Health check green
  • Server on :8090
  • Bruno: health OK, login OK, /me OK (after I fixed a typo in my own request headers and briefly gaslit the API)

I even signed up a temp user and confirmed it landed in the Contabo Postgres goauth schema. Part one: done.


Pennywise: the “this can’t be this easy” chapter

At this point I was emotionally prepared for another three-hour password archaeology dig.

Pennywise:

  • Env vars skimmed
  • Deploy started
  • Frontend + backend Docker build
  • New containers healthy
  • App loaded on the domain

Login failed.

Heart rate: Olympic.

Cause: I mistyped my password.

Second login: data there. Dashboard there. Transactions there. Credit cards, chit funds, insights, categories, groups, import/export, settings, profile-all intact.

New test user from GoAuth could log into Pennywise too.

Migration of the actual apps: done.

I still left the old Hetzner machine alive for a day or two as a “if I hallucinated success, reality is still parked next door” backup. Highly recommend not immediately deleting the previous universe.


The cleanup nobody finds glamorous (and where I messed up again)

Backups to R2 again

On the new Coolify:

  • Re-added R2 as S3-compatible destination (same endpoint/keys pattern)
  • Scheduled DB backups daily around 4:00 AM
  • Keep a handful of backups / ~30 days retention thinking

Without backups, a migration isn’t finished. It’s just a temporary feeling of competence.

Contabo firewall: create ≠ apply

Contabo’s UI feels older than Hetzner’s. Fine.

I created a firewall with:

  • Allow HTTP (80)
  • Allow HTTPS (443)
  • Allow SSH (22)
  • Drop everything else

Then I checked raw ports and discovered my APIs were still publicly reachable on things like 8090.

Why?

On Hetzner, firewalls felt glued to the machine.

On Contabo, you create a firewall, then separately assign it to the instance.

I had built a very nice shield and left it in the hallway.

After assigning it: direct port access blocked, HTTPS apps still worked, Coolify still worked. Mobile check: data still there.

Then, and only then: hurrah.


What actually changed (the scoreboard)

Thing Before After
Provider Hetzner (Singapore) Contabo (India)
Rough cost ~₹5,000 / ~$47 ~€10–15 / ~₹1.5–1.8k
Orchestration Coolify Coolify (again)
DB Shared Postgres, multi-DB Same idea, restored
Object backups Cloudflare R2 Cloudflare R2 (reconfigured)
Proxy preference Caddy Caddy
Latency feel Noticeable in UI/Coolify Noticeably snappier
Emotional state Mildly overpaying Mildly free

Expected savings: about ₹3,000/month.

For hobby projects, that is not “retire early.” That is “stop subsidizing the ocean.”


What I’d do differently next time (so future-me can ignore this)

  1. Temporary subdomains first if downtime / confusion matters more than speed.
  2. Set Coolify’s instance domain before declaring war on Cloudflare TLS errors.
  3. Treat Postgres restore as “credentials may change meaning”-verify password against the restored cluster, not the UI’s optimistic new secrets.
  4. Duplicate vault entries before editing anything during a cutover.
  5. On Contabo: assign the firewall. Creating it is just arts and crafts.
  6. Keep the old box warm for 24–48 hours. Ego can wait; rollback cannot.

Also: AI was genuinely useful for provider comparison and for debugging the Cloudflare/Caddy failure mode. It was less useful as a substitute for reading the one settings field that mattered.


Old Hetzner: still breathing for a couple days, just in case.

Me: going to save about three thousand rupees a month and pretend that was always the plan.

That’s very nice. That’s very nice.