Getting Started

Move a Linux server from another provider to the dataforest cloud

Server migration from another provider step by step. Set the services up on a dataforest seed, bring files across with rsync and databases as a dump, test under the real domain and switch the DNS records with only a short downtime.

AuthorLaurenz Seipel
PublishedSeptember 24, 2026
min read~12 min
Words2.100
Difficulty Intermediate
StackMigration · rsync · DNS · Networking

When you move from your previous provider, you set the services up freshly on a seed, bring your data across from the old server, test everything under your real domain and switch the DNS records last. Until then the old server keeps running unchanged and remains your fallback. Whether it runs with another cloud provider, as a dedicated server or on your own hardware makes no difference.

How long the move takes depends mostly on how much data you have. Your application only has to go offline for the final sync. In our test move of a PHP application with MariaDB it took less than a second; with larger databases it takes a few minutes.

Before you start. You need root access to both servers and should be comfortable on the command line. You are working with root privileges on your own systems, and carrying this out is your responsibility. Check each command before you run it, and make sure you have a backup beforehand.

The move comes down to five steps:

  1. Take stock and create a suitable seed
  2. Install the services on it
  3. Transfer files and databases
  4. Test under your real domain
  5. Switch the DNS records

We recommend this route because you end up with a clean, current system. If your server has grown over years and can hardly be rebuilt, you can also move it without reinstalling, including operating system and configuration. The guide Move a Linux server from another provider without reinstalling describes how. This route takes more effort and carries more risk.

Preparation: understand the old server

Before you create anything, get an overview. These commands run on the old server and answer the questions that matter later.

bash
systemctl list-units --type=service --state=running   # What is running?
ss -tulpn                                             # On which ports?
df -h /                                               # How much is in use?
du -sh /var/www /home /srv /opt /var/lib/mysql 2>/dev/null   # Where does the data sit?
ls /etc/cron.d/ /var/spool/cron/crontabs/ 2>/dev/null # What runs on a schedule?
systemctl list-timers --no-pager                      # And what through systemd?

If you have further volumes mounted, df -h without a path lists them all. Their contents have to move as well.

If you already know what runs on your server, that is enough. If you are unsure which packages sit behind the services, this loop maps one to the other. It works on Debian and Ubuntu as well as on AlmaLinux, RockyLinux or CentOS:

bash
for s in $(systemctl list-units --type=service --state=running --no-legend --no-pager | awk '{print $1}'); do
  f=$(systemctl show -p FragmentPath --value "$s")
  p=$(dpkg -S "$f" 2>/dev/null | cut -d: -f1)
  [ -z "$p" ] && p=$(rpm -qf --qf '%{NAME}' "$f" 2>/dev/null)
  [ -n "$p" ] && printf "%-26s %s\n" "$s" "$p"
done

Entries such as systemd, udev, dbus, cron or openssh-server belong to the base system and can be skipped. What remains is the list of what makes up your server. Note the version numbers too, for PHP or the database for example. If they differ on the seed, you will have to adjust paths in configuration files.

Create a suitable seed

Operating system. You can choose from Debian, Ubuntu LTS, AlmaLinux and RockyLinux. Take the current version of the distribution you are used to.

Size. Go by the used storage of your old server, not its disk size, and add headroom. You can upgrade the plan later without losing data. The seed restarts once for that, and the disk cannot be shrunk afterwards.

IPv4 address. Every seed comes with a free IPv6 subnet, an IPv4 address is an add-on. For a publicly reachable application you will almost always need one, since a large share of your visitors still has no IPv6. Add it while creating the seed, and it gets configured in the operating system automatically. An address added later has to be configured by hand, even after a restart.

Firewall. Create a firewall under Network › Firewalls and assign it to the seed right away, before your data lands there. The panel suggests SSH and ping inbound. Add the ports from your inventory, for a web server that means 80 and 443. Databases are not among them, they should stay unreachable from outside. Leave the outbound rules for all ports that the panel also creates in place. A firewall only lets through what a rule explicitly allows. Without outbound rules the seed reaches neither your old server nor package mirrors or mail servers.

On AlmaLinux and RockyLinux, firewalld also runs in the operating system and only lets SSH through. Open the services locally there as well:

bash
firewall-cmd --permanent --add-service=http --add-service=https
firewall-cmd --reload

Access and backups. Add an SSH key and enable automatic backups.

The user is root on all of our images:

bash
ssh root@<SEED-IP>

Connect the two servers

You start every transfer on the seed and pull the data from the old server. That keeps all commands in one place, and no outside access is left behind on your new server.

Generate a key for the move on the seed:

bash
ssh-keygen -t ed25519 -C "migration" -f /root/.ssh/migration -N ""

If the old server allows password logins, install the key with ssh-copy-id -i /root/.ssh/migration.pub root@<OLD-SERVER-IP>. Many servers only let root in with a key, though. In that case print the key on the seed with cat /root/.ssh/migration.pub and append the line on the old server:

bash
echo 'ssh-ed25519 AAAA… migration' >> /root/.ssh/authorized_keys

Check the connection from the seed:

bash
ssh -i /root/.ssh/migration root@<OLD-SERVER-IP> hostname

If there is no answer, a firewall in front of the old server or a protection such as fail2ban is usually blocking the connection. Allow SSH there for your seed's IP address.

Finally, both sides need rsync. Our Ubuntu, AlmaLinux and RockyLinux images ship it, on Debian you install it. It is often missing on the old server as well.

bash
apt install -y rsync    # Debian, Ubuntu
dnf install -y rsync    # AlmaLinux, RockyLinux, CentOS

Set up the services and bring the data across

You install the services from your inventory on the seed and then fetch only what is really yours. That is user data, database contents, certificates and your own configuration files.

Users first. If your application runs under its own user, create it on the seed with the same name before you transfer any files:

bash
useradd -m -s /bin/bash <USER>

rsync then maps files correctly by name, even if the user gets a different number (UID) on the seed. Without the user, the files end up owned by a bare number.

Files come across with rsync. The command runs on the seed:

bash
rsync -aAX --info=progress2 -e "ssh -i /root/.ssh/migration" \
  root@<OLD-SERVER-IP>:/path/to/the/data/ /path/to/the/data/

Mind the trailing slashes on both paths. They make sure the contents are transferred rather than the directory being copied into itself. You can repeat the command as often as you like. On the second run it only transfers what has changed, which is exactly what you use later during the switch.

Certificates. If you use Let's Encrypt, take /etc/letsencrypt as a whole, including the links inside it:

bash
rsync -aAX -e "ssh -i /root/.ssh/migration" \
  root@<OLD-SERVER-IP>:/etc/letsencrypt/ /etc/letsencrypt/

Your web server configuration points to these files, and nginx does not start without them. With the certificates in place, HTTPS works on the seed during the test and right after the switch. Install the certbot package on the seed like any other service, renewal then continues there.

Databases are never copied as files. While the service runs it writes constantly, and a file copy would be internally inconsistent. Create a dump instead, for MariaDB and MySQL like this:

bash
ssh -i /root/.ssh/migration root@<OLD-SERVER-IP> \
  "mysqldump --single-transaction --routines --triggers --events --databases <DATABASE>" \
  > /root/dump.sql

Two things matter here, whichever database system you run. The dump has to reflect one consistent state without blocking the running application. That is what --single-transaction does above. And it has to be complete. The three remaining options include stored procedures, triggers and scheduled events, which would otherwise be missing without a word. Gaps like that surface weeks later, because the dump runs through without an error.

Before importing, check that the dump was written completely. With mysqldump the last line reads -- Dump completed on …. You then import it on the seed with mysql < /root/dump.sql.

A dump contains the data but not the user accounts. Without them your application cannot log in, even though every table is there. The simplest way is to take the account with its privileges and password straight from the old server:

bash
ssh -i /root/.ssh/migration root@<OLD-SERVER-IP> \
  "mysql -N -e \"SHOW CREATE USER '<DB-USER>'@'localhost'; SHOW GRANTS FOR '<DB-USER>'@'localhost';\"" \
  | sed 's/$/;/' > /root/users.sql
mysql < /root/users.sql

The password only travels as a hash, and your application's configuration stays valid as it is.

MySQL and MariaDB are not interchangeable in every direction. Debian ships MariaDB, many older servers run MySQL 8. The MariaDB in Debian 13 imports a MySQL 8 dump without changes. The older MariaDB in Debian 12 aborts with Unknown collation: 'utf8mb4_0900_ai_ci'. There you replace the collation before importing:

bash
sed -i 's/utf8mb4_0900_ai_ci/utf8mb4_unicode_ci/g' /root/dump.sql

User accounts from MySQL 8 use the caching_sha2_password method by default, which MariaDB does not know. In that case create the account anew with the password from your application's configuration.

Configuration files are fetched one by one, never as a whole directory. Copying all of /etc or /etc/nginx overwrites the seed's base settings. Take individual files instead: your site configurations, PHP pools and your own systemd units. Then check the paths inside them. If the PHP version differs, for example, references to its socket point nowhere. The old IP address likes to hide in configurations too. This search on the seed finds both:

bash
grep -rn -e "<OLD-SERVER-IP>" -e "php<OLD-VERSION>" /etc /var/www /srv 2>/dev/null

Cron jobs stay behind for now. Otherwise they would run on both servers at once, send mails twice or process data twice. They come across during the switch.

Test before the domain moves

Here you check the seed under your real domain without touching the DNS records. On your own computer, add a line to /etc/hosts (on Windows C:\Windows\System32\drivers\etc\hosts):

text
<SEED-IP>    your-domain.com www.your-domain.com

Only your computer now sees the new server, everyone else still sees the old one. Because the certificates are already on the seed, HTTPS works too. Go through everything your application has to do: log in, save, upload, submit forms. A look at the seed's logs belongs here as well. Remove the line again when you are done.

As long as anything is off here, leave the domain alone.

Switch over

Lower the TTL of your DNS records to 300 seconds one to two days beforehand. The TTL tells other servers how long they may cache an answer. A low value makes the change reach everyone quickly. Wait out the old TTL: if it was set to a day, the reduction only takes effect after a day.

If your application sends mail itself, add the seed's IP address to your domain's SPF record during the same period. Otherwise mail from the new server lands in spam or gets rejected. Remove the old address only after the move.

On the day of the move, go in this order:

  1. Stop the web server, application services and cron on the old server so no new data comes in. The database keeps running, it is needed for the final dump.
  2. Final file sync, the same rsync command as before, now with --delete so deleted files disappear too
  3. Final database dump and import on the seed. Databases are always transferred in full, there is no incremental route.
  4. Set up the cron jobs on the seed
  5. Switch DNS: point the A record at the seed's IPv4 address and an existing AAAA record at its IPv6 address. If the old AAAA record stays, visitors with IPv6 keep landing on the old server. Think of subdomains and any other records pointing at the old address as well.
  6. Check with dig +short your-domain.com A and dig +short your-domain.com AAAA and in the browser

Afterwards raise the TTL again.

If you need to go back

As long as the old server is still there unchanged, the way back is short: you set the DNS records back. Anything created on the seed since the switch is then missing on the old server, though. Bring it back first in the opposite direction, with the web server on the seed stopped:

bash
mysqldump --single-transaction --routines --triggers --events --databases <DATABASE> \
  | ssh -i /root/.ssh/migration root@<OLD-SERVER-IP> mysql
rsync -aAX -e "ssh -i /root/.ssh/migration" /path/to/the/data/ root@<OLD-SERVER-IP>:/path/to/the/data/

Then start the services on the old server again.

Follow-up

Certificates. Once the domain points at the seed, check with certbot renew --dry-run that renewal works there.

Backups. Check that the first backup has run.

Keep the old server. Let it run for two to four weeks before you cancel it. Experience shows that a missing detail often only turns up after a few days. Then remove the migration key from /root/.ssh/authorized_keys on the old server and the key files /root/.ssh/migration* on the seed.

Before you cancel. If your domain or its DNS management is part of the previous contract, it ends with the cancellation. Transfer the domain to a registrar of your choice beforehand or detach it from the contract. The same applies to mailboxes held with the previous provider.

Special cases in brief

Other databases than MariaDB and MySQL follow the same pattern: create a dump, transfer it, import it, recreate the user accounts. Which tools your system provides is described in its documentation, for PostgreSQL in the chapter Backup and Restore.

Docker and Docker Compose: Transfer the compose.yml, the .env and the directories behind your volumes. The images do not move, the seed pulls them fresh on first start. Treat databases in containers as above, with a dump rather than a file copy.

Mail servers need a matching PTR record beyond the move, which you set in the panel with the seed's IP addresses. They also have to rebuild their reputation with recipients. Plan more time and switch the MX records only once sending demonstrably works. Transfer mailboxes over IMAP, for example with imapsync, and compare the number of mails per folder afterwards.

Control panels such as Plesk or cPanel bring their own migration tools. Use them instead of copying files past the panel.

Servers grown over the years, whose setup is hard to retrace, can also be moved without reinstalling, including operating system and configuration. That route is covered in Move a Linux server from another provider without reinstalling. Cloning the raw disk, with dd for example, does not get you there. A clone like that will as a rule not boot on a seed.

Windows Server cannot be selected as an image with us. You install it yourself from an ISO, with your own licence. The guide Install Windows Server yourself from an ISO describes how.

Ready to get started?

Create your first Seed and start deploying in minutes.

Back to overview