Skip to content

DevSkillsIT/cpanel-addon-domain-migration-between-accounts

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

cPanel Addon Domain Migration Between Accounts (Same Server)

A production-grade Bash tool to migrate an addon domain — including its subdomain children, MySQL database, DNS records, SSL certificates, and mail routing — between two cPanel accounts on the SAME server.

Shell cPanel License


Why this script exists

cPanel & WHM does not provide a native API or WHM interface for moving an addon domain between two accounts on the same server. The official options cover only:

  • Transfer Tool — account-to-account, but only between DIFFERENT servers.
  • Convert Addon Domain to Account — extracts an addon into a NEW standalone cPanel account (cannot merge into an existing one).
  • JetBackup Restore — restores to the SAME origin account; no cross-account support (open feature request since 2022).

For hosting providers consolidating clients, merging resellers into a single account, or simply reorganizing domains between accounts they already run, there is no supported path. Every guide online ends with "do it manually, step by step, domain by domain" — which is where bugs and data loss creep in.

This script composes the cPanel primitives that DO exist (whmapi1 create_subdomain, whmapi1 create_parked_domain_for_user, whmapi1 delete_domain, uapi Mysql *, rsync, mysqldump) into a single idempotent flow, with defensive checks for every failure mode we encountered in production.


What it does

For a single addon domain (or a batch via --batch-file), the script:

  1. Preflight — validates source/destination accounts, ownership, disk space, PHP version per vhost, presence of subdomain children (which otherwise block the delete), MySQL DB reachability, email account detection, domain registration status (RDAP / DoH).
  2. DNS snapshot — dumps the full DNS zone as JSON (not just the raw .db file). This is what drives record replay after cPanel regenerates the zone during vhost creation.
  3. File syncrsync -aHAX --delete-after --backup --backup-dir=... with the destination user's ownership, preserving .user.ini, .htaccess, WordPress drop-ins, ACLs and xattrs.
  4. Database migrationmysqldump of the source DB, creates the destination DB / user (with a prefix discovered via uapi Mysql get_restrictions — does not assume ${user:0:8}_), imports, patches wp-config.php and verifies the login works, auto-resetting the password via uapi Mysql set_password if cPanel stored a different hash from what we generated.
  5. Subdomain children — if the parent addon has children (e.g. lp.example.com sitting under example.com), each child is migrated first, because cPanel rejects delete_domain on the parent while any child still exists.
  6. Vhost creation on destinationcreate_subdomain + php_set_vhost_versions (preserves the source PHP version).
  7. Vhost removal on sourcedelete_domain for the addon and its control subdomain. This must run BEFORE create_parked_domain_for_user on the destination, because cPanel bans the same domain being in two accounts simultaneously.
  8. Addon registration on destinationcreate_parked_domain_for_user, parsing metadata.result from the JSON response rather than trusting the CLI exit code (which is always 0).
  9. DNS record replay — every MX / CNAME / TXT / SRV / A / AAAA record from the pre-migration snapshot is re-applied via whmapi1 addzonerecord. cPanel wipes customizations during vhost creation; without this step you lose Microsoft 365 / Google Workspace MX, DKIM, SPF, SRV Teams/Lync, custom subdomain CNAMEs, etc. + characters in TXT values are URL-encoded as %2B so whmapi1 does not decode them to spaces (silent SPF corruption).
  10. DNS completeness check + auto-retry — after replay, the script diffs the backup JSON against the live zone and retries any missing record up to 3 times before failing the migration.
  11. Exim mail routing — if the restored MX points outside the apex (M365, Google, Mailgun, CarrierZone, etc.), the domain is moved from /etc/localdomains to /etc/remotedomains and Exim is rebuilt/restarted so the local MTA does not intercept mail that should go to the external provider.
  12. SMTP banner probe — opens TCP/25 to the external MX to confirm it actually responds.
  13. httpd/LSWS rebuild + cache flush + LSWS reload.
  14. AutoSSL reissue with active wait loop — triggers start_autossl_check_for_one_user and polls every 10s (up to 120s by default) for the cert to be reissued for the real apex, reloading LiteSpeed the moment a new cert is detected.
  15. Smoke test via GET + body analysis — uses a browser User-Agent (sites with defensive .htaccess rules return 403 on HEAD requests, which is a false alarm) and scans the body for signatures like "Database Error", "erro crítico", "Fatal error" or empty bodies under 80 bytes.
  16. Final DNS reconciliation — one last replay + verify pass after AutoSSL / httpd rebuild, in case those actions wiped records again.

Each step is idempotent — a re-run after a partial failure resumes cleanly rather than duplicating work or corrupting state.


Requirements

  • cPanel & WHM (tested on 132.x; should work on 106+ where the WHM API 1 calls used here exist).
  • Root access on the server.
  • Tools: whmapi1, uapi, rsync, mysqldump, mariadb (or mysql), jq, curl, awk, grep, find, openssl, sed, dig. Optional: wp-cli.
  • A DNS backend reachable locally (PowerDNS with launch=bind, or standalone BIND).
  • Web server LiteSpeed or Apache (the script uses /scripts/rebuildhttpdconf + /usr/local/lsws/bin/lswsctrl reload; replace that function if you run stock Apache).
  • Exim as the MTA (cPanel default).

Installation

# 1. Clone into a directory of your choice
sudo git clone https://github.com/DevSkillsIT/cpanel-addon-domain-migration-between-accounts.git /opt/cpanel-migrate-addon
sudo chmod 750 /opt/cpanel-migrate-addon/cpanel-migrate-addon.sh

# 2. (Optional) create a stable install prefix where logs and state will live
sudo mkdir -p /usr/local/cpanel-migrate-addon/{bin,log,state,reports}
sudo ln -s /opt/cpanel-migrate-addon/cpanel-migrate-addon.sh \
           /usr/local/cpanel-migrate-addon/bin/cpanel-migrate-addon.sh

# 3. (Optional) expose via PATH
sudo ln -s /usr/local/cpanel-migrate-addon/bin/cpanel-migrate-addon.sh /usr/local/bin/cpanel-migrate-addon

Environment variables

Variable Default Purpose
INSTALL_PREFIX /usr/local/cpanel-migrate-addon Where logs, state and DNS backups are written
AUTOSSL_TIMEOUT_SEC 120 Maximum time the script waits for AutoSSL to reissue
AUTOSSL_POLL_SEC 10 Polling interval while waiting for the new cert
NO_COLOR (unset) Set to disable ANSI color output

Set them before the invocation if you need to override:

INSTALL_PREFIX=/srv/cpmig AUTOSSL_TIMEOUT_SEC=240 \
  /usr/local/bin/cpanel-migrate-addon --domain=... --to=... --apply --yes

Usage

One domain

# Source account is auto-detected from /etc/userdomains
cpanel-migrate-addon --domain=example.com --to=destacct --dry-run
cpanel-migrate-addon --domain=example.com --to=destacct --apply --yes
cpanel-migrate-addon --domain=example.com --to=destacct --verify

Batch (multiple domains from a file)

cat > domains.txt <<EOF
# one domain per line, # for comments, blank lines skipped
site1.com
site2.net
# optional per-line source override:
site3.org,olduser,newuser
EOF

cpanel-migrate-addon --batch-file=domains.txt --to=destacct --apply --yes

A consolidated Markdown report is written to $INSTALL_PREFIX/reports/batch-<timestamp>.md.

Modes

Mode Effect
--dry-run Plans the migration, prints every command, shows real rsync --dry-run stats, touches nothing.
--apply Executes. Writes logs to $INSTALL_PREFIX/log/<domain>-apply-<ts>.log.
--verify Post-migration deep health check — DB login via defaults-file (bypasses the MariaDB MYSQL_PWD CLI bug), WP siteurl via the auto-detected $table_prefix, body smoke (GET + WordPress error detection), certificate SAN coverage, DNS record diff against the pre-migration JSON snapshot, Exim routing sanity. Read-only.

Useful flags

Flag Behavior
--from=<user> Override auto-detected source account
--no-subs Fail at preflight if the parent has subdomain children (default is to auto-migrate them first)
--strict-dns Abort if the domain is expired / NXDOMAIN (default is WARN)
--skip-db Do not migrate MySQL even if wp-config.php exists
--skip-ssl Do not trigger AutoSSL (cron will catch up within 4h)
--force-wp / --force-static Override site-type autodetection
--yes Non-interactive (no confirmation prompt)
--quiet Reduce log chatter

Run --help for the full synopsis.


Example output

▶ Preflight checks
   ✓ domain example.com confirmed as addon of olduser
   ✓ source docroot: /home/olduser/public_html/_domains/example.com
   ✓ site type: wordpress (wp-config.php found)
   ✓ source DB: olduser_wp @ localhost
   ✓ dest DB planned: newuser_wp
   ✓ DB host reachable: localhost
   ✓ source PHP version: ea-php83
   ✓ detected 1 subdomain children under example.com
   ✓ will migrate them automatically before the parent
   ✓ domain status: active (registered, expires 2028-03-17T21:15:41Z)
▶ Backup DNS zone for example.com (full JSON snapshot)
   ✓ zone JSON: /usr/local/cpanel-migrate-addon/state/dns-backup/example.com.db.20260424-094512.json (34 records)
▶ Destination directory
▶ rsync data (source -> destination)
▶ Database migration
   ✓ DB login confirmed with generated password
▶ Migrate subdomain child: blog.example.com
▶ Create control subdomain on destination
▶ Set PHP version on destination vhost (ea-php83)
▶ Remove from source (olduser)
▶ Park domain on destination vhost
▶ Restore DNS custom records (post-migration)
   ✓ records replayed — added=24 skipped=8 failed=0
▶ Verify DNS restore completeness
   ✓ zone complete — all 1-pass records match backup
▶ Exim mail routing
   ✓ MX points to 'example-com.mail.protection.outlook.com' (external) — domain must be REMOTE
   ✓ Exim reconfigured: example.com marked as remote
▶ SMTP banner probe on external MX
   ✓ MX example-com.mail.protection.outlook.com responded: 220 VI1PE...EURP250CA0077.outlook.office365.com
▶ AutoSSL reissue + wait for cert
   ✓ (40s) cert emitted for example.com (CN=example.com)
   ✓ LSWS reloaded to serve new cert
▶ Smoke test (GET + body analysis)
   ✓ http://example.com/ -> HTTP 200 (15623B, ok)
   ✓ https://example.com/ -> HTTP 200 (15623B, ok)
✅ apply completed for example.com

Rollback

On failure during the destructive section (steps 7 onward), the script prints the exact whmapi1 commands required to undo each change, plus the paths of preserved artifacts:

❌ ABORT (code=17): remove_source_addon failed
   Preserved artifacts:
     snapshot: /usr/local/cpanel-migrate-addon/state/example.com.snapshot.json
     log:      /usr/local/cpanel-migrate-addon/log/example.com-apply-...log
     DB dump:  /usr/local/cpanel-migrate-addon/state/example.com.dump.sql

   # To roll back:
   whmapi1 delete_domain domain=example.com
   whmapi1 delete_domain domain=example.com.destacct.example.com
   whmapi1 create_subdomain domain=example.com.olduser.com document_root=...
   whmapi1 create_parked_domain_for_user username=olduser domain=example.com ...
   uapi --user=destacct Mysql delete_database name=destuser_wp
   uapi --user=destacct Mysql delete_user name=destuser_wp

The source account's DB is never dropped by the script. The rsync uses --backup-dir so overwritten destination files are preserved.


⚠️ What this script does NOT do

Please read this list carefully before running the script. Anything outside this list must be handled separately — typically through JetBackup / cPanel full-backup, manual copy, or a dedicated tool.

Email is NOT migrated

  • Mailboxes under /home/<user>/mail/<domain>/ (IMAP accounts, Dovecot index/cache, sent/trash/archive folders)
  • Mail forwarders, autoresponders, filters (Exim routing rules tied to the account)
  • BoxTrapper / SpamAssassin user rules
  • cPanel-generated DKIM private keys — the DNS TXT record is restored from the backup, but the signing key lives in /etc/exim.conf.local / /var/cpanel/domain_keys/. If mail is routed externally (M365/Google/etc.) this is irrelevant; if mail was previously local, mail signed by the old key will fail DKIM verification at recipients that have cached the key.

The preflight step detects local mailboxes and prints a loud warning before continuing. If the domain has mailboxes and you want to keep them, migrate them BEFORE running this script with tools like imapsync, doveadm backup, JetBackup's mailbox restore, or a full cPanel backup + restore.

Account-level resources NOT migrated

  • FTP accounts (additional FTP users beyond the main cPanel account)
  • SSH keys
  • Cron jobs (/var/spool/cron/<user> — neither rsync nor the script touches this path)
  • Password-protected directories configured through cPanel GUI (.htpasswd files inside the docroot ARE copied, but the cPanel-side association is not)
  • IP Blocker rules, Hotlink Protection, Leech Protection
  • Custom Apache handlers / MIME types (GUI-managed ones; .htaccess-based rules migrate with rsync)
  • Softaculous installations metadata — files are copied, but the Softaculous registry entries (backup schedules, auto-updates) remain on the source account
  • cPanel account packages, feature lists, CloudLinux LVE limits, reseller ACLs
  • WebDisk accounts
  • Backup job configurations (JetBackup, cPanel backup, custom)
  • Manually-installed SSL certificates — AutoSSL is triggered after migration, so Let's Encrypt / Sectigo free certs are reissued. Paid / manually-installed certificates need to be re-installed on the destination.

DNS / domain edge cases NOT handled

  • Wildcard subdomains (*.domain.com) — partially handled but not fully validated in production
  • Multi-level subdomain children (sub.sub.domain.com) — only direct children of the parent addon are detected and migrated
  • cPanel Redirects configured through the Redirects GUI — these are stored in the user's userdata file, not the docroot
  • DNSSEC keys (/var/cpanel/dnssec-keys/) — regenerated on the destination by cPanel; external DS records at the registrar will need updating
  • Custom name server delegation changes at the registrar

Operational caveats

  • Same-server only. For cross-server migration, use the cPanel Transfer Tool.
  • LiteSpeed / Apache assumption. The reload call assumes cPanel's LSWS binary path. For OpenLiteSpeed or stock Apache you'll need to adapt cache_reload() / rebuild_httpd_conf().
  • Brazilian TLD (.br) RDAP is queried via rdap.registro.br; other TLDs use Cloudflare DoH. Less common TLDs may need specific adjustments.
  • Browser caching / HSTS during the 1–3 second cutover window may cause some users to temporarily see the cPanel SORRY page. Recommend them to use an incognito window if they hit that.

💼 Need help with cPanel migration, hosting automation, or mail deliverability?

Skills IT — Technology Solutions specializes in hosting infrastructure and has deep expertise in cPanel/WHM administration, large-scale WordPress migrations, mail deliverability (SPF/DKIM/DMARC), DNS architecture, and hosting automation at scale. Our team also builds Artificial Intelligence and Model Context Protocol (MCP) integrations for hosting providers, system integrators, and agencies running multi-tenant environments.

Our services:

  • ✅ cPanel/WHM consulting, hardening and migration
  • ✅ Mail deliverability audits (SPF, DKIM, DMARC, BIMI)
  • ✅ Custom automation scripts for hosting operations
  • ✅ DNS architecture, DNSSEC rollout, secondary/GSLB design
  • ✅ WordPress / WooCommerce performance tuning on LiteSpeed
  • ✅ Security hardening (CSF, Imunify360, ModSecurity tuning)
  • ✅ Custom MCP development for hosting infrastructure
  • ✅ AI integration with provisioning, billing and support systems
  • ✅ Incident response and post-mortem consulting
  • ✅ Specialized training for hosting operations teams

📞 WhatsApp/Phone: +55 63 3224-4925 — Brazil 🇧🇷 🌐 Website: skillsit.com.br 📧 Email: contato@skillsit.com.br

"Transforming infrastructure into intelligence"


Disclaimer

This script is provided "as is" and "as available" without warranty of any kind, express or implied, including but not limited to warranties of merchantability, fitness for a particular purpose, non-infringement, or data preservation. Use at your own risk.

Skills IT — Technology Solutions and the script's contributors are not liable for any direct, indirect, incidental, consequential, special, exemplary or punitive damages arising out of or in connection with the use of this software, including but not limited to: data loss, website downtime, email delivery failures, DNS outages, SSL certificate issues, MySQL corruption, broken WordPress installations, or any service interruption affecting your customers or your business.

Before running --apply in production:

  1. Take a full snapshot at your hypervisor / cloud provider level (Vultr snapshot, AWS AMI, DO snapshot, Proxmox backup, etc.).
  2. Run a cPanel /scripts/pkgacct <user> backup of both source and destination accounts if possible.
  3. Run --dry-run first and review the entire output.
  4. Test the full cycle on a non-production domain before production use.
  5. Perform the migration during a maintenance window and notify affected users.

By using this script you acknowledge that you have read and understood the limitations in the "What this script does NOT do" section above, and that you are solely responsible for validating the migration outcome, rolling back if necessary, and communicating with affected users.



Contributing

Pull requests welcome, especially:

  • Support for servers running OpenLiteSpeed or stock Apache without LSWS.
  • RDAP handling for more ccTLDs (.ar, .uy, .co, .pt, etc.).
  • Integration with JetBackup / cPanel backup system for a pre-migration snapshot when neither mysqldump nor rsync are desirable (e.g., very large sites where block-level snapshots are preferable).
  • Hooks system so operators can plug in site-specific pre/post actions (search-replace for URL changes, flushing custom caches, etc.).

Please open an issue describing the environment and reproduction steps before submitting a large change.


Troubleshooting

See TROUBLESHOOTING.md for the historical list of real-world failure modes encountered during development and the fix each one triggered in the script.


License

MIT — see LICENSE.

About

Bash tool to migrate cPanel addon domains — with subdomain children, MySQL, DNS records (M365/Google safe), SSL, and Exim mail routing — between two cPanel/WHM accounts on the SAME server. Fills the gap where cPanel has no native API for this. Dry-run, batch mode, deep verification.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages