Context
A year earlier I had modernized this client's abandoned order-management backend — described in a separate case study, Legacy Rescue. The system was back in a state where it could be developed further, but physically it still ran on the same server it had for years. Migration was mentioned back then as a "further option" — outside the scope of the base contract.
After a few weeks of the modernized system running stably, the client came back to act on that option. A separate contract followed: moving the system to new infrastructure, HTTPS, backups, and a week of post-deployment supervision.
The problem: "it works" is not the same as "it's safe"
The server worked. To the user nothing hinted at a problem. Underneath, the picture was different:
- Operating system past end-of-life. Ubuntu 16.04 — the vendor stopped shipping security patches in 2024. For about two years the machine received no fixes at all.
- No HTTPS. Staff logged into the system while order data travelled between browser and server in plain text.
- Zero backups. A single disk failure meant irreversibly losing the entire database — over 18,000 historical orders.
- Manual updates — meaning, in practice, none.
- An expensive dedicated server — 240 PLN a month. Its price and specs had long outgrown the company's actual needs.
These are risks that don't hurt — until they hurt catastrophically. A missing backup bothers no one for years. It bothers you exactly once.
Approach
Migrating a system the company uses every day across three locations has one ironclad requirement: the next morning staff must find a working system — ideally without noticing anything changed. I built the plan on three principles:
- An identical environment on the new server — the same database, the same Node version, the same reverse proxy. Zero risk of anything behaving differently.
- Cutover on the weekend, in a window when no one is working.
- The old server kept as a safety net — ready for an instant rollback until the new one proves stable.
How the migration went
1. Identical environment, new machine
I stood up the new server on Hetzner Cloud running Ubuntu 24.04 — supported until 2029. I recreated the whole stack one-to-one: MySQL in a Docker container, the same Node version, Caddy as the reverse proxy. An identical environment isn't about convenience — it eliminates a whole class of risk. The migration was not an occasion to "while we're at it" bump library versions.
2. A 14-minute cutover
I lowered the DNS record's time-to-live (TTL) to 5 minutes several days ahead — so that at cutover the domain would switch to the new server almost instantly, rather than after hours of propagation. I ran the actual cutover on a Saturday evening: dump and import the database, repoint the domain, verify everything works. From 21:00 to 21:14. On Monday staff logged in at the same address, with the same passwords, to the same system.
3. HTTPS — and a trap I didn't see coming
Caddy with Let's Encrypt issues and renews the certificate automatically — the client gets the padlock in the browser and never has to think about renewals. On the first attempt, though, domain validation kept failing.
The cause: Let's Encrypt checks the domain from several locations around the world at once, and some of those requests — with DNS still propagating — were hitting the old server. The fix: I stopped the web server on the old machine. From then on every validation path led only to the new server. The certificate issued immediately.
4. The old server as a safety net
I didn't delete the old server right after cutover. I left it with its data-writing parts disabled — so an order couldn't accidentally be saved in two places at once and drift the databases apart — but ready to restore within minutes if something went wrong on the new server. It was only retired once the new infrastructure had proven stable for a full week.
A backup that's actually a backup
The contract required a backup in a second location. Here lies the trap that leaves many companies with a "backup" that protects against nothing:
A copy kept in the same data center as the server is not a copy in a second location. A fire, flood, or power failure across the whole facility then takes the original and the copy at the same time.
So the copies go to dedicated storage in a different country than the main server. Daily, automatically: database dump, compression, transfer over an encrypted channel. Rotation: 7 daily copies, 4 weekly and 12 monthly — so you can roll back both to yesterday and to the state from half a year ago.
And the thing most often skipped: you have to restore a backup to know it works. I ran a test restore onto a clean database — the record count matched to the row. A backup no one has ever restored isn't protection, just hope.
A week of supervision and a performance tune-up
The contract included a week of supervision after cutover. After a few days staff reported that the orders-list tab was loading slowly. Diagnosis: with over 18,000 records, the interface fetched the entire order history on every visit.
Two changes: capping how many orders are fetched by default, and compressing server responses. The list that used to take several seconds began opening in about half a second. All within the contract price, as part of the supervision — because that's what supervision is for.
Handing over ownership
At cutover I registered the new server account temporarily in my own name, so as not to block work before the weekend. After a week I transferred it to the client's company: invoices now go to the company (VAT reclaimable), and full ownership of the infrastructure sits on their side. I kept technical access for the supervision period. The client should own their infrastructure — not be hostage to the contractor.
Stack
- Server: Hetzner Cloud, Ubuntu 24.04 LTS
- Application: Node.js + MySQL in a Docker container, managed by PM2
- Reverse proxy / TLS: Caddy 2 + Let's Encrypt (auto-HTTPS)
- Backups: rclone → off-site storage in another country, cron, 7 / 4 / 12 rotation
Results
- 14-minute cutover — the next day staff worked with no change to their habits.
- An operating system supported until 2029 with automatic security updates.
- HTTPS with a self-renewing certificate — previously the connection was unencrypted.
- A daily backup in another country, with a verified restore — previously there was none.
- Orders list: from several seconds to about half a second.
- Running cost dropped from 240 PLN to ~39 PLN a month — about 200 PLN saved every month, close to 2,400 PLN a year.
- Infrastructure ownership and invoices — on the client's company side.
Challenges and lessons
1. Lower the DNS TTL ahead of time, not on cutover day
Had the TTL been left at its default, repointing the domain would have stretched over hours. Lowering it several days in advance is what let the cutover fit into a dozen-odd minutes.
2. Let's Encrypt validates from multiple locations at once
As long as the old server answered web traffic, some validations landed on it and the certificate wouldn't issue. Lesson: during a migration, stop the web server on the old machine before the new one fetches its certificate.
3. A provider "backup" ≠ a second location
Snapshots offered within the same data center are convenient, but they don't protect against the failure of the whole facility. A "second location" requirement is only met by a copy physically outside it — ideally in another country.
4. A backup without a restore test is not a backup
A copy that has never been restored is an assumption, not a safeguard. The first test restore is part of the deployment, not an option.
5. An identical stack = a cutover without drama
Migration and stack modernization are two different operations. Merging them into one multiplies the risk. First a safe move onto an identical environment — version upgrades are a separate, deliberate decision for later.
What this case study says about how I work
- Risk that doesn't hurt is still risk. An end-of-life OS, no HTTPS, no backup — "it works" lulls you, and the cost of inaction shows up all at once, at the worst possible moment.
- A successful production migration is recognized by the absence of drama. The best cutover is the one users never noticed.
- A safety net before the leap. The old server stays as a way back until the new one proves it's stable.
- A backup exists only once you've restored it. The rest is hope wired to a cron job.
- The client owns their infrastructure. The account, invoices, and full control belong to the company — not the contractor.
In line with a confidentiality agreement, this case study omits: the company name, its location, personal data of employees and customers, and infrastructure addresses and credentials. It focuses solely on the process, the technical decisions, and the qualitative results.