# Moving from cPanel

Move a WordPress site from a cPanel host to Deplane with one command: `deplane import cpanel` reads the cPanel account, shows what moves and what stays, then copies the site's files, database and users into a new WordPress app. You test the copy on its own address while the old host keeps serving your domain; when it works, you run the command once more with `--cutover` and change DNS. Email stays at your host.

The command runs on your own computer with a cPanel API token that never leaves it. It only reads cPanel, apart from writing one backup file into your home directory. The command and its flags are in the [CLI reference](cli.md#deplane-import-cpanel), and every error has a fix in [Troubleshooting and error codes](errors.md#cpanel_import_refused). There is no cPanel import in the dashboard yet: use the CLI.

## Before you start

1. [Install the Deplane CLI](install.md) and run `deplane login`.
2. Be on a plan with MySQL (Hobby or larger): a WordPress site needs a small app, a medium [MySQL](mysql.md) and a 5 GB [disk](disks.md). Free has no MySQL, and the report says so.
3. Find your cPanel server's name: the address you open cPanel at, such as `https://server1.host.example:2083`. Use that name for `--host`, not your own domain. Your cPanel username goes in `--user`.

## The API token

Create a token in cPanel: **Security**, **Manage API Tokens**, **Create**. Name it `deplane-import` and give it an expiry date (tomorrow is enough). A token works when your account has two-factor authentication, and you can revoke it in the same screen when you are done. Deplane takes only a token, never your cPanel password.

Give the token to Deplane in one of three ways, never as a flag:

- pipe it in: save it to a file and run `deplane import cpanel --host ... --user ... < token.txt`;
- set it for one command: `CPANEL_API_TOKEN=... deplane import cpanel ...`;
- or run the command in a terminal and paste it at the hidden prompt.

A token piped on stdin wins over `CPANEL_API_TOKEN`. The CLI says which one it used, never the token itself. It sends the token only to your cPanel server (`https://HOST:2083`). It is never written to disk, never printed, left out of `--debug`, and never sent to Deplane or to your app.

## Check first

`--check` reads the account and prints the report without changing anything:

```sh
$ deplane import cpanel --host server1.host.example --user shop --check < token.txt
example.com (cPanel shop@server1.host.example, WordPress 6.8.1, PHP 8.1) → Deplane app example-com

Site
  ✓ example.com, document root /home/shop/public_html on server1.host.example → Deplane app example-com.
  ✓ Left out: the files of other.example, which live inside this folder (each site is imported on its own, with --site).

WordPress
  ✓ WordPress 6.8.1, wp-config.php in the document root.
  ✓ Table prefix wpx7_, kept (WORDPRESS_TABLE_PREFIX).
  ✓ PHP 8.1 on cPanel → PHP 8.4 on Deplane.
  ⚠ Other settings in wp-config.php are not copied: MAILGUN_API_KEY. ...

Database
  ✓ shop_wp (84 MB on cPanel) → a new MySQL example-com-mysql (MySQL 8.4, medium), from the backup's dump. ...

Files
  ✓ cPanel writes a full account backup into your home directory, and the CLI downloads it to this computer. ...

Email and DNS
  ⚠ Email for example.com stays at your host (3 mailbox(es), MX mail.example.com (0)). ...

Plan
  ✓ Fits your plan: a small app, a medium MySQL and a 5 GB disk (the WordPress template).
```

Without `--site`, the report is about the account's main domain; pass `--site example.org` for an addon domain or subdomain. The app is named after the domain (`example.com` becomes `example-com`) unless you pass `--name`. Values from `wp-config.php`, such as the database password, are never shown or sent anywhere.

## What the import does

Without `--check`, after you confirm (`--yes` skips the question):

1. **Backup.** cPanel writes a full account backup (`backup-…_USER.tar.gz`) into your home directory, and the CLI downloads it to your computer. It keeps only the site's document root and its database dump, in a private temporary folder that is deleted at the end. The full backup includes your email, so it can be larger than the site.
2. **Sizes.** The site's files and dump are measured here, before anything is created on Deplane: up to 4.5 GB of files (the app's disk is 5 GB, and the rest is room for new uploads) and up to 5 GB of database.
3. **The app.** It is created from the [WordPress template](templates.md): the app, its MySQL and its disk, as `deplane launch --template wordpress` would.
4. **Files and database.** The CLI turns on the app's one-time import receiver, replaces the fresh WordPress files with your site's (WordPress core, themes, plugins and uploads), empties the database, loads your dump, and turns the receiver off. Meanwhile the app shows a "being moved" page to everyone, so no visitor sees a half-copied site. Your `wp-config.php` is not copied: Deplane's own reads the database settings Deplane manages, and your table prefix is kept.
5. **Test address.** Your site's addresses in the database (`https://example.com`, its `http` and `www` forms) are rewritten to the app's own address, such as `https://example-com.deplane.app`, so you can test every page there. The rewrite understands PHP's serialized values (widgets, theme options, page builders), as `wp search-replace` does.

Then it prints the test address and the steps left. Sign in at `/wp-admin` with your usual WordPress user and password: the users come with the database. Everyone is signed out once, because Deplane uses new security keys.

The copy on the test address is public, like your site. If something went wrong, run the same command again: it replaces the files and database of the app an earlier import made of the same site, without creating anything new. It backs up the app's disk and MySQL first and prints the commands to restore them. If a run stops half-way, the app keeps showing the "being moved" page until a run finishes.

WordPress's web installer (`/wp-admin/install.php`) is always blocked on Deplane: Deplane installs WordPress itself, so nobody can take over a site whose database is empty.

### What the backup fix-ups change

- cPanel's PHP handler block in `.htaccess` (`# php -- BEGIN cPanel-generated handler`) is removed: it names cPanel's PHP and would stop PHP from running.
- Your old document root (`/home/USER/public_html`) is written as `/var/www/html` in `.htaccess`, `.user.ini`, Wordfence's `wordfence-waf.php`, and in the database.
- MyISAM and Aria tables become InnoDB, which Deplane's backups need. MariaDB collations that MySQL 8.4 does not have become `utf8mb4_0900_ai_ci`. `DEFINER` clauses of views and triggers are removed. The report lists each of these.
- `error_log` files, `cgi-bin`, symbolic links and other domains' folders inside the document root are left out.
- A second WordPress in a subfolder (a `staging/` or `blog/` copy) is copied as files, with its own `wp-config.php`, and listed with a ⚠ line. Its database is not imported, so on Deplane it shows a database error; its installer is blocked, like the main site's. Delete the folder on Deplane if nobody uses it, or import it on its own later.

## PHP and WordPress versions

Deplane runs WordPress on PHP 8.4. Your site keeps the WordPress version it has; update it from the dashboard afterwards. The report warns when your site runs PHP older than 8.1 (plugins and themes that use removed functions may break), and when WordPress is older than 6.7, the first version that supports PHP 8.4. Update WordPress, themes and plugins on the old host first if you can, and test every page on the test address.

## What is refused

These stop the import before anything is created, with a ✗ line:

- No `wp-config.php` in the document root or one folder up. Only WordPress sites are imported for now, and only a WordPress at the root of its domain (not `example.com/blog`).
- A WordPress multisite network.
- A database on another server (`DB_HOST` is not `localhost`): the cPanel backup does not hold it.
- A database over 5 GB, or site files over 4.5 GB.
- A plan without MySQL (Free), or without room for one more app, MySQL and disk.
- An app name your team already uses for an app that was not made by an import: pick another with `--name`.

## Email stays at your host

Deplane does not host email. Your mailboxes, the MX records and the mail server stay at your cPanel host; only the website moves. The report shows the domain's mailboxes and MX records.

Watch for one trap: if the MX record names your domain itself (`example.com`), or `mail.example.com` is a CNAME to it, then pointing the domain at Deplane moves mail delivery too, and email stops arriving. Before the cutover, give `mail.example.com` its own A record to your cPanel server's IP address (the report shows it) and point the MX record at `mail.example.com`. Keep the SPF, DKIM and DMARC TXT records as they are.

## The cutover

When the test copy works:

1. A day before, lower the TTL of your domain's A record and the `www` record to 300 seconds at your DNS provider.
2. Stop editing the old site (or put it in maintenance mode), so nothing is lost.
3. Run the same command with `--cutover`:

   ```sh
   $ deplane import cpanel --host server1.host.example --user shop --cutover --yes < token.txt
   ```

   It copies the files and database again, this time without the address rewrite (only `http://` links become `https://`), sets the site's address to your domain, and adds `example.com` and `www.example.com` to the app. It prints the DNS records for both, as `deplane domains add` does. Anything changed on the test copy is replaced.
4. At your DNS provider, set the TXT records, then the A record of `example.com` and the CNAME of `www`. Remove any AAAA record of the domain. Keep the MX records, `mail`, and the SPF, DKIM and DMARC records. See [Custom domains](domains.md).
5. Deplane connects the domain and gets its certificate within minutes of DNS pointing at it. Until then, the app's own address redirects to your domain, which still reaches the old host.

### After the cutover

The app is now your live site. Running the import again, or `--cutover` again, would replace it with the cPanel copy and lose everything changed on Deplane since: orders, posts, comments, users. So the command asks you to type the domain first, and `--yes` does not skip that; in a script, pass `--replace-live` if you really mean it. An app with a custom domain attached counts as live too, cutover or not. Either way it backs up the disk and the MySQL first, for this run, and prints the restore commands; if a backup already ran in the last hour, it stops without deleting anything and asks you to run it again when the next backup is allowed.

### If an import stopped half-way

The app keeps showing the "being moved" page until an import finishes, also after a restart, so nobody sees a half-copied site. Run the same command again: it says it found the page from an earlier run, replaces the files and database, and ends it. To go back instead, restore the disk and MySQL backups the earlier run printed.

## Afterwards

- Delete the backup file (`backup-…_USER.tar.gz`) from your cPanel home directory (File Manager): it holds your whole account.
- Revoke the API token (cPanel, Security, Manage API Tokens).
- Keep the cPanel account until your email has moved elsewhere, or for as long as your mail stays there, and until the site has run on Deplane for a while.
- WordPress sends no email until you add an SMTP plugin (the [template's note](templates.md) says why); if your site sent mail through the cPanel server, set up SMTP.

## If cPanel does not let the token download

Some hosts let only a signed-in browser download files. Then the command stops after the backup is made, with [cpanel_download_refused](errors.md#cpanel_download_refused), and nothing is created on Deplane. Download the backup yourself and pass it with `--backup`:

```sh
$ deplane import cpanel --host server1.host.example --user shop --backup ~/Downloads/backup-10.31.2026_12-00-00_shop.tar.gz --yes < token.txt
```

`--backup` takes a full account backup (cPanel, **Backup**, "Download a Full Account Backup"), or a home directory backup with the database's own backup in `--db-dump` (`shop_wp.sql.gz` from the same screen). The token is still needed: the CLI reads the site's settings from cPanel.

## Limits

| Limit | Value |
|---|---|
| Sites per command | 1 (run it once per domain, with `--site`) |
| Site files | 4.5 GB, one file at most 4.5 GB |
| Database | 5 GB as cPanel reports it; 8 GB of SQL; one statement 32 MB |
| PHP | 8.4 |
| Multisite, WordPress in a subfolder, sites without WordPress | Not imported yet |
| Dashboard | Not yet: CLI only |
| Space on your computer | About the site's files plus twice the database, in the temporary folder (`TMPDIR`) |
