Migration changes a system, not just a host#

A production server rarely operates independently. Even a simple application can depend on DNS, SMTP, a database, an external API, certificates, storage, schedules, monitoring and network rules. Copying /opt/app does not recreate those dependencies.

Before starting, answer three questions:

  1. What exactly needs to move?
  2. How will we prove the new instance works correctly?
  3. How will we return if the criteria are not met?

Without an answer to the third question, you have a deployment plan rather than a migration plan.

Inventory: the actual state rather than historical documentation#

Documentation can be outdated, so an inventory should combine declared information with observation of the system. Collect:

  • the operating system, kernel version and architecture,
  • systemd units, containers and manually started processes,
  • listening ports and outbound connections,
  • firewall rules, routing, NAT and additional provider ACLs,
  • volumes, mounts, data directories and permissions,
  • databases, users, extensions and the backup schedule,
  • cron jobs and systemd timers,
  • Nginx/Apache configuration, domains and certificates,
  • secrets, .env files and their rotation process,
  • integrations: SMTP, webhooks, APIs, monitoring and DNS.

Example inspection commands:

bash
systemctl list-units --type=service --state=running
ss -lntup
findmnt
crontab -l
systemctl list-timers --all
docker compose ls
docker ps --format '{{.Names}}\t{{.Image}}\t{{.Ports}}'

Do not copy results without interpreting them. A process may be left over from an old version, and an open port may be unused. Identify an owner, purpose and criticality for every item.

Define acceptance criteria before building the new host#

“The website opens” is an insufficient criterion. A good list includes user journeys and technical operations:

AreaExample criterion
HTTPevery domain returns the correct status and canonical redirect
TLSvalid chain, hostname and automatic renewal
Applicationlogin, forms, uploads or transactions work end to end
Datarecord counts and checkpoints match
IntegrationsSMTP, payments, webhooks and APIs pass a test
Operationshealth checks, backups, monitoring and logrotate are active
Performanceresponse time and resource use have not deteriorated beyond the threshold

The criteria should be executable before and after cutover. This lets you compare the old and new systems using the same test.

Build the new server as a reproducible environment#

The new host should not be a manually reconstructed copy of the old one. Organise its installation around:

  • a current, supported operating system,
  • a minimal set of packages,
  • a separate deployment user,
  • SSH keys and restricted access,
  • a firewall configured before exposing services,
  • Docker Compose or clearly defined systemd units,
  • Nginx as a reverse proxy,
  • automated certificates,
  • logging and rotation,
  • liveness, readiness and TLS monitoring.

For a containerised application, build the image from versioned source and store data outside the container layer. Do not treat docker commit as a release process.

Plan data migration around consistency requirements#

Static data can usually be synchronised in advance. A transactional database needs a controlled cut-off point. Choose a strategy:

  • complete write freeze — stop the application, take the final backup, restore and start,
  • replication — synchronise in the background and switch once the replica catches up,
  • dual write — less common and riskier without application preparation,
  • logical export/import — useful for version changes or selective migration,
  • block snapshot — quick, but requires an assessment of application consistency.

For each method, define the maximum acceptable data loss (RPO) and downtime (RTO). If a contact form accepts data while the database is copied, you must know where messages submitted between the initial and final synchronisations will go.

Perform a trial restore before cutover. A backup that has never been restored is only an assumption.

DNS, certificates and incoming traffic#

Lower record TTLs a few days before the change, while recognising that caches will not disappear everywhere immediately. The new server should serve the domain before the public switch. Tests can use a local hosts entry or a request with an overridden Host header and the correct address.

A certificate can be issued using HTTP-01 or DNS-01, or existing certificate material can be transferred according to policy. Prepare automatic renewal on the new host and run certbot renew --dry-run.

During propagation, both servers may receive traffic. If the application accepts writes, provide shared data or clearly define the point after which the old host stops accepting writes.

Cutover should be a short, documented sequence#

Example runbook:

  1. Announce the start of the window and freeze configuration changes.
  2. Enable maintenance mode or stop writes.
  3. Perform the final data synchronisation.
  4. Confirm checksums, schema version and record counts.
  5. Start the application on the new server.
  6. Run a local smoke test against the new host's address.
  7. Switch DNS, the floating IP, load balancer or routing.
  8. Run a public smoke test.
  9. Observe logs, metrics, queues and integrations.
  10. End maintenance only after the criteria are met.

Every step needs an owner, expected outcome and time limit. If a step exceeds that limit, follow the predefined decision: continue, pause or roll back.

Rollback also includes the data#

The simplest traffic rollback restores the previous DNS record or address. That is insufficient if the new system has accepted writes. Decide whether:

  • new data will be transferred back,
  • the write window will close until a decision is made,
  • data loss is accepted and documented,
  • the old system can read a schema changed by the new version.

Combining a migration with a database or application upgrade complicates rollback. Separating the host change from the version change reduces the number of variables. If possible, run the same version on the new infrastructure first, then upgrade after stabilisation.

Stabilisation after migration#

Do not delete the old host immediately after success. For an agreed period, monitor:

  • HTTP and application errors,
  • latency and CPU, RAM and disk use,
  • database, replication and queue state,
  • backup success,
  • forms, email and webhooks,
  • certificate expiry,
  • unexpected traffic still going to the old address.

Also perform a controlled restart of the new host. A system that only works until its first reboot is not production-ready. Check service ordering, automatic container startup, mounts and unlocking of secrets.

Completion checklist#

  • All acceptance criteria passed after the public cutover.
  • External monitoring sees the new system.
  • A post-migration backup has been taken and verified.
  • Automatic certificate renewal is active.
  • Logs show no old addresses, integration errors or growing queues.
  • Documentation, diagrams and access details have been updated.
  • The old host is disconnected from traffic but retained according to the rollback plan.
  • The final removal date for the old infrastructure is recorded.

A safe migration is boring in the best sense: every step is expected, outcomes are measurable, and the decision to return is prepared before pressure builds.