Virtualmin is the panel built on top of Webmin that small hosts and self-hosters reach for when they want something free, open source, and Linux-native. The Pro edition adds reseller support, scripted installs, and a paid update channel — Virtualmin GPL is the version you see most often on $5 VPSes and on shops that are one or two engineers deep. It works, and the migrations we see off it are rarely about a panel defect. They're about a hosting business that has outgrown a single-server, single-operator setup and needs CageFS-level account isolation, JetBackup, LiteSpeed Enterprise, WHM resellers, Imunify360, or a brand its customers already recognise.
This guide is the account-by-account playbook for moving an existing Virtualmin server to cPanel & WHM. It assumes a working cPanel destination — if you don't have one yet, start with installing cPanel on a VPS. Budget roughly 25-40 minutes of operator time per virtual server plus 15-30 minutes of customer-visible downtime per DNS cutover. A 25-account Virtualmin box is a comfortable day of work spread across two evenings.
What doesn't translate one-to-one
Virtualmin and cPanel share more DNA than HestiaCP does — both run Apache, Postfix or Sendmail, MySQL/MariaDB, BIND, and ProFTPD or Pure-FTPd. But the defaults and on-disk layouts diverge in ways that bite during restore:
| Virtualmin | cPanel equivalent |
|---|---|
| Apache (or NGINX via the optional plugin) | Apache, or LiteSpeed Enterprise as a paid add-on |
| Postfix + Dovecot + Usermin webmail | Exim + Dovecot + Roundcube/Webmail Lite |
| BIND as authoritative DNS | BIND, PowerDNS, or DNS-only cluster |
| Per-domain PHP via FPM pools | EasyApache 4 PHP versions per account |
Sites under /home/<user>/public_html/ | Sites under /home/<user>/public_html/ |
Mail in /home/<user>/Maildir/ (master) and /home/<user>/homes/<acct>/Maildir/ (sub-accounts) | Mail in /home/<user>/mail/<domain>/<acct>/ |
virtualmin backup-domain .tar.gz archives | pkgacct tarballs |
| GPL: no resellers. Pro: server-owner roles | WHM reseller accounts and ACLs |
| Webmin firewall (iptables) + fail2ban | cPHulk, ConfigServer Firewall, or Imunify360 |
Two structural deltas matter most. First, Virtualmin's Maildir layout is not cPanel's —
sub-accounts on Virtualmin live under /home/<user>/homes/<sub>/Maildir/, while cPanel
expects every mailbox under /home/<user>/mail/<domain>/<localpart>/. Maildir's on-disk
format is identical, so the messages move cleanly, but the paths must be rewritten.
Second, Virtualmin uses Postfix and cPanel uses Exim. Mail queues do not transfer —
flush the source queue before cutover or any pending outbound messages disappear.
Step 1 — Inventory the source
SSH into the Virtualmin server as root and pull a complete picture before touching
anything. Virtualmin ships a comprehensive CLI under virtualmin (and the legacy
/usr/sbin/virtualmin shim) that gives you machine-readable output for every entity:
# All virtual servers (top-level and sub-servers)
virtualmin list-domains --name-only
# Mail users per virtual server
for d in $(virtualmin list-domains --name-only); do
echo "=== $d ==="
virtualmin list-users --domain $d --multiline | grep -E '^(Username|Email address):'
done
# Databases owned by each virtual server
for d in $(virtualmin list-domains --name-only); do
virtualmin list-databases --domain $d --multiline
done
# DNS records served from this box
for d in $(virtualmin list-domains --name-only); do
virtualmin list-dns-records --domain $d --multiline 2>/dev/null | head -40
done
# PHP version per virtual server
for d in $(virtualmin list-domains --name-only); do
printf "%-40s %s\n" "$d" "$(virtualmin get-php-version --domain $d 2>/dev/null)"
done
Capture all of that into a single migration manifest spreadsheet. The columns you want are
Virtualmin domain, parent server (for sub-servers), Linux username, PHP version, database
names, mail accounts, DNS-only or full-DNS, and a notes column for oddities. The oddities
matter — a virtual server with no public_html is usually a customer who provisioned a
domain and never built the site, and a database with no matching domain points at deleted
content that's still consuming backup space.
Step 2 — Prepare the cPanel destination
On the destination cPanel server:
-
Match the source's PHP versions through EasyApache 4. Run
virtualmin list-available-php-versionson the source to see what's installed, then install the matching versions following adding a PHP version to EasyApache 4. A WordPress site on PHP 7.4 will throw deprecated-function warnings on PHP 8.2, and Magento 2 sites are version-pinned hard. -
Match MariaDB / MySQL. Virtualmin defaults to MariaDB on Debian/Ubuntu and MySQL on RHEL-family. Run
mysql --versionon the source, then match it on the cPanel destination — this avoids the JSON-column andcaching_sha2_passwordauthentication edge cases that bite cross-engine restores. -
Pick the web server now, not later. Virtualmin's optional NGINX plugin caches at the proxy layer, and customers who relied on it will see slower pages on plain cPanel Apache. The cleanest cPanel path is LiteSpeed Enterprise with LSCache for WordPress installed before the first account move.
-
Decide your DNS topology. If the Virtualmin server was authoritative via BIND, you can either keep its IP serving DNS only post-migration or rebuild the zones on the new cPanel BIND. Most operators move DNS to a DNS-only cluster on the same migration weekend to avoid having two authoritative answers in flight.
-
Pre-create packages. Build the WHM hosting packages (disk, bandwidth, addon-domain count) before the first account creation. If the source is Virtualmin Pro and you used resellers, recreate the reseller accounts in WHM with matching package allotments first.
Step 3 — Package each virtual server
Virtualmin's virtualmin backup-domain creates a tarball that contains the user's web
root, mail Maildirs, databases, DNS zone, cron entries, and per-domain configs. The
tarball is not in pkgacct format, so cPanel's transfer wizard will not ingest it, but
the contents extract cleanly and the pieces map directly onto cPanel paths.
On the source, package one virtual server at a time. Including sub-servers under the parent keeps related mail and addon domains together:
DOMAIN=example.com
virtualmin backup-domain \
--domain $DOMAIN \
--include-sub \
--all-features \
--dest /backup/${DOMAIN}.tar.gz \
--newformat
ls -lh /backup/${DOMAIN}.tar.gz
The --newformat flag is important — it produces the multi-file tarball that contains a
clean per-feature breakdown rather than the legacy single-blob format that's harder to
unpack selectively. The archive layout looks like this:
example.com.tar.gz
├── example.com.dir # full /home/<user>/ tree
├── example.com.dom # virtual server metadata
├── example.com.mysql # mysqldump of every DB owned by the domain
├── example.com.dns # BIND zone file
├── example.com.spam # SpamAssassin config (if installed)
└── example.com.users # mail user list with hashed passwords
Move the tarball to the destination via rsync rather than scp — rsync is resumable
on a flaky network, and a mail-heavy domain's backup can run into tens of gigabytes:
rsync -P /backup/${DOMAIN}.tar.gz destination:/home/
Step 4 — Restore each virtual server as a cPanel account
On the destination cPanel server, create the new account in WHM under Account Functions → Create a New Account. Pick the cPanel username carefully — Virtualmin allows usernames up to 32 characters, but cPanel caps at 16, so plan a shortened mapping in your migration spreadsheet and notify customers if their FTP username will change.
With the account created, restore the pieces:
CUSER=customer1 # cPanel username
DOMAIN=example.com # primary domain on cPanel
TARBALL=/home/${DOMAIN}.tar.gz
mkdir -p /tmp/vm-restore-$CUSER
tar -xzf $TARBALL -C /tmp/vm-restore-$CUSER
# Find the home directory the backup originally lived under
VM_HOME=$(find /tmp/vm-restore-$CUSER -maxdepth 3 -name public_html -type d | head -1 | xargs dirname)
# Web files — copy public_html into the cPanel account
rsync -av $VM_HOME/public_html/ /home/$CUSER/public_html/
chown -R $CUSER:$CUSER /home/$CUSER/public_html
# Databases — Virtualmin uses domain-derived names, cPanel enforces user prefix
for sql in /tmp/vm-restore-$CUSER/${DOMAIN}.mysql/*.sql; do
oldname=$(basename $sql .sql)
newname="${CUSER}_$(echo $oldname | sed 's/[^a-zA-Z0-9_]/_/g' | cut -c1-$((63-${#CUSER}-1)))"
mysql -e "CREATE DATABASE \`$newname\`"
mysql $newname < $sql
done
# Mail — Virtualmin's nested Maildir layout flattened into cPanel's per-domain structure
mkdir -p /home/$CUSER/mail/$DOMAIN
# Master domain mailbox
[ -d $VM_HOME/Maildir ] && rsync -av $VM_HOME/Maildir/ /home/$CUSER/mail/$DOMAIN/$(echo $CUSER)/
# Sub-account mailboxes under /home/<vm_user>/homes/
if [ -d $VM_HOME/homes ]; then
for sub in $VM_HOME/homes/*/; do
localpart=$(basename $sub)
[ -d $sub/Maildir ] && rsync -av $sub/Maildir/ /home/$CUSER/mail/$DOMAIN/$localpart/
done
fi
chown -R $CUSER:$CUSER /home/$CUSER/mail
The database rename matters: cPanel enforces a <cpaneluser>_<dbname> prefix and rejects
operations on databases that don't match it. Restoring under the old Virtualmin name leaves
the database visible only over SSH, never in cPanel's MySQL UI — which means the customer
cannot change passwords or grant users from the panel.
After the file restore, update any WordPress wp-config.php, Joomla configuration.php,
or Drupal settings.php to use the new database name and the new MySQL password cPanel
generated for the database user:
sed -i "s/'DB_NAME', '[^']*'/'DB_NAME', '${CUSER}_wordpress'/" \
/home/$CUSER/public_html/wp-config.php
sed -i "s/'DB_USER', '[^']*'/'DB_USER', '${CUSER}_wpuser'/" \
/home/$CUSER/public_html/wp-config.php
sed -i "s/'DB_PASSWORD', '[^']*'/'DB_PASSWORD', 'newpass-here'/" \
/home/$CUSER/public_html/wp-config.php
This is the step people skip and then waste an hour debugging "error establishing a database connection" on a freshly migrated site.
Step 5 — Recreate mail accounts and DKIM
The Maildir data is in place, but cPanel needs each mailbox to exist in its own account
database before IMAP will serve it. The example.com.users file inside the backup tarball
is a plain-text list of local-part:hashed-password:quota:... rows — useful as a
checklist, but the hashes themselves use Postfix's Dovecot-passwd format and cPanel's
account-creation flow won't ingest them.
For each mail account in the manifest:
- Create the account in cPanel → Email Accounts with the same local part.
- Either set the same password the customer was using (if you captured it during a pre-migration prompt) or force a reset and notify them.
- Re-publish DKIM. Virtualmin generates a per-domain DKIM key under
/etc/dkim/<domain>/(or/etc/opendkim/keys/<domain>/depending on the version) that is not portable to cPanel's key store. Generate fresh DKIM in cPanel → Email Deliverability for each domain and follow cPanel email deliverability with SPF, DKIM, and DMARC to get the records right at cutover.
Reusing the old DKIM selector tanks deliverability for a week while inbox providers learn the new key. Publish the new selector ahead of the DNS cutover so the records are warm when mail starts flowing.
Step 6 — DNS cutover
Drop public DNS TTLs to 300 seconds at least 24 hours before the cutover. On the day:
- Verify the destination responds correctly over the new IP:
curl -I --resolve example.com:443:NEW.IP.HERE https://example.com/. A 200 or expected redirect means the document root and SSL are wired up. - Update A and AAAA records at the registrar or external DNS to point at the new server.
- If you're moving DNS hosting too, change the NS records last and only after the authoritative zones serve correctly from the new nameservers.
- Trigger AutoSSL on the new account from WHM → SSL/TLS → Manage AutoSSL → Check "username" Now. If issuance fails, work through the WHM AutoSSL failures decision tree — most post-migration AutoSSL failures are stale CAA records published by Virtualmin's Let's Encrypt integration that need to be removed at the registrar before cPanel can issue a fresh certificate.
Step 7 — Decommission carefully
Leave the Virtualmin server up for at least 7 days post-cutover, serving the same vhosts so direct-IP traffic and stale resolvers continue to land somewhere. Pull mail from the old IMAP store once per day during the grace window — IMAP clients can fail over slowly, and any mail delivered to the old Maildir in that window is invisible to the customer until you copy it across.
After the grace period, snapshot the source, archive it to cold storage for 90 days (enough to cover a customer "I lost a file" ticket), and destroy the instance. Unlike CyberPanel, Virtualmin doesn't carry a known security debt that requires an urgent shutdown — there is no harm in keeping the image around for warm rollback if a customer reports something missing in the first week.
FAQ
Is there a one-shot tool to migrate Virtualmin to cPanel?+
Does the migration work the same for Virtualmin GPL and Virtualmin Pro?+
How do I migrate Virtualmin sub-servers?+
Will customer mailbox passwords survive the migration?+
What about the Postfix mail queue at cutover?+
How long does a Virtualmin to cPanel migration take?+
Next steps
- New to cPanel? Walk through the cPanel VPS install, then WHM two-factor authentication setup before exposing WHM to the internet.
- Want LiteSpeed performance to replace whatever NGINX caching Virtualmin was doing? See installing LiteSpeed on cPanel and enabling LSCache for WordPress on cPanel.
- Activate your destination panel before the first cutover with a fresh cPanel licence — running migration loads against a trial activation hits rate limits at the wrong moment.