# Importing cPanel accounts

> Migrate accounts from a cPanel/WHM server into CorePanel — over SSH from the admin panel, or from a staged cpmove archive with the CLI. Scope, procedures, credential preservation and the import report.

Source: https://www.corepanel.net/docs/cpanel-import/
Last updated: 2026-09-02
Part of the CorePanel documentation — https://www.corepanel.net/docs

---

CorePanel can take over hosting accounts from an existing **cPanel/WHM** server and
recreate them natively — files, mail, databases, DNS, cron and SSL — while **preserving
the original passwords** wherever the hash scheme allows, so most users never have to
reset anything.

There are two ways to run an import, and they take **different inputs**:

| | Admin panel (UI) | CLI |
|---|---|---|
| Input | **Live pull** over SSH from the source WHM | A **staged `cpmove`/backup archive** already on the server |
| Scale | One or many accounts (batch) | One account per invocation |
| DNS cut-over | Optional, automatic | Not performed |
| Progress | Live, reconnectable | Printed to the terminal |
| Best for | Migrating a whole server, hands-off | Scripting, air-gapped transfers, a single archived account |

Both paths feed the **same import engine**, so the scope of what gets imported and how
credentials are handled is identical.

> **Root is required**
>
> Preserving system passwords and the original UID/GID means writing `/etc/shadow` and
> running `useradd -u/-g`. The UI needs **root SSH credentials** on the source server; the
> CLI must run as **root** on the CorePanel server.
## What gets imported (scope)

Everything below is provisioned automatically from one cPanel account:

- **System account** — the Linux user, its password hash (when compatible), and
  optionally its original **UID/GID**.
- **Closed logins** — a mailbox or FTP account the source had **locked** arrives closed:
  it is created inactive, and the password cPanel kept behind the lock marker is preserved,
  so reopening it later restores the login its owner already knows instead of replacing it.
  These are *not* listed as passwords needing a reset — there is nothing to reset, and
  acting on such a list would reopen an access somebody deliberately shut.
- **Suspension state** — an account cPanel had **suspended** is imported suspended, with
  the source's own suspension date. That is not a detail of bookkeeping: importing it
  active would put a site somebody deliberately took down — for non-payment, for abuse —
  back on the internet. Its sites serve the [suspension page](https://www.corepanel.net/docs/accounts/suspension)
  and its logins stay closed until you lift it, which any edition can do.
- **Primary domain** — virtual host, managed DNS zone and a fresh **DKIM** key.
- **PHP version** — CorePanel runs **one PHP version per account** (the FPM pool belongs to
  the account's system user), so the primary domain's source version sets the account's:
  exact `X.Y` if installed, otherwise the newest minor of the same major, otherwise the
  server default. Any downgrade is noted in the report — and so is every domain that ran a
  *different* version on cPanel, because it now runs the account's.
  A site left on its old server's default records no version at all (cPanel writes
  `inherit`), and an archive does not carry that server's configuration — so there is
  nothing to read it from. Such a domain is reported by name, and the account's version is
  then decided by the rule above without it: another of its domains that *does* name one,
  otherwise the newest version installed here, which can be several releases newer than
  the site ran. Check it after the import and move the account with
  `corepanel account update <account> --php <version>` if it needs another one. A
  [whole-server transform](https://www.corepanel.net/docs/cpanel-transform) has the source server in front of it
  and reads the answer instead of guessing.
- **Secondary domains** — addon domains, domain aliases (parked) and subdomains, each
  with its own vhost. cPanel does not store an addon as a domain: it creates an internal
  subdomain of the primary (`addon.com` → `addon.example.com`) and keeps the real vhost
  under *that* name. CorePanel imports the addon as a first-class domain carrying that
  vhost's document root, PHP version and certificate, and does not recreate the internal
  hostname — the report notes it rather than standing up a second site on the same files.
- **Domains parked on an addon** — a domain parked on top of an addon rather than on the
  account's main site. cPanel files it as a *second* addon entry pointing at the same
  internal subdomain, so nothing but the name says which of the two is the site. CorePanel
  imports it as an **alias of that addon**: it serves the addon's document root and its
  mail is redirected there, and the report says which addon it belongs to.
- **Nested subdomains** — a subdomain whose prefix is more than one label
  (`dev.blog.example.com`), imported as it is: one vhost, one record in the zone that owns
  it. Note a nested name is **not covered by the parent's wildcard certificate** — a
  wildcard spans a single label — so CorePanel issues one of its own for it.
- **Mailboxes** — with their **messages** (full Maildir) and passwords preserved.
- **Mail extras** — forwarders, autoresponders and mail filters (translated to
  CorePanel's Sieve model).
- **FTP accounts** — the main account plus any extra ones, passwords preserved.
- **MySQL** — databases **with their data**, plus database users (their MySQL password
  hash is re-applied, so app config keeps working).
- **Cron jobs** — installed as panel-managed cron.
- **SSL certificates** — cert, chain and key installed via the certificate manager.
- **DNS records** — parsed from the source BIND zones and merged into the new zone;
  A/AAAA records that pointed at the **old server IP are rewritten** to this server's IP,
  while MX, TXT/SPF, DKIM, CNAME and CAA are preserved.
- **Home directory** — the full site tree, restored and re-owned (with SELinux contexts
  fixed).
- **WordPress installations** — the import ends by scanning the account it just created,
  so every WordPress it brought over is already listed (and manageable) in
  [WordPress](https://www.corepanel.net/docs/wordpress) when the job finishes. cPanel has no catalog of its own to
  copy: the sites are found on disk, exactly as the manual **Scan** does.
- **`.htaccess` rules** — read where they are, not converted. Permalink blocks, front
  controllers, `Deny from all` files and password-protected directories keep working
  without anything being turned on. See [.htaccess Support](https://www.corepanel.net/docs/web/htaccess) for what
  is applied and what is reported instead.

### Where the sites end up

**Every site keeps serving from the path it had on the source server.** The importer
does not move the files into CorePanel's own layout, it re-points the vhosts at where
the restored home actually put them:

| On cPanel | On CorePanel after the import |
|---|---|
| `~/public_html` (primary) | `~/public_html` |
| `~/public_html/addon.com` (addon, cPanel nests them) | `~/public_html/addon.com` |
| `~/public_html/blog` (subdomain, named after the *label*) | `~/public_html/blog` |
| `~/dl.example.com` (a site edited by hand to sit beside `public_html`) | `~/dl.example.com` |

Only the **home directory prefix** changes, and only when the account gets a different
username here (a name conflict on import). A site provisioned by CorePanel afterwards
uses CorePanel's own layout — `~/domains/<name>` — but nothing already imported is
rewritten to match it.

This matters because absolute paths are baked into the sites themselves: `wp-config.php`,
compiled caches, `.htaccess` include paths, and the **cron jobs the importer restores
alongside them**. Moving the tree would break all of those silently. CorePanel instead
extends the two things that make a directory servable — its SELinux file context and the
PHP pool's `open_basedir` — to cover the imported path.

> **A path outside the account home cannot be preserved**
>
> A document root that never lived inside the account's home directory (`/var/www/...`) has
> nowhere to land. The domain is still created — you keep the vhost, the DNS zone and the
> mail identity — but it serves CorePanel's default empty directory, and the report says so
> with an *unmapped document root* warning naming both paths. Move the content into the
> account home and repoint the domain to finish it.
### What is *not* imported

This is a **one-shot import**, not a two-way sync, and it only reads cPanel archives:

- Other panels (Plesk, DirectAdmin) — cPanel only.
- The cPanel **reseller tree** — you decide which CorePanel seller each account belongs to.
  Its *package* is resolved for you from the plan name when a package of that name exists.
- cPanel-specific state with no CorePanel equivalent — `cpbackup` jobs, Softaculous
  installs, cPanel API tokens.
- A mailbox on a domain that has **no mail identity** on this server (e.g. a mailbox
  under an alias or a subdomain) — it is reported as *dropped*, never silently lost.
- **Per-domain PHP versions** — CorePanel gives the whole account one version (see above).
  An addon still running an older PHP on cPanel moves onto the account's version, and the
  report names every domain this affects so you can check those sites first. The account's
  version can be [changed afterwards](https://www.corepanel.net/docs/accounts/editing#changing-the-php-version) if
  the importer's choice turns out to be the wrong one.

> **Nothing is ever dropped silently**
>
> Anything the importer can't reproduce is recorded in the job **report** as a warning or a
> per-resource status. A clean run reports `completed`; a run with any forced reset, dropped
> or failed resource reports `partial` (completed with warnings).
## How passwords are preserved

CorePanel imports the original password **hashes** verbatim whenever they can be verified
later — no plaintext is ever needed or shown.

| Credential | Preserved when the hash is… | Otherwise |
|---|---|---|
| System / SSH / main FTP | SHA-512 `$6$`, SHA-256 `$5$`, MD5 `$1$`, bcrypt `$2a/2b/2y$`, yescrypt `$y$` | Random password set, **reset required** |
| Mailbox & extra FTP | Same crypt families as above | Random password set, **reset required** |
| MySQL users | `mysql_native_password` / `caching_sha2_password` (re-applied at the MySQL layer) | — |

The **system** login is the strictest case: RHEL's `crypt(3)` verifies the hash at SSH/FTP
login, so a scheme it can't check (e.g. argon2) is treated as non-preservable — the account
gets a random password and is flagged for a reset, rather than becoming an account that can
never log in. When a credential can't be preserved, the login **still exists** — the user
just has to set a new password. Those users are listed under **Password resets required** in
the report.

## Procedure — Admin panel (live pull over SSH)

Open **Migration → Import account from cPanel** in the panel. The import runs entirely on
the CorePanel server, so you can close the browser and reconnect to it later.

### 1. Connect to the source server

Enter the source WHM/cPanel **hostname or IP**, the SSH **port**, and **root** SSH
credentials. The password is used only for the connection and is **never stored**.

![Step 1 — connect to the source cPanel/WHM server with root SSH credentials](https://www.corepanel.net/_astro/cpanel-import-connect.DN5Rss1K.svg)

### 2. Select the accounts

CorePanel lists every account on the source server with its primary domain, plan and disk
usage. Verify the server's **host-key fingerprint** matches the real source, search/filter,
and tick one or many accounts (**Select all** acts on the current filter). Suspended
accounts are flagged.

![Step 2 — pick one or more accounts from the source server inventory](https://www.corepanel.net/_astro/cpanel-import-select.C6Q5Qr8F.svg)

### 3. Choose options

![Step 3 — target package, UID/GID preservation, DNS cut-over and conflict policy](https://www.corepanel.net/_astro/cpanel-import-options.DPlUKjtP.svg)

- **Target hosting package** — pick one to attach every imported account to it. Leave it
  unset and CorePanel resolves each account's own cPanel plan by name, so accounts that came
  from different plans land on the matching CorePanel packages instead of all on one.

  Either way, **the limits an account arrives with are the ones the source recorded for that
  account** — disk, bandwidth, mailboxes, databases, FTP, aliases, subdomains and addon
  domains — not the package's. Both panels copy limits into the account when it is created
  and let you edit a single account afterwards, so an account whose database limit was
  raised by hand keeps the number it was running under. The package only supplies what the
  source left unsaid. Applications are the exception: cPanel's Passenger limit is a
  different product, so imported accounts get no applications until you grant them.
- **Preserve UID / GID** *(recommended)* — keep the source system user/group ids for a
  full-fidelity migration.
- **Point DNS to this server** — when each account finishes, CorePanel logs into the
  **source** server (still authoritative during the switch) and repoints only the A records
  that pointed at the old IP, so live traffic follows the migration without waiting for a
  registrar change. Domains whose DNS lives elsewhere are left untouched and flagged as
  *external*.
- **If the account or domain already exists** — **Abort** (fail) or **Skip** (do nothing).

### 4. Review and start

Confirm the summary and start. Accounts are imported **one at a time** on the server.

![Step 4 — review the migration summary before starting](https://www.corepanel.net/_astro/cpanel-import-confirm.zY0ncLP1.svg)

### 5. Watch live progress

Each account shows a phase tracker (**Connect → Transfer → Restore → Done**), the transfer
size, a live count of created / reset / deferred / dropped / failed resources, and the full
per-resource log. The import **keeps running on the server** if you navigate away — reopen
the page and pick it up from **Recent imports**.

![Step 5 — live per-account import progress with the resource report](https://www.corepanel.net/_astro/cpanel-import-progress.Ck-WcLvQ.svg)

## Procedure — CLI (from a staged archive)

Use the CLI when you already have a `cpmove-<user>.tar.gz` / `backup-*.tar.gz` archive (or
an extracted account directory). The archive path is resolved **on the CorePanel server**,
so stage it there first — the conventional location is `/var/lib/corepanel/import/`.

### Preview first (`--dry-run`)

A dry-run parses the archive and prints the plan — resource counts, credential
compatibility, conflicts and warnings — **without touching the system**:

```bash
corepanel account import /var/lib/corepanel/import/cpmove-acme.tar.gz --dry-run
```

```text
cPanel import preview (dry-run)
==============================
Source:    cpanel 110.0.5 @ web-oldhost-01.example.net
Account:   acme (uid 1042, gid 1042)
Domain:    acme-store.com
Plan:      business
Contact:   admin@acme-store.com

Resources:
  domains=5 mailboxes=8 forwarders=4 databases=2 db-users=2
  dns-records=27 ftp=3 cron=1 certificates=1

Credentials:
  system password: preserved
  mailbox/ftp: 9 preserved, 2 reset (of 11)

Domains:
  - acme-store.com                 primary [ssl]
  - shop.acme-store.com            subdomain
  - dev.shop.acme-store.com        subdomain
  - acme-labs.io                   addon
  - acme-labs.net                  alias of acme-labs.io

Conflicts: none
```

### Run the import

```bash
corepanel account import /var/lib/corepanel/import/cpmove-acme.tar.gz \
    --package 3 --preserve-uid --report /root/import-acme.json
```

Flags:

| Flag | Purpose |
|---|---|
| `--dry-run` | Parse and validate only; print the plan and touch nothing |
| `--seller <id>` | Target seller id (default: the primary seller) |
| `--package <id>` | Target hosting package; its limits drive the account |
| `--preserve-uid` | Keep the source UID/GID via `useradd -u/-g` |
| `--on-conflict abort\|skip` | Policy when the user/domain already exists (default `abort`) |
| `--report <path>` | Write the full JSON import report to this path |

The command prints the per-resource result and exits **non-zero** if the job status is
`failed`, so it fits cleanly into migration scripts:

```text
cPanel import
=============
Job:      #42
Status:   partial
Account:  acme (id 17)
Domain:   acme-store.com

Resources:
  [created ] account:acme — system password preserved ($6$)
  [created ] domain:acme-store.com — primary domain, vhost and managed DNS zone
  [created ] mailbox:sales@acme-store.com — mailbox password preserved
  [reset   ] mailbox:old@acme-store.com — mailbox password could not be preserved; a reset is required
  [created ] db:acme_shop — database restored

Password resets required (1):
  ! old@acme-store.com
```

A running or finished job can also be polled with `core.GetImportJob` and listed with
`core.ListImportJobs` — the same jobs the panel's **Recent imports** shows.

## Reading the report

Each resource ends in one of these states:

| Status | Meaning |
|---|---|
| **created** | Imported successfully, credential preserved |
| **reset required** | Imported, but the password couldn't be preserved — the user must set a new one |
| **deferred** | Recognized and handled in a later phase (not lost) |
| **dropped** | Permanently not imported (e.g. a package limit was hit, or a mailbox with no mail identity here) |
| **skipped** | Already existed, or skipped by the conflict policy |
| **failed** | An error occurred provisioning this resource |

The overall job is **`completed`** only when every resource is `created`/`deferred` with no
forced resets; any reset, dropped or failed resource makes it **`partial`**.

Warnings sit beside those statuses and can also downgrade a job to `partial` — an
*unmapped document root* (see [Where the sites end up](#where-the-sites-end-up)) does,
because the domain exists but its site is not serving.

## After the import

1. **Hand out password resets.** Everyone listed under *Password resets required* has a
   working login but a randomized password — send them a reset.
2. **Check DNS.** If you didn't use *Point DNS to this server*, update the A records (or the
   registrar's nameservers) so the domains resolve to CorePanel. Domains flagged *external*
   always need a manual change.
3. **Verify the account.** Confirm the site serves over HTTPS, mail authenticates
   (STARTTLS + IMAP login), and the databases contain their data. An account that
   arrived **suspended** is the exception: its site correctly shows the suspension
   page and none of its logins work. The report says so, and the accounts list badges
   it — [lift the suspension](https://www.corepanel.net/docs/accounts/suspension#lifting-a-suspension) if it
   should be serving.
   The report's **wordpress** line says how many installations were cataloged; if it reads
   *deferred*, the scan could not run — the sites are unaffected, run
   `corepanel wp scan <account>` to list them.
4. **Re-run if needed.** Imports are **idempotent** — re-running against an existing
   imported account resumes only the missing resources; it never duplicates them.

> **Free up conflicts before retrying**
>
> With `--on-conflict abort` (the default), an existing Linux user or domain stops the
> import. Resolve the conflict — or choose **skip** — before re-running.
