Slot 42 Migration field notes

Cloudways to cPanel Migration: The Complete Manual Guide

Cloudways ships no cPanel-compatible backup, so this move is manual: rsync the app, mysqldump over SSH, rebuild configs and pull mailboxes out of Rackspace.

There is no automated path from Cloudways to cPanel. Cloudways is a managed stack, not a control panel in the cPanel sense, and it produces no cpmove archive and nothing WHM’s Transfer Tool can consume. You build every one of these by hand. The part that catches people is not the files or the database. It is the mail.

Below is the sequence I use. It assumes you have the Cloudways master credentials for the server and root or a reseller account on the destination cPanel box.

Why no tool does this for you

WHM restores accounts from archives produced by /scripts/pkgacct. That format carries the home directory, the MySQL dumps, the DNS zones, the Exim configuration, and the /var/cpanel/users/<user> metadata file all in one structure. Cloudways never generates any of that. Its own backups are server-level snapshots that only restore back into Cloudways.

So you extract three things separately: application files, databases, and mailboxes. The first two live on the Cloudways server. The third does not. That is the part most guides skip.

Step 1: get a clean copy of the application files

You have two options, and I use both on anything I care about.

The platform backup is the safety net. In the Cloudways console, under the application’s Backup and Restore tab, turn on Local Backups and take one manually. That writes an archive into the application’s own directory, so you can pull it with the same SSH credentials you use for everything else.

Then take the working copy over the wire. Find the application path first, because Cloudways does not use /home/<user>:

ssh master_abcdefghij@203.0.113.10
ls -la /home/master/applications/

Each application is a directory named with an opaque application ID, not your domain. Confirm which is which by checking the document root:

for d in /home/master/applications/*/; do
  echo "== $d"
  ls "$d/public_html" | head -5
done

The real document root is /home/master/applications/<app>/public_html. That is what you copy. Pull it directly to the destination cPanel server so the bytes only move once:

# run this ON the destination cPanel server
rsync -avz --numeric-ids --delete \
  -e "ssh -p 22" \
  master_abcdefghij@203.0.113.10:/home/master/applications/abcdefghij/public_html/ \
  /home/newuser/public_html/

Skip --numeric-ids if you like; the UIDs will not match anyway and you are going to fix ownership at the end. What matters is -a for permissions and timestamps, and running it twice, once before cutover and once during, so the second pass only carries the delta.

Step 2: dump the database

Cloudways does not expose MySQL publicly. The credentials are in the application’s Access Details panel in the console: database name, username, password. They only work from the server itself, so either dump on the source and copy the file down, or open a tunnel.

Dumping on the source is simpler. Put the credentials in an option file rather than in the command. A password given as -p'DBPASSWORD' is visible to every other user on the box in ps auxwwww and lands in your shell history on the way there.

Write that option file with mktemp, not to ~/.my.cnf. There may already be a ~/.my.cnf on that account, holding credentials the platform’s own scripts use, and cat > over it is not something you can undo:

umask 077
CNF=$(mktemp)
cat > "$CNF" <<'EOF'
[client]
user=abcdefghij
password=DBPASSWORD
EOF

mysqldump --defaults-extra-file="$CNF" \
  --single-transaction --quick --routines --triggers \
  abcdefghij > ~/app.sql
rm -f "$CNF"
gzip ~/app.sql

--defaults-extra-file has to be the first option on the line; MySQL’s client library parses the defaults options before anything else and ignores one that turns up later. A bare -p with no value also works and prompts you instead, which is fine for one command and tiresome inside a loop.

--single-transaction keeps InnoDB tables consistent without locking the site. If any table is still MyISAM, that flag will not protect it, so check before you trust the dump.

If you would rather pull it straight through, tunnel port 3306:

ssh -N -L 3307:127.0.0.1:3306 master_abcdefghij@203.0.113.10 &
mysqldump --single-transaction --quick \
  -h 127.0.0.1 -P 3307 abcdefghij | gzip > app.sql.gz

Step 3: build the cPanel account and land the files

Create the account first so the home directory, the UID, and the Apache vhost exist:

whmapi1 createacct \
  username=newuser \
  domain=example.com \
  plan=default \
  contactemail=admin@example.com

Then fix ownership on everything rsync just wrote. Skip this and you get 500 errors with nothing helpful in the log, because the PHP handler runs as the account user and cannot read files owned by root:

chown -R newuser:newuser /home/newuser/public_html

Ownership is the part that was actually wrong. Resist the urge to follow it with a blanket chmod across the tree. rsync -a brought the source’s modes over intact, and a full-mode rewrite is not a repair either way it points: chmod 755 on every directory reopens the 0700 ones somebody locked deliberately, and chmod 644 on every file republishes a 0660 config to the whole server. Fix the specific problem you found and normalise nothing.

The specific problem worth fixing after a copy is group or world writability, on directories every bit as much as on files. Look at it before you touch it:

find /home/newuser/public_html \( -type d -o -type f \) -perm /go=w \
  -printf '%m %p\n' | head -50

chmod go-w takes off exactly those bits and nothing else. Owner bits stay where they were, the executable bit survives on CGI and shell scripts, and a 0700 directory has no group or world write bit in the first place, so it never appears in the list and never gets touched:

find /home/newuser/public_html \( -type d -o -type f \) -perm /go=w \
  -exec chmod go-w {} +

Secrets are the one place a fixed mode is the right answer, because there is no legitimate reason for anyone but the owner to read them:

find /home/newuser -maxdepth 4 -type f \( -name '.env' -o -name 'wp-config.php' \) \
  -exec chmod 600 {} +

If the server runs SELinux or you have moved files between filesystems, restore contexts as well:

/scripts/restorecon_home newuser 2>/dev/null || restorecon -R /home/newuser

Step 4: import the database

Create the database and user through UAPI so cPanel’s own mapping tables stay correct. Creating them directly in MySQL leaves them invisible in the account’s interface:

uapi --user=newuser Mysql create_database name=newuser_wp
uapi --user=newuser Mysql create_user name=newuser_wp password='StrongPassHere'
uapi --user=newuser Mysql set_privileges_on_database \
  user=newuser_wp database=newuser_wp privileges='ALL PRIVILEGES'

Then load the dump:

zcat /root/app.sql.gz | mysql newuser_wp

Step 5: rewrite the application config

The database name, user, and password all changed. So did the absolute paths, because Cloudways used /home/master/applications/<app>/public_html and cPanel uses /home/newuser/public_html.

For WordPress:

cd /home/newuser/public_html
sudo -u newuser wp config set DB_NAME newuser_wp
sudo -u newuser wp config set DB_USER newuser_wp
sudo -u newuser wp config set DB_PASSWORD 'StrongPassHere'
sudo -u newuser wp config set DB_HOST localhost

Cloudways installs Redis and Varnish by default and many stacks there carry an object-cache drop-in and a Varnish purge plugin. Neither has anything to talk to on a stock cPanel box:

rm -f /home/newuser/public_html/wp-content/object-cache.php
sudo -u newuser wp plugin deactivate breeze varnish-http-purge 2>/dev/null

For Laravel or anything reading .env, edit /home/newuser/public_html/.env and then clear the compiled config, which otherwise keeps serving the old credentials:

sudo -u newuser php artisan config:clear
sudo -u newuser php artisan cache:clear

Finally, hunt for hardcoded paths that no longer resolve:

grep -rIl "/home/master/applications" /home/newuser/public_html | head -50

Cron entries are the usual offender. Cloudways cron jobs live in the platform console, not in a crontab you can copy, so open Application Settings → Cron Job Management and transcribe them by hand into crontab -u newuser -e. Rewrite both the PHP binary path and the script path while you are in there.

Step 6: the email problem

Cloudways has no built-in email hosting. None. Outgoing SMTP is an add-on relay, and actual mailboxes are sold as a paid Rackspace Email add-on. So the mail you are migrating is not on the Cloudways server at all. It is on Rackspace’s IMAP servers, and it will stay there after your DNS cutover unless you move it.

The sequence is: create the mailboxes in cPanel first, copy the mail while both sides are live, then flip MX.

Put every password this step needs into its own file first, one line each, owner-readable only. umask 077; mkdir -p /root/mailmig/secrets gets you the right modes without thinking about it, and the Rackspace side goes in info.src, the new cPanel side in info.dst.

uapi --user=newuser Email add_pop \
  email=info domain=example.com password="$(cat /root/mailmig/secrets/info.dst)" quota=2048

email takes the local part when domain is supplied, which is the pairing cPanel’s own example uses.

Be clear about what that $(cat ...) does and does not buy you, because it is easy to talk yourself into a safety you have not got. The shell expands the substitution before it executes uapi, so the password is in the process arguments regardless, and UAPI’s documented command line is parameter-value pairs only: there is no stdin or input-file mode to route around it. For the length of that one call the password sits in /proc/<pid>/cmdline, readable by anyone with a shell on the server. What the file genuinely gives you is a password that is not in your shell history, not in the terminal scrollback, and not in the script you are about to commit somewhere.

Close the window instead of denying it. Create the mailboxes while the destination is still yours alone, before a single customer has shell access on it, which on a server you built for this migration is the normal state anyway. If it is a live shared box with other people’s shells on it, assume the password was seen and rotate it at cutover rather than handing this one to the user. On CloudLinux the problem goes away properly, because CageFS gives every user a /proc view containing only their own processes. A stock cPanel box does not.

Then sync each mailbox from Rackspace into cPanel with imapsync. Rackspace’s IMAP endpoint is secure.emailsrvr.com on port 993:

imapsync \
  --host1 secure.emailsrvr.com --port1 993 --ssl1 \
  --user1 info@example.com --passfile1 /root/mailmig/secrets/info.src \
  --host2 mail.example.com --port2 993 --ssl2 \
  --user2 info@example.com --passfile2 /root/mailmig/secrets/info.dst \
  --automap

--passfile1 and --passfile2 read the password from the first line of a mode-600 file, which is what the imapsync manual asks for: --password1 puts the string into the process arguments, where ps auxwwww hands it to anyone with a shell on the box.

There is no --skipcrossduplicates here on purpose. It drops a message that has already been copied into some other folder, which is the right call coming out of Gmail, where one message with three labels shows up in three folders. Rackspace has no labels. A message sitting in two Rackspace folders is in two folders because someone put it there, and the flag would silently keep only one copy.

Run it once before cutover and once after, so anything delivered to Rackspace during the DNS propagation window still lands in the new mailbox. The full procedure, including the folder-mapping cases where --automap guesses wrong, is in the imapsync email migration runbook.

Get a real mailbox inventory from the Rackspace control panel before you start. Nobody remembers the three forwarding-only addresses until they stop working.

Step 7: DNS cutover

Lower the TTL on the records you are about to change at least 24 hours ahead. If the zone is sitting at 14400 seconds, dropping it to 300 on the morning of the migration does nothing, because resolvers still hold the old value for another four hours.

example.com.      300  IN  A     198.51.100.25
www.example.com.  300  IN  A     198.51.100.25
example.com.      300  IN  MX 0  mail.example.com.
mail.example.com. 300  IN  A     198.51.100.25

Change A records and MX in the same window if mail is moving with the site. Leave the TTL low for another day or two, then put it back.

Step 8: SSL

Do not try to move certificates. Once the A record resolves to the cPanel server, AutoSSL will issue and install on its own, and you can force the run instead of waiting for the nightly cycle:

/usr/local/cpanel/bin/autossl_check --user=newuser
tail -f /var/cpanel/logs/autossl/*.log

If AutoSSL fails, it is almost always because a subdomain in the account still points at Cloudways and the DCV check cannot complete. Fix the DNS or remove the subdomain from the account.

Verification checklist

Check Command or action
Site loads on the new IP curl -I --resolve example.com:443:198.51.100.25 https://example.com
Correct DB is in use sudo -u newuser wp db check
No leftover Cloudways paths grep -rIl "/home/master" /home/newuser/public_html
Ownership is clean find /home/newuser -not -user newuser | head
Mail sends Send from webmail, check headers for the new server
Mail receives Send in from an external address after MX flips
Mailbox counts match Compare imapsync’s final totals per folder
Cron jobs exist crontab -u newuser -l
SSL is valid openssl s_client -connect example.com:443 -servername example.com
Old server still up Keep Cloudways running 7 days before you destroy it

Keep the source alive until you have watched a full billing or business cycle run on the new box. I have never regretted paying for a week of overlap.

FAQ

Can I use WHM’s Transfer Tool against a Cloudways server?

No. The Transfer Tool needs root SSH on the source and expects cPanel to be installed there. Cloudways gives you a master user, not root, and there is no cPanel to talk to. Manual is the only route.

How do I find the application ID for my domain?

The Cloudways console shows it under the application’s Access Details, and it matches the directory name under /home/master/applications/. If you have SSH only, grep the vhost or check which public_html contains the site you recognise.

Will my Cloudways staging site come across?

Only if you copy it deliberately. Staging applications are separate applications with their own IDs and their own databases. Treat each one as its own migration or drop it.

Do I have to move email at all if the client is happy with Rackspace?

No, and sometimes that is the right call. Leave the MX records pointing at Rackspace and migrate only the site. Just be explicit about it, because the client will otherwise assume email moved with everything else and will keep paying two bills without knowing why.

What about the Cloudways SMTP add-on for outgoing mail?

Applications configured to send through an external relay will keep trying to reach it. Either update the SMTP plugin settings to use the cPanel server’s local mail, or point them at whatever relay the client keeps.

Where this fits

Cloudways to cPanel is a hand-built migration every time, and the mail leg is a second project inside the first. I do these end to end as server migration work, including the Rackspace mailbox sync and the DNS window. If you are heading in the other direction between managed platforms, the RunCloud and Cloudways email migration notes cover the same mail gap from the other side.