DEV Community

Oğuzhan KARACABAY
Oğuzhan KARACABAY

Posted on

A practical checklist for migrating an Ubuntu or Debian VPS

Changing VPS providers sounds simple until you list everything that actually lives on the server.

The application files are usually the easy part. The real migration also includes database state, Docker volumes, reverse-proxy configuration, systemd units, firewall rules, SSH settings, scheduled jobs, certificates, and the small configuration files you forgot existed.

After moving my own and client servers several times, this is the checklist I now use.

1. Inventory before copying anything

Start by building a machine-readable inventory of the current host.

At minimum, record:

  • OS and architecture
  • mounted filesystems and free space
  • listening ports
  • enabled systemd services
  • Docker Compose projects and standalone containers
  • bind mounts and named volumes
  • MySQL, MariaDB, PostgreSQL, and Redis instances
  • Nginx, Apache, or Caddy virtual hosts
  • cron jobs and system timers
  • SSH configuration
  • firewall rules
  • certificate and renewal configuration

Do not assume every application is under /var/www or /opt. Compose projects often live in home directories, systemd services may point anywhere, and containers can mount paths that are easy to miss.

The inventory should become the migration plan. If a workload cannot be identified or exported consistently, treat that as a blocker instead of silently skipping it.

2. Separate configuration, persistent data, and runtime state

Each workload normally has three different kinds of data:

  1. Configuration: Compose files, environment files, virtual hosts, service units.
  2. Persistent data: uploads, database files, bind mounts, named volumes.
  3. Runtime state: running processes, sockets, caches, temporary files.

Configuration and persistent data need an explicit transfer strategy. Runtime state usually should not be copied.

For databases, copying raw files from a running server is rarely the safest default. Prefer engine-aware exports such as mysqldump or pg_dump, then validate the import on the target. For Redis, decide whether the current RDB/AOF state is sufficient or whether replication is required.

3. Prepare the target as a different machine

A migration is easier to debug when target preparation is a separate phase.

Before moving application data:

  • confirm the target OS and package repositories
  • install the required runtime and database versions
  • create users and groups with the expected IDs
  • create directories with correct ownership
  • check port conflicts
  • validate disk capacity
  • keep DNS pointed at the current server

Ubuntu and Debian are similar, but package names, default paths, service versions, and firewall tooling can differ. Treat cross-distribution moves as compatibility migrations, not file copies.

4. Transfer in repeatable phases

Use resumable transfers for large files. rsync is still a practical choice for bind mounts and application directories.

A useful sequence is:

  1. Transfer configuration and static files.
  2. Restore databases and volumes on the target.
  3. Start target services on private or temporary ports.
  4. Run application-level verification.
  5. Perform a final delta transfer if the workload allows it.
  6. Change traffic only after the target passes checks.

The exact consistency strategy depends on the workload. Some applications can tolerate an online first pass followed by a short cutover. Others need database replication or an application-specific export. A migration tool should report that distinction instead of pretending every workload can be copied the same way.

5. Verify behavior, not just files

A successful command exit code does not prove a successful migration.

I verify at least:

  • expected services and containers are running
  • HTTP health endpoints return the right response
  • database row counts match
  • important tables or datasets have matching checksums
  • mounted volumes contain expected files
  • reverse-proxy routes resolve correctly
  • environment and configuration values are present
  • firewall and SSH access behave as intended
  • logs contain no startup or connection errors

For Laravel-style applications, I also run an actual framework database connection check. A reachable database port is not enough if credentials, TLS settings, or the selected database are wrong.

6. Make rollback part of the plan

Before changing DNS or accepting writes on the target, define how to reverse the cutover.

A useful rollback plan states:

  • what traffic change will be reverted
  • which target services will be stopped
  • whether writes occurred on the target
  • how data created after cutover will be handled
  • which verification failure triggers rollback

Rollback should apply to target-side changes. The original server remains the fallback until the new host has been verified under real traffic.

7. Test the process, not only the script

Migration code can pass unit tests and still fail against a real distribution.

I use two layers:

  • Docker-based source-to-target matrices for fast coverage
  • real Linux VMs for systemd, firewall, package-manager, and networking behavior

The matrix includes Ubuntu and Debian combinations and checks HTTP responses, database counts and checksums, transferred configuration, and the state of the original host after the run.

Turning the checklist into HostShift

I eventually turned this workflow into HostShift, an Apache-2.0 Go CLI for discovering, planning, migrating, and verifying Ubuntu and Debian web servers.

It supports common Docker and web workloads, keeps an audit journal, and can resume interrupted runs. The core is deterministic and does not require AI; an optional Codex plugin provides a guided interface.

The project is still young, so real migration reports are more valuable than stars. If your setup includes a workload or edge case that this checklist misses, I would like to hear about it.

Documentation: hostshift.karacabay.com

Top comments (0)