Skip to content

CorePanel CLI

The corepanel command-line interface is the administrative tool for operating a CorePanel server from the shell. It talks directly to corepanel-core over a local Unix socket, so it is ideal for initial provisioning, automation, and troubleshooting — no browser required.

Terminal window
corepanel system init # provision a fresh server
corepanel status # are the services up?
corepanel account # hosting accounts: create, update, import, back up, restore
corepanel domain / subdomain # domains, aliases and subdomains
corepanel mail # mailboxes, forwarders, antispam
corepanel cron # per-account cron jobs
corepanel wp # WordPress installations
corepanel app # application runtime
corepanel quota # disk quotas
corepanel ssl # commercial certificates
corepanel firewall # host firewall (firewalld)
corepanel license / edition # licensing and the edition in effect
corepanel notifications # what the server has been trying to tell you

Every group answers --help, and so does every command inside it.

Every command that acts on a hosting account takes it as <account>, and accepts any of three spellings:

SpellingExample
Linux usernamecorepanel domain list acmecorp
Any domain the account owns — primary, alias or addoncorepanel domain list example.com
Any subdomain of one of thosecorepanel domain list shop.example.com
Numeric account id (the legacy dom-{id} form works too)corepanel domain list 7

Reach for the username. It is what appears in your billing system, in /etc/passwd and in a support ticket, so it is the name you already know — whereas the numeric id is an internal row number you would have to look up first. The id keeps working everywhere it ever did, so existing scripts need no change.

A reference that names no account fails with a non-zero exit status, and that includes ids: corepanel domain list 999 reports that no account matches rather than printing an empty list, which would read like a real answer for an account that was never there.

There is no ambiguity for CorePanel to resolve: a domain always contains a dot and a Linux username never can, and CorePanel never creates a username made only of digits — so a bare number is always an id.

Manages certificates bought from a commercial CA. Every site already gets a free certificate, issued and renewed automatically — this command group is for the case where the certificate has to come from somewhere else: an OV/EV certificate, a corporate CA, or an organisation’s policy.

Terminal window
corepanel ssl list [--json]
corepanel ssl show <host> [--json]
corepanel ssl upload <host> --cert FILE --key FILE [--chain FILE]
corepanel ssl delete <host> [--yes]

Three rules explain the whole group:

  • An installed certificate is never replaced by a renewal. It lives in its own store, which nothing automatic writes to, and is served ahead of the automatic certificate.
  • Expiry degrades, it does not break. A bought certificate does not renew itself, so the panel warns from 30 days out. If it lapses anyway, the site falls back to its automatic certificate and keeps serving valid TLS — under a different issuer, which is why the warning matters.
  • Installing requires Pro or Business; listing, inspecting and removing do not. A server whose licence lapsed is never stuck with a certificate it cannot manage.

Lists every commercial certificate on the server with its issuer, expiry date and time remaining. The exit status is 1 when at least one certificate is inside its 30-day warning window or already expired, so it can be watched from monitoring without parsing the output. A server with no commercial certificates is not a problem: the list is empty and the status stays 0.

Shows one host’s certificate: subject, issuer, the names it covers, its validity window, the chain length and the SHA-256 fingerprint. The private key is never shown.

Installs a certificate. The material is read from files, not arguments: a private key on a command line ends up in the shell history and is visible to every user on the box through the process list.

--chain is optional — some CAs append the intermediate to the certificate itself, others mail it separately. The upload is refused when:

RefusalWhy it matters
The chain does not reach a trusted CAThe failure that works in the browser you test with (it cached the issuer elsewhere) and breaks on Android and curl
The key does not match the certificateWould take TLS down on the next handshake
The certificate does not cover the hostName mismatch in every browser
Expired, or not yet validCannot secure anything
Self-signedA trust error for every visitor

Nothing is written unless every check passes, so a rejected upload leaves the site exactly as it was. Installing over an existing certificate is how a renewal is applied.

Removes a host’s certificate and its private key, returning the host to the automatic certificate from the next connection on. This is also the way back if an installed certificate turns out to be wrong. Because the key is deleted, the command asks for confirmation; a non-interactive run must pass --yes.

Terminal window
# Monitoring: alert before a certificate runs out
corepanel ssl list >/dev/null || echo "a certificate needs attention"
# Install a certificate whose CA sent the bundle separately
corepanel ssl upload example.com \
--cert /root/example.com.crt \
--key /root/example.com.key \
--chain /root/example.com.ca-bundle
# Apply a renewal: same command, new files
corepanel ssl upload example.com --cert new.crt --key new.key --chain new.ca-bundle
# Scripting: days left on every certificate
corepanel ssl list --json | jq -r '.[] | "\(.host) \(.daysRemaining)"'
# Go back to the automatic certificate
corepanel ssl delete example.com --yes

bash corepanel [command] [subcommand] [flags]

| Command | Description |
|---------|-------------|
| `corepanel system init` | Interactive wizard to initialize a fresh server |
| `corepanel status` | Show the status of all CorePanel services |
| `corepanel quota` | Enable disk quotas, inspect state and usage, and set per-account limits |
| `corepanel account list` | List hosting accounts with their ids, usernames and domains |
| `corepanel account create` | Create a new hosting account and its primary domain |
| `corepanel account update` | Change an account's limits, owner details, hosting package or PHP version |
| `corepanel account suspend` / `unsuspend` | Suspend a hosting account across every plane, and lift it again |
| `corepanel account transfer` | Move a hosting account to another reseller |
| `corepanel account panel` | Show or change whether an account may sign in to the client panel |
| `corepanel account import` | Import a cPanel account (or preview with `--dry-run`) |
| `corepanel account backup` | Create a full local backup of a hosting account |
| `corepanel account restore` | Restore a hosting account from a cpbackup archive |
| `corepanel account backup-schedule` | Manage recurring backup schedules and retention |
| `corepanel domain` | List, add and remove an account's secondary (alias/addon) domains |
| `corepanel subdomain` | List, add, tune and remove an account's subdomains |
| `corepanel app` | Run and publish an account's persistent applications |
| `corepanel cron` | Manage an account's cron jobs |
| `corepanel wp` | Install, update, harden and roll back WordPress sites |
| `corepanel seller` | Create, edit, suspend and delete the resellers this server hosts for |
| `corepanel admin` | Create, list and delete the people who sign in to the panel |
| `corepanel mail sync` | Resynchronize the Postfix lookup database |
| `corepanel mail list` | List the mailboxes of a domain |
| `corepanel mail delete` | Delete a mailbox and its Maildir |
| `corepanel mail quota` | Set a mailbox storage quota |
| `corepanel mail suspend` / `activate` | Suspend or reactivate a mailbox |
| `corepanel mail forwarder` | Manage mail forwarders (aliases) |
| `corepanel mail catchall` | Manage a domain catch-all |
| `corepanel mail redirect` | Redirect all mail for a domain to another domain |
| `corepanel mail vacation` | Manage a mailbox vacation autoresponder |
| `corepanel mail filter` | Manage mailbox Sieve filter rules |
| `corepanel mail spam` | Manage spam thresholds and allow/deny lists |
| `corepanel mail deliverability` | Check whether mail leaving this server is accepted |
| `corepanel edition websites` | Show how many websites the edition allows and how many are in use |
| `corepanel license` | Buy, activate, inspect, refresh or release this server’s license |
| `corepanel telemetry` | Turn this server’s daily check-in on or off |
| `corepanel ssl` | Install and manage certificates bought from a commercial CA |
| `corepanel version` | Print the CLI build version |
Run `corepanel [command] --help` at any time to see the available subcommands and flags.
## `corepanel system init`
Interactive wizard that initializes a fresh CorePanel server. It detects the server identity,
sets the panel domain (the FQDN used to access CorePanel), validates DNS, sets the administrator
identity, and generates (or reuses) the administrator password.
```bash
corepanel system init

The wizard walks through six steps: detecting the public IP, prompting for the panel domain, checking DNS resolution against the server IP (advisory, non-blocking), prompting for the admin email, preparing the admin password, and applying the configuration. The panel domain is a DNS name pointing at the server — not the machine’s OS hostname, which is only offered as the default when it happens to be a fully qualified domain name.

You do not need a domain to finish the setup. Leave the prompt blank — or run unattended on a server whose hostname is not an FQDN, which is what cloud images and provisioning systems give you (almalinux-2gb-nyc1-01) — and CorePanel is served on https://<server-ip> with a self-signed certificate. The panel then asks for the domain on first login, together with the nameservers.

Set it whenever you are ready, from the panel or by re-running this command with --domain. Once the name resolves to this server, a trusted certificate is issued automatically.

A domain passed explicitly with --domain is still validated: a value that is not an FQDN is an error, not a silent fallback to the IP.

Nameservers and company details are not asked here. The panel’s initial configuration wizard collects them on first login and keeps opening until they are set — see Initial Server Setup.

FlagDescription
--domain <fqdn>Panel domain — the FQDN used to access CorePanel (e.g. panel.example.com). Optional; without it the panel is served on the server’s IP
--admin-name <name>Administrator display name (default Administrator)
--admin-email <email>Administrator email for notifications and Let’s Encrypt
--admin-password <password>Administrator password (generated if omitted)
--no-interactiveSkip all prompts; use flags and defaults for scripted provisioning

--hostname <fqdn> is retained as a deprecated alias for --domain.

For automated installs, pass the values as flags and disable prompts:

Terminal window
corepanel system init \
--domain panel.example.com \
--admin-email admin@example.com \
--no-interactive

When there is no domain yet — a cloud image, a marketplace image, a CI run — omit --domain entirely and the install still completes:

Terminal window
corepanel system init \
--admin-email admin@example.com \
--no-interactive

When --admin-password is omitted, a strong 16-character password is generated and stored at /root/.corepanel_password (mode 0600). If that file already contains a password, it is reused instead of generating a new one.

Displays the status of all CorePanel services — corepanel-api, corepanel-auth, corepanel-core, and corepanel-sys — in a table. For each service it shows the systemd active state, the detailed sub-state (running, exited, failed, …), and the availability of its sockets.

Terminal window
corepanel status
+----------------+--------+---------+---------------------------------+
| Service | Active | Status | Sockets |
+----------------+--------+---------+---------------------------------+
| corepanel-api | active | running | ● 127.0.0.1:16087 |
| corepanel-auth | active | running | ● corepanel-ftp-auth.sock |
| | | | ● corepanel-mail-auth.sock |
| | | | ● ftp-getcert.sock |
| corepanel-core | active | running | ● corepanel-core.sock |
| corepanel-sys | active | running | ● corepanel-sys.sock |
+----------------+--------+---------+---------------------------------+

Socket indicators: means the socket (or TCP port) is available, means it is unavailable. Use this command as a first check when the panel or a subsystem is not responding.

Manages disk quotas: the server’s quota state, what each account is using, and the per-account limits.

Usage is measured by the kernel’s per-uid quota accounting, so these commands always report the state of that accounting alongside the figures. With accounting off there is nothing to measure, and a usage of zero would read as an empty account rather than as “not measured”. Limits are stored either way and take effect when quotas are enabled.

Turns disk quotas on for this server: enables the kernel’s per-account accounting and enforcement, applies every account’s stored limit, and measures what each one is using.

Until this is run, CorePanel stores the quota an account was sold but nothing measures or enforces it. Quota accounting is a property of how the filesystem was mounted, which is why enabling it is a deliberate act and never something a package upgrade does on its own.

What the command can finish depends on the filesystem:

FilesystemWhat happensReboot
ext2/3/4the option is added to /etc/fstab and the filesystem is remountedno
XFS, separate /homeunmounted and mounted again, when nothing holds it openonly if busy
XFS on /configured on the kernel command line with grubbyyes

The output distinguishes configured from active. On a root XFS — the usual server layout — nothing is measured or limited until the machine reboots, and the command says so instead of reporting success.

Terminal window
corepanel quota enable [--json]
Disk quotas are CONFIGURED but NOT yet active — this server must be rebooted.
disk quotas are configured on the kernel command line and start at the next reboot
Configured in: the kernel command line (grubby)
grubby --update-kernel=ALL --args=rootflags=uquota
Usage figures stay unavailable and no limit is enforced until this server reboots.

Shows the quota state of the filesystem backing the account home directories.

The three layers are reported separately because they fail separately:

  • Accounting — is the kernel counting bytes per account at all.
  • Enforcement — is it refusing writes past a limit.
  • Reboot required — quotas are configured but the filesystem has not been mounted with them yet. On a root filesystem this is the normal state between enabling quotas and restarting the server.
Terminal window
corepanel quota status [--json]
Disk quotas: ACTIVE (accounting + enforcement)
Filesystem : /dev/sda4 on / (xfs)
Separate /home : no
Accounting : on
Enforcement : on
App data counted: yes

App data counted reports whether /var/opt/userapps shares the quota filesystem. When /home is a separate partition it does not, and application data is therefore excluded from every usage figure — the tidier-looking layout is the one that measures less.

Lists per-account disk usage, largest first.

Disk and database sizes are shown in separate columns and are deliberately not added together: only the disk figure is accounted by the kernel and enforced against, because the MySQL data directory belongs to the mysql user and no per-uid quota can see it.

Terminal window
corepanel quota list [--search <domain>] [--json]

When quotas are inactive the command prints a warning before the table: the figures are then whatever was last measured, not current usage.

Shows where an account’s disk usage is, not just how much it is.

Terminal window
corepanel quota inspect <account> [--deep] [--json]

A disk quota counts every file the account’s Linux user owns on the filesystem — not the contents of its home directory. Usually those are the same thing. When they are not, the account sees a figure it cannot explain and cannot reduce, because the files are somewhere it cannot even list.

Some of that is by design, and the report says so:

LocationWhat is there
/opt/userapps/<user>A published application’s program and releases. Root owns the code — deliberately out of reach of the account’s own FTP, so it cannot replace the binary systemd is executing — so this normally contributes nothing to the quota; it is checked because anything account-owned that lands here would
/var/opt/userapps/<user>That application’s data, which the account owns and survives every deploy
/var/spool/cron/<user>The account’s crontab
/var/spool/mail/<user>The system mail spool
/tmp, /var/tmpNot by design: files left there still count against the quota

The full picture of what a quota counts is in Quotas and Usage.

The rest is the reason this command exists: files that ended up owned by an account by accident. The account cannot delete them, so a quota applied on top of them puts it permanently over its limit with nothing it can do.

Anything the scan cannot place is reported as unexplained. --deep sweeps the entire filesystem to locate it and groups the results by directory. That takes minutes on a busy server, which is why it is not the default.

Disk usage of example.com (user example, uid 1007)
Counted by the kernel : 1.2 GiB
Inside the home : 840.0 MiB (/home/example)
Outside the home : 384.0 MiB
+----------------------------+-----------+-------+-----------+---------------------------+
| LOCATION | SIZE | ENTRIES | BY DESIGN | WHAT IT IS |
+----------------------------+-----------+-------+-----------+---------------------------+
| /home/example | 840.0 MiB | 9214 | yes | home directory |
| /opt/userapps/example | 320.0 MiB | 412 | yes | published application ... |
| /var/opt/userapps/example | 64.0 MiB | 88 | yes | application data ... |
+----------------------------+-----------+-------+-----------+---------------------------+

Sets an account’s disk quota in MB. A quota of 0 removes the limit.

Terminal window
corepanel quota set <account> <MB>
Terminal window
# 20 GB for account 12, and no limit at all for dom-7
corepanel quota set 12 20480
corepanel quota set dom-7 0

The kernel gets a hard limit slightly above the quota — 5 %, at least 50 MB, never more than double it. That margin is the grace window, and it is not generosity: at exactly 100 % the failures are indirect and ugly. PHP cannot write its session files, Dovecot cannot deliver and mail bounces, cron jobs fail half-way. The account goes over its stated quota, and is warned, before writes are refused.

If the account is already above the new quota, the command says so — its grace period starts immediately. On a server where quotas are not active the limit is still stored, and applies as soon as they are enabled.

Pushes every account’s stored quota to the kernel and reports which accounts are already over theirs.

Terminal window
corepanel quota apply [--dry-run]

This is what a server needs when its accounts predate disk quotas: the limits are in CorePanel’s database, but the kernel has never been told about them. It is also the repair path for a limit whose push failed when the account was created.

Run --dry-run first. Before enforcement is switched on, the number that matters is how many accounts are already over their limit — those are exactly the ones whose writes will start failing.

Forces an immediate refresh of the stored usage instead of waiting for the hourly job.

Terminal window
corepanel quota refresh

When quotas are inactive nothing is measured and the stored figures are left untouched. They are never zeroed — “cannot measure” must not be recorded as “uses nothing”.

Lists hosting accounts with their id, username and primary domain. Any of the three works as the <account> argument of every other command (see Naming an account), so this is a way to browse the server rather than a step you have to run before doing anything else.

Terminal window
corepanel account list
FlagDescription
--search <term>Filter accounts (case-insensitive substring over the names an account is reachable by: username, primary domain, addon domains and subdomains)
--jsonOutput the account list as JSON for scripting

The default table lists ID, Domain, User, Seller, Email (used/limit), DBs (used/limit), Quota MB (used/limit), and Package; a limit of means unlimited. The command pages through core transparently, so every matching account is returned in one call.

Terminal window
# List every hosting account
corepanel account list
# Find an account when you only remember part of the name
corepanel account list --search example
# Scripting: extract the account id for a domain
corepanel account list --search example.com --json | jq '.[0].accountId'

Creates a new hosting account together with its primary domain. The domain is passed as a positional argument; core provisions the Linux user, web vhost, DNS, and mail records.

Terminal window
corepanel account create example.com

The account password can be supplied in three ways, listed from most to least secure:

  1. --password-stdin — read the password from standard input. Best for automation, since the secret never appears in the shell history or the process list.
  2. Interactive prompt — when no password flag is given and the session is attached to a terminal, the CLI prompts for the password with hidden input.
  3. --password <value> — pass it directly. Convenient but visible in the shell history and in ps.

The password must meet core’s strength requirements: at least 12 characters including lower-case, upper-case, a digit and a special character.

FlagDescription
--password <value>Account password (prefer --password-stdin or the prompt)
--password-stdinRead the account password from standard input
--username <name>Linux username for the account. Defaults to a name derived from the domain. Lowercase letters and digits only, not starting with a digit, at most 16 characters; a name already taken by an account, an administrator or a system user is refused
--owner-name <name>Owner full name
--owner-email <email>Owner email address
--description <text>Free-form account description
--quota <mb>Disk quota in MB (0 = unlimited)
--max-emails <n>Maximum number of mailboxes (0 = unlimited)
--max-databases <n>Maximum number of MySQL databases (0 = unlimited)
--max-ftp <n>Maximum number of FTP accounts (0 = unlimited)
--max-apps <n>Applications the account may run. 0 = none, not unlimited, and there is no unlimited value — an account created without this flag cannot run applications
--plan <name>Hosting plan name to associate with the account
--php <version>PHP version to provision (e.g. 8.3). Defaults to the highest version installed on the server.
Terminal window
# Interactive password prompt, default (highest) PHP version
corepanel account create example.com
# Scripted: read the password from stdin, pin PHP 8.3 and a 5 GB quota
printf '%s' "$ACCOUNT_PASSWORD" | \
corepanel account create example.com --password-stdin --php 8.3 --quota 5120

On success the command prints the account ID, Linux username, domain, and home directory.

Changes an existing account’s limits, owner details, hosting package or PHP version (core.UpdateAccount). The account is passed as a positional argument — its username, any domain it owns, or the numeric id from corepanel account list; the dom-<id> form is accepted too.

Terminal window
corepanel account update <account> [flags]

Only the flags you pass are changed. Every other limit is left exactly as it is. Since 0 means unlimited, omitting a flag is not the same as passing it with 0: a limit you do not mention is untouched, while --max-emails 0 grants unlimited mailboxes.

FlagDescription
--owner-name <name>Owner full name
--owner-email <email>Owner email address
--description <text>Free-form account description
--quota <mb>Disk quota in MB (0 = unlimited)
--max-bandwidth <mb>Monthly bandwidth allowance in MB (0 = unlimited)
--max-emails <n>Maximum number of mailboxes (0 = unlimited)
--max-databases <n>Maximum number of MySQL databases (0 = unlimited)
--max-ftp <n>Maximum number of FTP accounts (0 = unlimited)
--max-aliases <n>Maximum number of domain aliases (0 = unlimited)
--max-subdomains <n>Maximum number of subdomains (0 = unlimited)
--max-addon <n>Maximum number of addon domains (0 = unlimited)
--max-apps <n>Applications the account may run. 0 = none, not unlimited — the one limit that reads the other way round. Lowering it below the applications already running is refused
--package <id>Apply a hosting package; its limits become the account’s. Any limit flag given alongside it still wins. 0 detaches the account without changing a limit.
--php <version>Move the account to another installed PHP version (e.g. 8.3). Not a limit — see below
--forceAllow a disk quota below current usage, leaving the account over quota
--keep-mailKeep receiving mail while the account is suspended — on by default; --keep-mail=false bounces instead (sending is cut either way)
Terminal window
# Upgrade: more mailboxes and more disk
corepanel account update 7 --max-emails 100 --quota 20480
# Move the account onto a package, then override one of its limits
corepanel account update 7 --package 3 --max-subdomains 50
# Plan downgrade below what the account currently stores
corepanel account update 7 --quota 1024 --force
# Move a customer off an end-of-life PHP release
corepanel account update 7 --php 8.4

An account created from a package stays linked to it after a manual edit and is reported as customized — see Editing an Account.

corepanel account suspend / corepanel account unsuspend

Section titled “corepanel account suspend / corepanel account unsuspend”

Suspends a hosting account across every plane it touches, and lifts it again (core.SuspendAccount / core.UnsuspendAccount).

corepanel account suspend <account> [--reason <text>]
corepanel account unsuspend <account>

<account> is the username, any domain the account owns, or the numeric id. A name that matches more than one account is refused rather than guessed.

PlaneWhile suspended
WebsitesEvery domain of the account serves a 403 suspension page
ApplicationsStopped; ones already stopped by their owner stay stopped
CronJobs stop running, definitions untouched
FTPLogin refused
Mail — sendingRejected at MAIL FROM with 550
Mail — receivingRejected with 550 unless the account keeps its mail
Mail — readingWorks normally, webmail included
DataUntouched — nothing is deleted

Flags:

FlagMeaning
--reason <text>Recorded for the operator and shown in the panel. Never published: the suspension page says nothing about why

suspend requires CorePanel Business. unsuspend is not gated on any edition, so a licence that lapses while an account is suspended can never leave a customer’s sites dark with no way back.

# Suspend by domain, with a note for your own record
corepanel account suspend example.com --reason "invoice 4471 unpaid"
# Lift it again by account id
corepanel account unsuspend 7

corepanel account list shows a Status column, and --json carries suspended, suspendedAt and suspendReason. See Suspending an Account for why a suspended customer can still read their mail.

Moves a hosting account to another reseller (core.TransferAccount).

corepanel account transfer <account> --to <reseller>

<account> is the username, any domain the account owns, or the numeric id; <reseller> is a seller id or name. Ownership is the only thing that changes — nothing on the server knows which reseller owns an account, so no service is restarted and no site goes down. The account keeps its own limits, its own password and its own suspension state.

FlagMeaning
--to <reseller>The reseller that will own the account, by id or name (required)

Two things move with it, and the command reports both:

  • the account’s hosting package is cleared, because a package belongs to a reseller and the account is leaving that catalog. Limits are untouched — put the account on one of the new owner’s packages with corepanel account update <account> --package <id>;
  • past notifications stay with the reseller that owned the account when they happened.

Refused when the destination reseller is suspended, and when the account is currently down because its own reseller is suspended — lift that account first with corepanel account unsuspend, which is you deciding it comes back online. An account suspended in its own right moves normally and stays suspended.

# Hand a customer to another reseller
corepanel account transfer alice --to "Acme Hosting"
# Take a reseller's customers over before removing the organisation
corepanel account transfer example.com --to 1
corepanel seller delete "Acme Hosting"

See Editing an Account for the whole picture.

Shows or changes whether a hosting account may sign in to the client panel (core.GetAccountPanelAccess / core.SetAccountPanelAccess).

corepanel account panel status <account>
corepanel account panel enable <account>
corepanel account panel disable <account>

<account> is the username, any domain the account owns, or the numeric id.

There is no panel user to create and no panel password to set. The account signs in with the username and password it already has — the same ones that open FTP and SSH. These commands only decide whether that credential also opens the panel. To change the password itself, use the account’s password controls; there is one place to do it, not two.

status prints four separate things, because “my customer cannot log in” is usually not the flag:

LineWhat it means
panel accessThis account’s flag — what enable and disable set
licenceWhether the client panel exists on this server at all (CorePanel Business)
suspendedA suspended account is refused, with its own message
passwordAn imported or weak credential must be changed before anything else
can sign inThe four above, resolved in the order the server enforces them
$ corepanel account panel status example.com
account: acmecorp (example.com) (id 7)
panel access: enabled
licence: the client panel is available on this server
suspended: no
password: must be changed at next sign-in (imported from another panel, never checked against this server's policy)
last sign-in: never
can sign in: yes, but only to change its password

disable revokes self-service, not hosting. Websites, mail, FTP and SSH are untouched, and the same password keeps working everywhere it worked before. Sessions that are already open are refused on their next request: the server re-reads this flag on every call rather than trusting the session token.

Both commands are idempotent, and neither is gated by the licence. Revoking access has to keep working on a server whose licence has lapsed, and gating enable would leave you able to turn access off and unable to turn it back on. The licence is enforced at the login itself.

Imports a cPanel account into CorePanel from a cpmove-<user>.tar.gz / backup archive — or from an already-extracted account directory. With --dry-run the archive is parsed server-side and the plan is printed (core.PreviewImport, read-only); without it, the import runs (core.ImportAccount).

Terminal window
# Preview (read-only)
corepanel account import <archive> --dry-run
# Run the import
corepanel account import <archive> [--package <id>] [--preserve-uid]
FlagDescription
--dry-runParse and validate the archive, print the plan, and touch nothing
--seller <id>Target seller id (default: primary seller)
--package <id>Target hosting package id; its limits drive the account
--preserve-uidPreserve the source UID/GID via useradd -u/-g
--on-conflict abort|skipPolicy when the linux user or primary domain already exists (default abort)
--report <path>Write the JSON import report to this path

The <archive> path is resolved on the machine running corepanel-core (the archive must already be staged on the server) and is sent as an absolute path.

Dry-run reports:

  • the detected source panel and version;
  • the account identity (Linux user, UID/GID, plan, contact) — and whether the source has it suspended, which is marked beside the name because such an account is imported suspended;
  • resource counts — domains, mailboxes, forwarders, databases, DB users, DNS records, FTP accounts, cron jobs, certificates;
  • credential compatibility — which passwords carry across verbatim versus which need a reset (cPanel $6$/$5$/$1$, bcrypt and $y$ yescrypt hashes are preserved);
  • conflicts against existing CorePanel accounts and domains;
  • every parse warning (anything skipped or unsupported — nothing is dropped silently).

A mailbox or FTP login the source had locked arrives closed (inactive), keeping the password stored behind the lock marker. It is not counted among the credentials needing a reset: there is nothing to reset, and reopening it is a decision rather than a chore.

An account suspended on the source arrives suspended, keeping the source’s own suspension date: its sites serve the suspension page and its logins stay closed until an administrator lifts it. A site that does not answer after importing one of these is the correct outcome, not a failed import.

Real import creates the system account preserving the source username, UID/GID (with --preserve-uid) and password hash where the scheme is compatible, then provisions the CorePanel account, the primary domain (vhost + managed DNS zone), the FTP accounts and the primary domain’s mailboxes with their credentials carried over verbatim. A credential whose scheme cannot be preserved gets a random password and is flagged for a reset (never surfaced in plaintext). It also restores the account’s heavy data: the home directory (site files and Maildir messages), the MySQL databases (schema + data) and their users (source password hashes preserved), the crontab (imported as panel-managed cron jobs), and the primary domain’s TLS certificate (installed verbatim when not self-signed). Addon/subdomain provisioning, source DNS-record import and mail forwarders remain later phases and are listed as deferred in the report. The job outcome is persisted and can be polled via core.GetImportJob or listed via core.ListImportJobs.

Examples:

Terminal window
# Preview a staged cpmove archive without importing
corepanel account import /var/lib/corepanel/import/cpmove-example.tar.gz --dry-run
# Preview an already-extracted account directory
corepanel account import /var/lib/corepanel/import/job-42/cpmove-example --dry-run
# Run the import into package 3, preserving the original UID/GID
corepanel account import /var/lib/corepanel/import/cpmove-example.tar.gz \
--package 3 --preserve-uid --report /root/import-example.json

Creates a full local backup of a hosting account. Every MySQL database is dumped with mysqldump --single-transaction, each database user’s credential is preserved, and the home directory (files + Maildir) is archived into a self-describing cpbackup-<user>-<timestamp>.tar archive under /var/lib/corepanel/backups/<user>/.

Local backups and their schedules are available in every edition; copying an archive to a remote destination (S3-compatible storage or SFTP) is a Pro/Business feature.

Terminal window
corepanel account backup <user> [--wait] [--dest <name|id>]

The backup runs asynchronously server-side and prints a job id. Pass --wait to block until it finishes and print the resulting archive path and status.

Terminal window
# Start a backup and return immediately with the job id
corepanel account backup pxdemo
# Start a backup and block until it completes
corepanel account backup pxdemo --wait
# Back up and push the archive off this server
corepanel account backup pxdemo --dest offsite --wait
# Show the status and report of a backup job
corepanel account backup-status 7

The local copy is kept even when a destination is configured, so a destination that is unreachable degrades the job to partial with the reason in its report — it never costs you the backup.

Restores a hosting account from a cpbackup-<user>-<timestamp>.tar archive produced by corepanel account backup. Recreates the system account (preserving UID/GID and the password hash where compatible), the CorePanel account row and primary domain, every secondary domain (aliases and addons) and subdomain — each with its vhost and managed DNS zone — the home directory (files + Maildir), MySQL databases + users with preserved credentials, FTP accounts, and every mailbox login (primary and addon domains) with its password hash carried over verbatim.

Terminal window
corepanel account restore <archive> [--on-conflict abort|skip] [--dry-run] [--wait]
corepanel account restore --dest <name|id> --remote-key <key> [--on-conflict abort|skip] [--dry-run] [--wait]

Use --dry-run to preview what would be restored without touching the system, and --wait to block until the restore finishes and print the per-resource report.

With --dest and --remote-key the archive is pulled from a remote destination instead of read from local disk: it is downloaded into a private staging directory, restored, and the download removed — the copy at the destination is left alone. This path is never gated by edition, because pulling a backup back is recovery.

Terminal window
# Preview a restore
corepanel account restore /var/lib/corepanel/backups/cph2/cpbackup-cph2-20260721T090349Z.tar --dry-run --wait
# Restore, blocking until done
corepanel account restore /var/lib/corepanel/backups/cph2/cpbackup-cph2-20260721T090349Z.tar --wait

Manages recurring backup schedules and their retention. A schedule targets one account (--user) or every account (--all), fires on a cron expression or a daily/weekly/monthly preset, and prunes only its own old archives. CorePanel runs an in-process scheduler that polls every minute, so no OS crontab entry is created. Retention never deletes a manual backup and always keeps at least the most recent scheduled archive per account. run starts the run in the background and returns immediately; track progress with the schedule’s last status or corepanel account backup-status.

Terminal window
corepanel account backup-schedule list
corepanel account backup-schedule create (--user <user> | --all) [--cron "m h dom mon dow" | --frequency daily|weekly|monthly] [--keep N] [--days N] [--name <label>] [--disabled]
corepanel account backup-schedule enable <id>
corepanel account backup-schedule disable <id>
corepanel account backup-schedule run <id>
corepanel account backup-schedule delete <id>

create flags:

  • --user <linux-user> — Back up a single account (mutually exclusive with --all).
  • --all — Back up every account on the server.
  • --cron "m h dom mon dow" — Explicit 5-field cron expression (wins over --frequency).
  • --frequency <daily|weekly|monthly> — Preset (daily 02:00, weekly Sun 02:00, monthly 1st 02:00).
  • --keep N — Keep the N most recent scheduled archives per account (0 = unlimited).
  • --days N — Delete scheduled archives older than N days (0 = disabled).
  • --dest <name|id> — Push this schedule’s archives to a remote destination (Pro/Business). Retention then prunes the remote copies as well as the local ones.
  • --name <label> — Optional human label.
  • --disabled — Create the schedule disabled. Before 1.6.14 this had no effect and the schedule ran at its first due time.
Terminal window
# Nightly backup of one account, keep the last 7 archives
corepanel account backup-schedule create --user pxdemo --frequency daily --keep 7 --name "pxdemo nightly"
# Weekly server-wide backup with a custom cron, delete archives older than 30 days
corepanel account backup-schedule create --all --cron "0 3 * * 0" --days 30
# List schedules, run one now, then disable it
corepanel account backup-schedule list
corepanel account backup-schedule run 3
corepanel account backup-schedule disable 3

Manages where finished backup archives are copied. Without a destination every archive stays on the same disk as the server it protects — the one failure a backup exists to survive. Two drivers are available: s3 for any S3-compatible service (AWS, MinIO, Cloudflare R2, Backblaze B2, Wasabi, and Google Cloud Storage through its S3 endpoint) and sftp for any SSH server.

Configuring and using a destination requires CorePanel Pro or Business. Listing, disabling and deleting one — and restoring an archive back from one — work on every edition, so a lapsed licence never leaves you with configuration you cannot manage or a backup you cannot recover.

Terminal window
corepanel backup-destination list
corepanel backup-destination add <name> --type s3|sftp [driver flags]
corepanel backup-destination test <name|id>
corepanel backup-destination enable <name|id>
corepanel backup-destination disable <name|id>
corepanel backup-destination rm <name|id>
corepanel backup-destination archives <name|id>
FlagDescription
--endpoint <host>Service host without a scheme (s3.eu-west-1.amazonaws.com)
--region <region>Region
--bucket <bucket>Bucket name
--prefix <folder>Folder inside the bucket, so several servers can share it
--path-styleAddress the bucket as <endpoint>/<bucket> — MinIO and most self-hosted gateways need this; AWS does not
--insecurePlain HTTP; only sensible on a private network
--access-key, --secret-keyCredentials
FlagDescription
--host <host>, --port <n>Server address (port defaults to 22)
--user <name>Username
--path </abs/dir>Absolute remote directory archives are written into; it must already exist
--key-file <path>Private key, read from disk locally so it never reaches a shell history
--passphrase <text>Passphrase for an encrypted private key
--password <password>Password authentication
--host-key <line>Pin the server key in authorized_keys form. Left empty, the key seen on the first successful connection is adopted and every later connection is verified against it
Terminal window
# S3-compatible destination, then prove it really accepts uploads
corepanel backup-destination add offsite --type s3 \
--endpoint s3.eu-west-1.amazonaws.com --region eu-west-1 --bucket cp-backups \
--access-key AKIA... --secret-key ...
corepanel backup-destination test offsite
# A MinIO on the LAN
corepanel backup-destination add minio --type s3 \
--endpoint 10.0.0.5:9000 --bucket backups --path-style --insecure \
--access-key minioadmin --secret-key ...
# An SFTP box, authenticating with a key
corepanel backup-destination add nas --type sftp \
--host nas.example.net --user backups --path /srv/backups \
--key-file ~/.ssh/id_ed25519

test is worth running every time you add or change a destination: it uploads a small probe object, lists it and removes it again. A plain connection or login check would report success for a destination that still fails on the first real push.

A destination is also the shortest path to rebuilding an account somewhere else. On the new server, add the same destination, find the archive and restore it:

Terminal window
corepanel backup-destination add offsite --type s3 ...
corepanel backup-destination archives offsite
corepanel account restore --dest offsite --remote-key cpbackup-pxdemo-20260802T031500Z-j42.tar --wait

The archive is self-contained, and the DNS zone is regenerated by the new server, so the records point at it rather than at the old one.

Manages the resellers this server hosts for. Aliases: sellers, reseller, resellers.

A reseller owns accounts, and which accounts it owns is what its administrators may see and touch: an administrator whose seller is a reseller reaches that reseller’s accounts and nothing else on the server.

The organisation that owns the server is a seller too, flagged as the primary vendor in the listing. It is not a reseller — its administrators see everything and its limits are never consulted — so it can be neither edited nor deleted here.

Every subcommand takes a reseller by id or by name, matched without regard to case.

Terminal window
corepanel seller list
corepanel seller show "Acme Hosting"

The Accounts and Disk columns read used / ceiling, so a glance answers the question you actually have — whether the next account will go through. A ceiling of 0 prints as unlimited.

For a reseller that came across from a cPanel migration, corepanel seller show also prints the privilege set the source panel granted them, in cPanel’s own names:

Imported from cPanel, where this reseller was granted 7 privileges and no others:
acct-summary, basic-system-info, basic-whm-functions, cpanel-api, list-pkgs, manage-dns-records, ssl-info
CorePanel does not grant privileges one at a time: this reseller's administrators
can do anything within its own accounts, and nothing outside them.

That set is a record of where the reseller came from and is never enforced here — see Your resellers come across with their limits. It is printed only where the source’s grant was a restriction: a reseller cPanel granted everything had nothing withheld, so there is nothing to say about it.

Creates a reseller. The name is what the panel shows and has to be free; it is compared without regard to case, because that is also how an import resolves a source panel’s reseller to a seller here.

Requires CorePanel Business. The entitlement gates creation — a new reseller, and an administrator under one. A reseller that already exists keeps working on any edition: you can still rename it, change its ceilings, lift a suspension and read everything about it, because a licence lapsing must never leave you unable to undo something.

FlagDescription
--email <address>Address the reseller is notified at (required)
--max-accounts <n>How many accounts the reseller may create in total (0 = unlimited)
--max-quota <mb>Total disk the reseller may sell, in MB (0 = unlimited)
--oversellingLet the reseller exceed its total disk (the account count stays enforced)
Terminal window
corepanel seller create "Acme Hosting" --email ops@acme.test \
--max-accounts 20 --max-quota 102400

The two ceilings are totals across every account the reseller owns, and they are enforced — see what the ceilings do below.

Changes a reseller’s name, contact address or limits. Aliases: limits, set.

Only the flags you pass are written, and everything else is left exactly as it is — so tightening one ceiling cannot revert a rename that happened in between. Flags: --name, --email, --max-accounts, --max-quota, --overselling.

Terminal window
# Tighten one ceiling and touch nothing else
corepanel seller limits "Acme Hosting" --max-accounts 10

Lowering a ceiling below what the reseller already holds is allowed and takes nothing away: the accounts that exist keep running, and it is the next one that is refused.

The primary vendor is refused.

--max-accounts and --max-quota are totals across every account the reseller owns, and 0 means unlimited. They are refusals, not decorations:

  • Creating an account for the reseller — from the panel, the CLI, WHMCS or an import.
  • Transferring an account into it, which raises its totals by exactly as much.
  • Raising a customer’s disk quota, or applying a hosting package that raises it.

The refusal names the wall, what it is and what has been used of it, so the reseller knows whether to delete an account or to call you.

Disk counts what was assigned, not what is used. Ten accounts of 10 GB are 100 GB whether or not anything has been uploaded — that is what selling 100 GB means — and a server whose filesystem has no active quotas measures nothing at all, which is exactly where a limit computed from measured bytes would quietly become no limit.

A reseller with a disk ceiling cannot be given an account with no quota. An unlimited account adds nothing to the total, so one of them would exempt the reseller from its own ceiling for good. Where there is no ceiling, or where the reseller may oversell, unlimited means what it always meant. Accounts that already have no quota — every reseller arrives from a cPanel import with some — are counted as accounts and shown separately under the disk figure, so a total that looks too small explains itself.

--overselling lifts the disk ceiling only. The account count is not an overselling switch and stays enforced, which is the same split cPanel makes: there, overselling is a property of diskspace and bandwidth and never of the account limit.

A ceiling belongs to the reseller, not to whoever is calling. Creating an account for a reseller that is full is refused for you as well — that is what stops a billing system provisioning past a limit through your own credential. Raise the ceiling first.

Stops a reseller trading. Requires CorePanel Business.

Two things happen, and the second is the one to be sure about before you press it:

  • The reseller stops trading. Its administrators are refused at the login form, and on every request of a session that is already open — a session minted an hour ago does not outlive the decision by the rest of its life.
  • Every account it owns is suspended. The same suspension a single account gets: sites serve a suspension page, applications stop, cron jobs stop running, FTP is refused and inbound mail is rejected. Nothing is deleted.

An account that was already suspended in its own right — one of the reseller’s customers who stopped paying weeks ago — is left exactly as it is, keeps its own reason, and is not put back online when you lift the reseller’s suspension.

FlagDescription
--reason <text>Why, in your words. Recorded on the reseller
--yesDo not ask for confirmation

--reason is for you. It is shown on corepanel seller show and never reaches the reseller’s customers: their accounts carry a fixed sentence saying only that the reseller who owns them is suspended. A note about somebody’s unpaid invoice is not something to mail to forty unrelated people.

While it is suspended the reseller is also given no new accounts — not from the CLI, not from an import, and not from WHMCS, none of which go through one of its sessions.

The command asks for confirmation, and refuses to run at all without a terminal unless you pass --yes. The organisation that owns this server cannot be suspended.

Terminal window
corepanel seller suspend "Acme Hosting" --reason "unpaid January invoice"

The output names every account, one group at a time — suspended, already down, and any that could not be taken down. Partial success is a real outcome: the reseller stops trading even when one of its accounts would not go offline, and you have to be told which one.

Lets a reseller trade again. Alias: resume.

Its administrators can sign in, and the accounts this suspension took down are restored. Only those.

No confirmation, and no edition gate — a licence that lapsed while a reseller was suspended must never be the reason its customers cannot come back.

Terminal window
corepanel seller unsuspend "Acme Hosting"

Deletes a reseller. Alias: rm.

Refused while it still owns accounts, administrators, packages or live API tokens, and the refusal names what is in the way. There is no cascade: cancelling a reseller contract is not a reason to delete customer hosting accounts — move them first with corepanel account transfer, one at a time, and then delete the empty organisation.

Manages the people who can sign in to the panel. Aliases: admins, administrator, administrators.

An administrator is not a reseller. An administrator is a person with a login; a reseller is an organisation that owns hosting accounts, and it is managed with corepanel seller above. Every administrator belongs to exactly one seller, and that is what decides what they see:

  • an administrator of the organisation that owns the server sees everything;
  • an administrator of a reseller sees only that reseller’s accounts.

A super administrator additionally may do the server-wide things — licensing, the firewall, PHP versions, the logs. Only a super administrator may create another administrator, and a super administrator may only ever belong to the organisation that owns the server.

Lists administrator accounts and prints their key fields — ID, username, email, seller, and super-admin flag — in a table. Alias: ls.

FlagDescription
--seller <id>Filter by a specific seller (default: 1)
--allInclude administrators from every seller (overrides --seller)
Terminal window
corepanel admin list
corepanel admin list --all
corepanel admin list --seller 42

corepanel list admins is the older spelling of this command and keeps working.

Creates an administrator.

The username is the login. It must be free among administrators and among hosting accounts: one login form answers both, so a name they share would be two different people told apart only by their passwords happening to differ.

FlagDescription
--email <address>Contact address, unique across administrators (required)
--name <name>Display name shown in the panel (defaults to the username)
--password <password>Initial password (generated and printed when omitted)
--seller <id|name>Reseller this administrator belongs to
--superGrant server-wide administration
Terminal window
# A second operator for the server's owner
corepanel admin create jane --email jane@example.com --name "Jane Roe"
# An administrator for a reseller, with a password you chose
corepanel admin create acmeops --email ops@acme.test \
--seller "Acme Hosting" --password 'S0me-Strong!Pass'

Without --password a strong one is generated and printed once. Either way the administrator is asked to replace it the first time they sign in: whoever created the account knows the password, and a credential its holder has not chosen is still the issuer’s. This password is never written to /root/.corepanel_password — that file has exactly one subject, the server owner’s own administrator.

Creating an administrator under a reseller requires CorePanel Business. A second administrator for the server’s own organisation is not a reseller feature and works on every edition.

Giving an imported reseller its first login prints a note

Section titled “Giving an imported reseller its first login prints a note”

It is the one thing a cPanel migration makes wider. On cPanel a reseller holds a named set of privileges — they may create accounts but not terminate them, or touch DNS but not packages. CorePanel has no such setting: a reseller is all-or-nothing within their own accounts. So a reseller who could not terminate an account on the source can terminate one here.

Nothing happens at the migration itself, because the reseller that arrives has no login and can do nothing. It happens the day you run this command:

Note: acmehost was imported from cPanel, where it was granted 7 privileges and no others:
acct-summary, basic-system-info, basic-whm-functions, cpanel-api, list-pkgs,
manage-dns-records, ssl-info. CorePanel does not grant privileges one at a time — this
administrator can do anything within acmehost's own accounts, including whatever the source
withheld, and nothing outside them. Run `corepanel seller show acmehost` to see the full set
again.

The administrator was created: this is a note, not a failure, because widening those privileges may well be what you intend. If it is not, delete the administrator — the reseller goes back to having no login, and its accounts keep working the whole time. The note appears only for a reseller imported from a cPanel that restricted them.

Deletes an administrator, by id or by username. Alias: rm.

It removes the login and nothing else: hosting accounts belong to the seller, not to the person who created them, so nothing a deleted administrator touched is affected. A live session of theirs stops working on its next request.

Refused for the last super administrator — a server nobody can administer is not a state to leave a machine in — and for the administrator behind your own session.

Terminal window
corepanel admin delete acmeops

Triggers a complete resynchronization of all mail domains, mailboxes, and aliases from corepanel-core into corepanel-auth’s Postfix lookup database. Use it to recover from data inconsistencies, initialize the Postfix database after installation, or force a refresh after bulk imports or manual database changes.

Terminal window
corepanel mail sync

Add -v / --verbose to see a breakdown of how many domains, mailboxes, and aliases were synchronized:

Terminal window
corepanel mail sync --verbose
Synchronizing Postfix lookup database...
Synced:
- Domains: 12
- Mailboxes: 148
- Aliases: 37

The rewrite is a single transaction, and it is all-or-nothing on both sides: if any part of the server’s mail data cannot be read, nothing is sent and the lookup database keeps the contents it already had. Postfix reads that database live, so a partial rewrite would mean refusing mail for whatever was left out — worse than the inconsistency the command was run to fix. A failure prints the reason and changes nothing.

Manage mailboxes for a domain. Each command persists the change in corepanel-core and synchronizes it to the Postfix/Dovecot lookup database.

Terminal window
# List the mailboxes of a domain (address, status, quota)
corepanel mail list example.com
# Delete a mailbox: removes the Maildir and revokes the credentials
corepanel mail delete intern@example.com
# Set a mailbox storage quota in MB (0 = unlimited)
corepanel mail quota sales@example.com 1024
# Suspend a mailbox (blocks authentication and delivery), then reactivate it
corepanel mail suspend sales@example.com
corepanel mail activate sales@example.com

suspend closes both halves of a mailbox — delivery and sign-in — and activate opens both. A mailbox listed as sign-in blocked is the third state: it still receives mail and only its login is shut, which is how an imported mailbox arrives when the source panel had locked its password. activate is what reopens it.

A quota is applied by Dovecot at delivery: a message for a full mailbox is refused with 552 5.2.2 Quota exceeded and Postfix bounces it back to the sender. The new limit reaches Dovecot at the mailbox’s next authentication, so nothing needs restarting. Remember that the same messages count against the owning account’s disk quota, which stops delivery for every mailbox at once — see Quotas and Usage.

Manage mail forwarders (aliases). A forwarder maps a source address to one or more destinations; a forwarder that shadows an existing mailbox or forms a mail loop with another alias is rejected, and so is one pointing at an address on a domain this server hosts that cannot receive — no mailbox, no other forwarder, no catch-all, or a suspended mailbox. Mail through such a forwarder would be bounced straight back at its sender.

That check guards new saves; a destination deleted or suspended afterwards still leaves a forwarder pointing at nothing, so list flags those where you will see them:

support@example.com -> old-staff@example.com
!! cannot receive: old-staff@example.com
Terminal window
# List forwarders for a domain
corepanel mail forwarder list example.com
# Create or replace a forwarder to one or more destinations
corepanel mail forwarder set sales@example.com a@example.com b@partner.tld
# Keep a copy in the mailbox and send one on
corepanel mail forwarder set sales@example.com sales@example.com boss@partner.tld
# Delete a forwarder
corepanel mail forwarder delete sales@example.com

Naming the source address among its own destinations is what keeps the mail in its mailbox while forwarding a copy elsewhere. It requires a mailbox at that address whose delivery is open; without one the address resolves to nothing and the forwarder is refused. Deleting the mailbox drops the local copy, and removes the forwarder if that was its only destination.

Manage the domain catch-all: any address that does not match a mailbox or forwarder is routed to the catch-all destinations.

Terminal window
# Show the current catch-all
corepanel mail catchall get example.com
# Route all otherwise-unmatched mail to a destination
corepanel mail catchall set example.com bucket@example.com
# Remove the catch-all
corepanel mail catchall delete example.com

Redirect all mail for a domain to the same local part at another domain (Postfix @source → @target). Useful when a domain is renamed or consolidated: sales@old.com becomes sales@new.com. A domain has a single @domain route, so setting a redirect replaces any catch-all configured on the domain.

Terminal window
# Show the current whole-domain redirect
corepanel mail redirect get old-example.com
# Redirect every address to the same local part at another domain
corepanel mail redirect set old-example.com example.com
# Remove the redirect
corepanel mail redirect delete old-example.com

Manage a mailbox vacation autoresponder (an out-of-office reply). CorePanel renders the reply as a Sieve script that Dovecot runs at delivery, so replies are sent server-side even when no mail client is connected. Replies are rate-limited per sender by --days.

Terminal window
# Show the current autoresponder
corepanel mail vacation get sales@example.com
# Enable an autoresponder (optionally bounded to a date window)
corepanel mail vacation set sales@example.com \
--subject "Out of office" --message "Back on Monday." --days 2 \
--start 2026-07-20 --end 2026-07-27
# Save it without turning it on yet
corepanel mail vacation set sales@example.com --subject "OOO" --message "" --inactive
# Remove the autoresponder
corepanel mail vacation clear sales@example.com

Manage mailbox Sieve filter rules. Each rule matches on message headers and applies actions; CorePanel compiles the rules to a Sieve script executed at delivery.

  • fields: from, to, cc, subject
  • operators: contains, is, matches (the last supports */? wildcards)
  • actions: fileinto (folder), redirect (address), discard, keep, stop
Terminal window
# Print the current rules as JSON
corepanel mail filter get sales@example.com
# Replace the rules from a JSON array
corepanel mail filter set sales@example.com --file rules.json
# Clear all rules
corepanel mail filter clear sales@example.com

Example rules.json:

[
{
"name": "Newsletters to a folder",
"matchAll": true,
"conditions": [{ "field": "subject", "op": "contains", "value": "newsletter" }],
"actions": [{ "type": "fileinto", "arg": "Lists" }, { "type": "stop" }]
}
]

Manage spam thresholds and allow/deny sender lists at a scope, enforced by Rspamd.

  • scope: global (no target), domain <domain>, or mailbox <address>
  • thresholds: reject, add_header, greylist (Rspamd action scores); 0 inherits the Rspamd default, and the effective values must satisfy greylist ≤ add_header ≤ reject
  • lists: allow (never mark matching mail as spam) and deny (always reject it); a pattern is a sender email address or a bare domain

More specific scopes and explicit allow/deny lists take precedence over broader ones.

Terminal window
# Show a mailbox's thresholds and lists
corepanel mail spam show mailbox sales@example.com
# Set per-domain thresholds (0 = inherit default; all-zero clears the policy)
corepanel mail spam threshold domain example.com --reject 12 --add-header 5
# Allow / deny senders
corepanel mail spam allow mailbox sales@example.com friend@partner.tld
corepanel mail spam deny global spammer.example
# Remove a list entry by its id (from `show`)
corepanel mail spam remove domain example.com 7

A message that reaches the add-header threshold is filed in the recipient’s Junk folder; one above the reject threshold never reaches a mailbox. Spam training is automatic from there: moving a message to Junk teaches the filter it is spam, and moving it back out teaches it as legitimate.

Reports whether mail leaving this server is accepted — the server-wide counterpart to the per-domain SPF/DKIM/DMARC records. It checks the name the server announces (HELO) against the panel domain, the reverse DNS of every address it sends from, their blocklist reputation, which address family Postfix prefers, and record coverage across the server’s domains. See Email Deliverability for what each check means.

Terminal window
# Run the checks (results are cached for an hour)
corepanel mail deliverability
# Re-run everything, blocklist queries included
corepanel mail deliverability --refresh
# Set the HELO name to this server's panel domain
corepanel mail deliverability repair-helo
# Choose which address family outbound mail tries first
corepanel mail deliverability prefer ipv4
corepanel mail deliverability prefer any
Email deliverability — checked 2026-08-10T14:38:00Z
HELO: vps-1a0b77bc.vps.ovh.net (panel domain: server.example.com)
[FAIL] helo the HELO name is "vps-1a0b77bc.vps.ovh.net" but this server's domain is "server.example.com"
[FAIL] reputation 2001:db8:305:2100::1:3d3a on Spamhaus Zen: CSS — snowshoe/low-reputation sending detected
https://check.spamhaus.org/query/ip/2001:db8:305:2100::1:3d3a
[warn] ptr the reverse DNS of 203.0.113.25 does not match the HELO name
[ ok ] outbound_preference outbound mail prefers IPv4
[ ok ] dkim the signer is running and all 4 domains publish a DKIM key

Each check prints one of [ ok ], [warn], [FAIL] or [ ? ]. [ ? ] is not a pass: it means the check could not be made at all — most often a blocklist refusing queries that arrive through a public resolver such as 8.8.8.8, or one that is over its daily budget. Point the server at its own resolver and run it again with --refresh.

prefer ipv4 is what CorePanel installs with. Postfix’s own default, any, chooses between IPv4 and IPv6 at random for every destination that publishes both — so on a server whose IPv6 address has no reputation, roughly half of all mail is a coin flip.

Reports how many websites this server may host and how many are already in use.

Terminal window
corepanel edition websites
corepanel edition websites --json
Edition: personal
Websites: 12 / 20
Available: 8

A website is a primary or an addon domain — the two kinds that own a document root and serve a site of their own. Domain aliases (parked domains) and subdomains are not counted: they republish a document root that is already counted, and stay unlimited in every edition. The figure is server-wide, across every account.

The allowance is enforced by the backend on every creation path — account create, domain add --type addon, a cPanel import and a backup restore — so hitting the limit refuses the operation before anything is provisioned. The free Personal edition hosts up to 20 websites; Pro and Business are unlimited (reported as "limit": 0, "unlimited": true).

The exit status is 1 once the allowance is exhausted, so a provisioning script can check for room before it starts:

Terminal window
# Bail out early when the server is full
corepanel edition websites >/dev/null || { echo "no room left"; exit 1; }
# How many are left?
corepanel edition websites --json | jq '.remaining'

See the server-wide website limit for the full picture.

Manages this server’s license — the credential that unlocks the Pro and Business features. edition reports what the server is allowed to do; license is where that allowance comes from.

Terminal window
corepanel license status [--json]
corepanel license activate [key] [--key-stdin] [--json]
corepanel license trial --edition pro|business --email ADDRESS [--consent] [--json]
corepanel license refresh [--json]
corepanel license deactivate [--yes]
corepanel license upgrade [--edition pro|business] [--cycle monthly|yearly] [--wait] [--json]
corepanel license claim [--json]
corepanel license cancel-purchase

Two rules explain the whole command group:

  • The license binds to the public IPv4 the license server observes on the connection, never to one this machine reports. There is nothing to configure and nothing to spoof; a change of address is handled by refresh, which rebinds and retries by itself.
  • Licensing never stops sites from being served. If the license server cannot be reached, the last verified license keeps working for 14 days (a warning starts at 7); only a verdict — expired or revoked — degrades the server immediately. Degrading means falling back to Personal features; nothing is taken offline.

Reports the licensing state from the license cached locally, so it works offline and is safe to call from a monitoring script.

Edition: pro
Key: CPL-EQ0T6-…-2ES83
Status: active
Bound to: 203.0.113.24
Valid to: 2027-08-01 00:00 UTC
Verified: 2026-08-01 04:12 UTC

The report distinguishes the edition in effect from the edition the license pays for; they differ exactly when something needs attention, and then the licensed edition is printed as not in effect.

The exit status is 1 once the licensed edition is not in effect and 0 otherwise. A server with no license at all is not a degradation — Personal is a supported edition and the status stays 0.

Alert (alert in JSON)Meaning
(empty)Healthy, or unlicensed Personal
validation_failingCheck-ins have been failing for over 7 days; the edition still holds until the grace deadline
grace_expiredCheck-ins never recovered; the server fell back to Personal
expiredThe license ran out (payment stopped)
revokedThe license was revoked (refund or chargeback)
unverifiableThe stored license no longer verifies against the trusted signing key

The key is always shown masked (CPL-EQ0T6-…-2ES83): the panel never returns it in full.

Binds a license key to this server and unlocks its edition. The key looks like CPL-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX; it is case-insensitive and the dashes are optional.

A key already bound to another server is rebound to this one, spending one of the license’s rebinds (3 per 30 days). Releasing the old server first with corepanel license deactivate does not spend one.

The key is a bearer credential: passing it as an argument leaves it in the shell history and in the process list, so --key-stdin is the right form on shared machines. With no argument and no --key-stdin, an interactive run prompts for it and a scripted one fails rather than hanging.

Terminal window
corepanel license activate --key-stdin < license.key

Forces a check-in instead of waiting for the daily job. Use it after paying an invoice, after changing the server’s IP address, or to confirm connectivity to the license server. A failed check-in does not degrade the server — the grace window has to run out first.

Releases the license so it can be activated on another server: the license server drops the binding and this server forgets the key. The server returns to Personal features immediately.

Because the key is forgotten locally, the command asks for confirmation — keep the key at hand before running it. A non-interactive run must pass --yes; without it, it refuses rather than proceeding silently.

Starts a 14-day free trial of Pro or Business on this server. No card and no key: the license server mints the trial, binds it to the public IPv4 it observes and this server stores it, exactly as if it had been bought. It is the same thing the panel offers on the first sign-in, for a server being set up over SSH.

Terminal window
corepanel license trial --edition business --email ops@example.com

--email is required — it is where the warnings go before the trial ends (7, 3 and 1 days before, and once after). --consent is a separate opt-in for product news and changes nothing about the trial.

  • One trial per server and per email address. A second request is refused and the answer is the checkout, not a retry.
  • Switching edition mid-trial is free and keeps the original end date.
  • When it ends the server returns to Personal. Sites, mail, databases and certificates keep running; what stops is the paid features and creating sites beyond the allowance of 20.

On a Business trial, plan for one consequence: hosting accounts sign in to the client area, which Personal does not include. When the licence ends they keep access for 5 days, with a notice telling them to contact their provider, and then those logins stop. Their sites and mail are unaffected.

Opens a checkout for this server and prints the URL to complete it. Nothing is charged by the command: it mints a single-use token, ties it to this server and hands back the link.

--edition takes pro or business; the catalogue lives in corepanel-core, so an edition it does not sell is refused here rather than opening a checkout that fails at the far end.

The link works from any browser — it does not have to be one on this server. The license is fetched afterwards by this server, which is what binds it to this server’s public address rather than to anything entered at checkout. The first server to redeem a link takes the license, so a link is not something to share.

--wait keeps the terminal open until the payment is confirmed, polling every 5 seconds for up to 10 minutes. Interrupting it (or never using it) costs nothing: the panel finishes the purchase in the background, and an unredeemed one is retried at every check-in.

A server that already holds a working license is refused — changing plan is a change to the subscription, not a second purchase.

Installs the license for a purchase that was already paid for. The panel does this by itself, so this is for when nobody was watching: a browser closed at the wrong moment, or a restart mid-checkout. It is safe to repeat — a repeated claim from the same server returns the same license — and while the payment has not reached the license server yet it says so and changes nothing.

Forgets a checkout that was never completed. It redeems it first: a purchase that turns out to have been paid for is installed rather than discarded. It never cancels a payment.

Terminal window
# Buy Pro, yearly, and wait until the payment lands
corepanel license upgrade --edition pro --cycle yearly --wait
# Buy Business, monthly
corepanel license upgrade --edition business
# Paid in the browser, closed the tab: install the license now
corepanel license claim
# Monitoring: alert when the paid edition is not in effect
corepanel license status >/dev/null || echo "licensing needs attention"
# Moving to a new server: release here first so the rebind is free
corepanel license deactivate --yes
# Scripting: how long until the grace window closes?
corepanel license status --json | jq -r '.graceExpiresAt // "n/a"'

Controls whether this server takes part in the daily check-in. It has nothing to do with licensing despite sharing an endpoint: it carries no credential, unlocks nothing, and runs on every edition including Personal. See Usage statistics for the complete field list.

Terminal window
corepanel telemetry status [--json]
corepanel telemetry enable [--json]
corepanel telemetry disable [--json]

It is on by default. What it sends, once a day: a random installation id, the CorePanel version and edition, the OS and architecture, how long ago it was installed, counts of accounts/domains/mailboxes/databases/applications/WordPress sites, and the names of the features that are switched on. What it never sends: hostnames, domain names, email addresses, account names, or anything typed into the panel. The receiving end keeps the public IP the check-in arrives from, resolves it to a country and may look up its reverse DNS; CorePanel staff can see both.

status exits 0 either way — not reporting is a supported configuration, not a fault.

Turning it off stops future check-ins. It does not remove what was already reported: your server cannot reach into our records to delete rows.

For a fleet, COREPANEL_NO_TELEMETRY=1 in the environment of corepanel-core turns it off without a database write, so a golden image decides it once for every machine cloned from it.

Terminal window
# Are we reporting, and under which id?
corepanel telemetry status
# Opt out
corepanel telemetry disable
# Scripting: the id this server is counted under
corepanel telemetry status --json | jq -r .installId

Reads what CorePanel has been trying to tell you — and proves that the email side of it works before you need it. See Notifications for what produces these events and how they are delivered.

Terminal window
corepanel notifications list [--unread] [--severity <s>] [--source <m>]
[--audience admin|seller|owner|all] [--account <user>]
[--limit <n>] [--json]
corepanel notifications ack [--all | --id <id>...] [--audience <a>] [--account <user>]
corepanel notifications test [--audience admin|seller|owner] [--account <user>] [--seller-id <id>]

Two things are specific to the CLI:

  • It is root-local, so it may read every audience. The panel cannot — a session only ever sees its own notifications. This is what answers “did my customer actually get that mail?” without opening the database.
  • --account implies --audience owner. Asking for one account’s notifications as the administrator would otherwise return your copies of events about that account, which is a different list.

Lists notifications newest-first with the delivery state of each one. The DELIVERY column is the useful part when something did not arrive:

StateMeaning
pendingNot decided yet; the next delivery pass (once a minute) will
sentHanded to the local mail server — which is not the same as delivered
skippedDeliberately not mailed, with the reason beside it

The skip reasons are no_address, disabled, below_severity, local_recipient_affected (the recipient’s mailbox lives inside the very account the alert is about, so the message could not have arrived) and send_failed.

A ×N beside the event means repeats were folded into one row while it was unread.

Marks notifications as read, by id or all at once. Read state is per recipient: acknowledging your copy leaves the customer’s unread, because they have not seen it. A read notification stops counting towards the bell badge and is kept for 90 days rather than 180.

Writes a test notification for one recipient and mails it immediately, bypassing the severity policy and the hourly rate limit. It fails loudly — rather than quietly doing nothing — when the recipient has no address on file or has notification email turned off, because those are the two things you would otherwise spend an afternoon discovering.

“Sent” means the local mail server accepted it. A fresh VPS sending as corepanel@<hostname> with no SPF, no DKIM and no matching PTR is exactly the profile large providers drop silently, so check the receiving mailbox including its spam folder — and see Making the email actually arrive.

Terminal window
# What has the server been trying to tell me?
corepanel notifications list --unread
# Only the things that are actually on fire
corepanel notifications list --severity critical
# Did this customer get their "WordPress is ready" mail?
corepanel notifications list --account john
# Everything, every audience — support view
corepanel notifications list --audience all --limit 200
# Prove email works before something breaks
corepanel notifications test
corepanel notifications test --account john
# Clear the badge
corepanel notifications ack --all
# Monitoring: anything unread and critical?
corepanel notifications list --unread --severity critical --json | jq '.total'

Prints the CLI build version embedded at compile time.

Terminal window
corepanel version

Manage the FTP accounts of a hosting account. Every account is provisioned with a primary FTP account named after its system user, reaching the whole home; these commands manage the additional ones beside it. Accounts are identified by their username, any domain they own, or their numeric account id (see Naming an account).

Terminal window
corepanel ftp list <account>
corepanel ftp add <account> <username> --domain <domain> [flags]
corepanel ftp passwd <account> <ftp-account-id>
corepanel ftp remove <account> <ftp-account-id>

An additional account is named user@domain, and the domain has to be one the account owns. That is not cosmetic: FTP authenticates by username alone, so the names are unique across the whole server — requiring a domain you already hold is what stops one customer claiming a name another account may need later.

These accounts are virtual: no Linux user is created. They log in with the hosting account’s own uid and gid, which means they share its disk quota and see exactly what that user can see — there is no way for one to reach outside the account.

--directory confines the login to a subdirectory of the account home, which is the usual reason to make one: a designer who should reach uploads and nothing else. The directory is created if it does not exist (0700, owned by the account); a path that would leave the home is refused, symlinks included.

Flags for add:

FlagDescription
--domain <domain>A domain of this account, forming user@domain (required)
--directory <path>Directory relative to the account home, created if missing (default: the whole home)
--password <pw>Password — visible in shell history; prefer the options below
--password-stdinRead the password from standard input

passwd takes --password / --password-stdin with the same meaning and changes one account’s password, the primary included. Omitting both on a terminal prompts for the password without echoing it.

Terminal window
corepanel ftp add 12 designer --domain example.com --directory domains/example.com/uploads
corepanel ftp add 12 deploy --domain example.com --password-stdin < /root/ftp.pw
corepanel ftp list 12
corepanel ftp passwd 12 4
corepanel ftp remove 12 4

Deleting an account removes the credential and leaves its files alone — the directory it pointed at stays exactly as it was. The primary account cannot be deleted: it is the account’s own credential, and removing it would leave the owner unable to reach their files.

How many additional accounts an account may have comes from its package (--max-ftp on corepanel account create and update, where 0 means unlimited), with a hard ceiling of 100 underneath it. The primary account is mandatory and counts against neither.

Manage per-account cron jobs. CorePanel stores the jobs and renders the account’s crontab (/var/spool/cron/<user>), which cronie executes as the account user — so scheduled jobs keep running independently of the panel and behave exactly like cPanel. A bare php in a command resolves to the account’s own PHP version, so standard WHMCS/WordPress cron lines work unchanged. Accounts are identified by their username, any domain they own, or their numeric account id (see Naming an account).

Terminal window
corepanel cron list <account>
corepanel cron add <account> --schedule <spec> --command <cmd> [flags]
corepanel cron update <account> <job-id> --schedule <spec> --command <cmd> [flags]
corepanel cron remove <account> <job-id>
corepanel cron enable|disable <account> <job-id>
corepanel cron run <account> <job-id>

The --schedule value is a five-field cron spec ("*/5 * * * *") or one of the macros @hourly, @daily, @midnight, @weekly, @monthly, @yearly, @annually. It is validated and the next run computed before the job is stored. @reboot and @every <duration> are not supported — use a five-field spec (*/30 * * * * for “every 30 minutes”).

Flags for add / update:

FlagDescription
--schedule <spec>Five-field cron spec or @macro (required)
--command <cmd>Command to run (required)
--comment <text>Human-readable label
--mailto <email>Email job output to this address (empty disables mail)
--timezone <tz>IANA timezone for the schedule (e.g. America/Santiago)
--disabledCreate/leave the job disabled (had no effect before 1.6.14: the job was created enabled)
--single-instanceSkip a run if the previous one is still active (flock)
--timeout <sec>Kill the job after N seconds
--cpu <sec>CPU-time cap
--mem-kb <kb>PHP memory limit in KB (applied to PHP jobs via memory_limit)
--nproc <n>Max processes
--nice <0-19>Niceness (higher = lower priority)
Terminal window
# WordPress and WHMCS cron, cPanel-compatible:
corepanel cron add 12 --schedule "*/5 * * * *" --command "php /home/user/public_html/wp-cron.php"
corepanel cron add 12 --schedule "@daily" --command "php ~/whmcs/crons/cron.php" --mailto ops@example.com --single-instance
corepanel cron list 12
corepanel cron run 12 3 # run once, now, and show the output
corepanel cron disable 12 3

Jobs run through a wrapper that applies the resource limits, single-instance lock, and a timeout, and captures output. Disabling a job keeps it in the list but comments it out of the crontab.

There is no allowlist: the command is any shell line the account could type itself. It runs as the account user (never root), so it can only reach what that user can reach — homes are 0700 and files 0600.

Every job is executed as /bin/sh -c '<your command>' with:

SHELL=/bin/sh
PATH=/opt/corepanel/bin:/usr/local/bin:/usr/bin:/bin

What that means in practice:

  • It is sh, not bash. No [[ ]], arrays or process substitution. If you need bash features, wrap them: bash -c '...'.
  • php works on its own and resolves to that account’s PHP version, even though the host has no global PHP. The shim also points PHP’s temporary files, uploads and sessions at the account’s private ~/tmp, and applies the job’s --mem-kb as PHP’s own memory_limit. Do not hardcode /usr/bin/php — it does not exist on a CorePanel host, and an absolute Remi path such as /opt/remi/php84/root/usr/bin/php pins the job to one PHP version and loses the temp-dir isolation and memory limit.
  • wp (WP-CLI) is on the PATH and runs with the account’s PHP.
  • Pipes, redirections, && and variables are all fine — it is a normal shell line.
  • Base tools available to jobs include curl, wget, git, tar, unzip, rsync, jq, find and the MariaDB client (mysqldump).
  • % needs no escaping. CorePanel escapes it for cron, so date +%Y reaches the shell verbatim.
  • ~ expands to the account home, since cron sets HOME — but only at the start of a word. Inside a flag (--path=~/public_html) it stays literal; write "$HOME/..." there instead.
Terminal window
# Application cron entry points
php ~/public_html/cron.php
php ~/whmcs/crons/cron.php
php ~/app/artisan schedule:run # Laravel scheduler (run every minute)
# WordPress via WP-CLI (after disabling wp-cron.php in wp-config.php)
wp --path="$HOME/public_html" cron event run --due-now
wp --path="$HOME/public_html" plugin update --all
wp --path="$HOME/public_html" db export ~/backups/wp-$(date +%F).sql
# Trigger something over HTTP instead of on disk
curl -fsS https://example.com/tasks/run > /dev/null
curl -fsS -m 30 https://example.com/health || echo "health check failed"
# Database dump with rotation (credentials in ~/.my.cnf, mode 0600)
mysqldump --defaults-extra-file="$HOME/.my.cnf" mydb | gzip > ~/backups/db-$(date +%F).sql.gz
find ~/backups -name 'db-*.sql.gz' -mtime +14 -delete
# Housekeeping
find ~/tmp -type f -mtime +7 -delete
find ~/logs -name '*.log' -size +100M -delete
# Your own scripts (must be executable, or invoke the interpreter explicitly)
~/bin/nightly-report.sh >> ~/logs/report.log 2>&1
sh ~/bin/sync-assets.sh
bash -c 'for d in ~/sites/*/; do echo "checking $d"; done'
# Archive a directory
tar -czf ~/backups/uploads-$(date +%F).tar.gz -C ~/public_html uploads

Passing a password on the command line (mysqldump -p'secret') works but exposes it in the process list to other processes on the host; a ~/.my.cnf with mode 0600 is the safer form shown above.

CommandWhy
/usr/bin/php script.phpNo global PHP on the host — exits 127. Use php.
sudo ..., anything as rootJobs run as the account user; cronie drops privileges first.
Multi-line commandsNewlines and control characters are rejected — a job is one line.
--schedule "@reboot"Not a supported macro (see above).
Reading another account’s filesHomes are 0700; jobs are confined to their own account.

An account may define up to 100 jobs.

Manage an account’s domains beyond the primary. Two types are supported:

  • alias (parked): serves the account’s primary docroot under its own DNS zone (pointing at the server IP), and its mail is routed to the primary — it has no independent mailboxes. Aliases count against the account’s parked-domain limit.
  • addon: a fully independent domain inside the same account — its own docroot at ~/domains/<domain>, its own DNS zone, and its own mail identity (DKIM + a webmail.<domain> vhost). Addon domains can have their own mailboxes and count against the account’s dedicated addon-domain limit.

Both limits use 0 = unlimited. Accounts are identified by their username, any domain they own, or their numeric account id (see Naming an account) (list them with corepanel account list); domain names are globally unique.

Terminal window
corepanel domain list <account>
corepanel domain add <account> <domain> [--type alias|addon] [--alias-for <domain>] [--web-mode <mode>] [--canonical <host>]
corepanel domain delete <domain>
corepanel domain canonical show <domain>
corepanel domain canonical set <domain> <none|www|nonwww|primary>
corepanel domain optimizations show <domain>
corepanel domain optimizations set <domain> [--webp on|off|default] [--minify-js on|off|default] [--minify-css on|off|default] [--early-hints on|off|default] [--page-cache off|shadow|on] [--state-cookies <names>]
  • list prints every domain the account owns — primary and secondary — with its type, alias target, document root, web mode and canonical host (core.ListAccountDomains). The document root is printed because it is not derivable from the name: an addon owns ~/domains/<domain>, an alias shares the primary’s ~/public_html, and an account imported from another panel keeps the layout it arrived with.
  • add validates the FQDN, global uniqueness, ownership and the matching per-type limit, provisions the vhost + DNS zone (and, for an addon, its own docroot, DKIM and webmail.<domain>) via sys.AddDomain, and moves the usage counter (core.AddDomain). An alias republishes the account’s primary domain by default; --alias-for parks it on one of the account’s addon domains instead, so it serves that site’s document root and its mail is redirected there. The target must belong to the same account and be a primary or addon domain — an alias of an alias is refused.
  • delete removes a secondary domain (core.DeleteDomain). For an addon this also tears down its mailboxes, DNS zone, DKIM, docroot, subdomains and any alias parked on top of it — those serve the addon’s document root, so they cannot outlive it. The account’s primary domain cannot be removed this way — it is only removed by deleting the account.
  • canonical show prints which hostname of the domain serves the site and what the others do (core.GetDomainCanonicalHost).
  • canonical set picks the real hostname (core.SetDomainCanonicalHost) and re-emits the site config. Every domain answers under both <domain> and www.<domain>, and an alias adds two more hostnames serving a copy of the primary’s site — identical content on several hostnames is duplicate content, which splits link equity and leaves search engines guessing which URL is authoritative. The hostnames that are not canonical answer a 301 that preserves the path and query string, so deep links and indexed URLs keep working.
  • optimizations show prints a domain’s web optimization overrides (core.GetDomainOptimizations).
  • optimizations set sets them (core.SetDomainOptimizations) and re-emits the site config. WebP-on-the-fly and Early Hints (HTTP 103) are baseline in every edition and on by default — WebP transparently serves WebP to browsers that accept it (safe fallback to the original image); Early Hints learns a page’s preload sub-resources and replays them as a 103 on later requests (HTTP/2 and HTTP/3 only, inert for static/SPA sites). CSS/JS minification and the dynamic page cache are Speed Optimizer (Pro/Business) and off by default — minification can break sites (source maps, already-minified assets, execution order), and a page cache should be measured before it serves anything, so both are opt-in. Use these commands to override each per domain.

Flags for add:

FlagDescription
--type <type>Domain type: alias (parked, default) or addon (independent)
--alias-for <domain>For an alias, the site it republishes: an addon domain of the same account. Empty parks it on the primary (the default)
--web-mode <mode>Web serving mode plain|cms|spa (empty ⇒ plain)
--canonical <host>Canonical hostname none|www|nonwww|primary (empty ⇒ www)

Values for canonical set (and --canonical):

ValueEffect
noneEvery hostname serves the site directly, no redirect. Domains created before this feature keep this after an upgrade — nothing changes for a live site until you choose a canonical host.
wwwwww.<domain> serves; <domain> answers 301 to it. Default for new domains.
nonwww<domain> serves; www.<domain> answers 301 to it.
primaryAlias (parked) domains only. Every hostname of the alias answers 301 to the primary’s canonical hostname, in a single hop, instead of serving a copy of its site. Changing the primary’s canonical host re-points every alias set to primary automatically.

Flags for optimizations set:

FlagDescription
--webp <state>WebP-on-the-fly: on (force), off (disable), or default (follow the server default, on). Omitting the flag resets to default.
--minify-js <state>CSS/JS minify — JavaScript (Speed Optimizer, Pro/Business): on (force), off (disable), or default (follow the server default, off). Omitting the flag resets to default.
--minify-css <state>CSS/JS minify — CSS (Speed Optimizer, Pro/Business): on (force), off (disable), or default (follow the server default, off). Omitting the flag resets to default.
--early-hints <state>Early Hints (HTTP 103): on (force), off (opt this domain out), or default (follow the server default, on). Omitting the flag resets to default.
--page-cache <mode>Dynamic page cache (Speed Optimizer, Pro/Business): off, shadow (evaluate without serving) or on. Not tri-state — there is no server default to follow, so omitting the flag turns the cache off. Refused on alias (parked) domains.
--state-cookies <names>Extra cookies that mean “this visitor has state”, comma-separated; a request carrying one bypasses the page cache and its response is never stored. A trailing * matches a prefix; at most 32. Added to the cookies CorePanel already knows (WordPress session, comments, WooCommerce, Aelia currency), which always apply. Not whole-state: omitting the flag keeps the site’s current list; pass "" to clear it.

The page cache stores whole pages so a repeat visitor is answered without running PHP at all. Two things make it safe to switch on:

  • Only what WordPress itself declares shareable is stored. CorePanel’s companion plugin decides, page by page, whether a response can be handed to the next visitor, and purges the right URLs when a post is published or edited. A site without the plugin is not cached at all rather than guessed at.
  • shadow measures before anything is served. In shadow the cache decides and stores exactly as it would in production and serves none of it, so you can see the hit ratio a site would get before a single visitor is affected. Leaving shadow is self-healing: those entries are dropped on sight, with nothing to purge by hand.
Terminal window
corepanel domain list 12
corepanel domain add 12 parked.example.com
corepanel domain add 12 shop.example.net --type addon --web-mode cms
corepanel domain add 12 parked.example.com --canonical primary
corepanel domain add 12 alias-of-shop.example.com --alias-for shop.example.net
corepanel domain delete parked.example.com
corepanel domain canonical show example.com
corepanel domain canonical set example.com www
corepanel domain canonical set example.com nonwww
corepanel domain canonical set parked.example.com primary
corepanel domain optimizations show shop.example.net
corepanel domain optimizations set shop.example.net --webp off
corepanel domain optimizations set shop.example.net --webp default
corepanel domain optimizations set shop.example.net --minify-js on --minify-css on
corepanel domain optimizations set shop.example.net --early-hints off
corepanel domain optimizations set shop.example.net --page-cache shadow
corepanel domain optimizations set shop.example.net --page-cache on
corepanel domain optimizations set shop.example.net --state-cookies "my_currency,my_shop_*"
corepanel domain optimizations set shop.example.net --state-cookies ""

Publish a local service under one of the server’s domains. A route forwards the requests matching a path to a host:port upstream; every unmatched path keeps being served from the docroot. Routes are evaluated first match wins, and CorePanel keeps them ordered most-specific-first so a catch-all never hides a more specific route. The full guide — what the feature is for, when to use it and how the panel presents it — is in Reverse Proxy Routes.

These commands are administrator-only, and that is a security boundary rather than a UI choice: the caller picks the upstream, so pointed at an internal service the route would publish it on the public internet under the domain’s name. Targets on the local loopback are filtered accordingly — the web server’s own ports (80/443) are refused outright because they would loop, and well-known service ports (corepanel-api, MySQL, PostgreSQL, Redis, Memcached, SSH, SMTP) require --allow-reserved-port.

Terminal window
corepanel domain proxy list <domain> [--check]
corepanel domain proxy add <domain> <path> <upstream> [--strip-prefix] [--tls] [--allow-reserved-port]
corepanel domain proxy remove <domain> <path>
  • list prints the domain’s whole route table in evaluation order (core.GetDomainRoutes). The MANAGED column marks which entries these commands own: yes for a proxy route you added, auto for the docroot fallback CorePanel maintains, no for a hand-written route it leaves untouched. With --check every proxy target is dialed (core.CheckProxyUpstreams) and a STATUS column reports up (Nms) or DOWN: <reason> — the fastest way to tell “my service died” from “my site is broken”, which from the outside are the same 502.
  • add adds or replaces a route (core.SetDomainProxyRoute). <path> is the matcher — a prefix (/api/*), an exact path (/health) or a catch-all (/*) — and it is the route’s identity: re-running add with the same path edits that route in place instead of adding a second, unreachable one. <upstream> is host:port, never a URL — or the unix socket of an application CorePanel published, written as unix:/run/corepanel/apps/<account>/<app>.sock. Only that exact layout is accepted, so no other socket on the server can be named; CorePanel writes those routes itself. The upstream is probed right after the route is applied, and a warning is printed if nothing is listening (the route is applied either way).
  • remove drops the route matching <path> (core.DeleteDomainProxyRoute). Removing the last one returns the site to serving its docroot directly; removing a path that has no route is not an error.

Flags for add:

FlagDescription
--strip-prefixRemove the matcher prefix from the proxied request, so /api/* reaches the upstream as /*. Ignored for a catch-all matcher.
--tlsDial the upstream over HTTPS instead of plain HTTP.
--allow-reserved-portConfirm a target on a well-known local service port.
Terminal window
corepanel domain proxy list app.example.com
corepanel domain proxy list app.example.com --check
corepanel domain proxy add app.example.com '/api/*' 127.0.0.1:8080 --strip-prefix
corepanel domain proxy add app.example.com '/*' 127.0.0.1:8080
corepanel domain proxy remove app.example.com '/api/*'

Manage an account’s subdomains (e.g. blog.example.com). A subdomain gets its own document root (~/domains/<fqdn>, beside the primary’s public_html rather than inside it) and web vhost, plus a single A record inside its parent domain’s existing DNS zone — it does not create a new zone and has no mail of its own. TLS is still issued for the subdomain FQDN. The parent must be an existing primary or addon domain owned by the account. Subdomains count against the account’s subdomain limit (0 = unlimited). Accounts are identified by their username, any domain they own, or their numeric account id (see Naming an account) — find it with corepanel account list.

Terminal window
corepanel subdomain list <account>
corepanel subdomain add <account> <label> <parent-domain> [--web-mode <mode>]
corepanel subdomain delete <fqdn>
corepanel subdomain optimizations show <fqdn>
corepanel subdomain optimizations set <fqdn> [--webp on|off|default] [--minify-js on|off|default] [--minify-css on|off|default] [--early-hints on|off|default] [--page-cache off|shadow|on] [--state-cookies <names>]
  • list prints every subdomain the account owns, with its parent, web mode, and document root (core.ListSubdomains).
  • add validates the label, parent ownership, global uniqueness (across both domains and subdomains) and the subdomain limit, provisions the docroot + vhost + parent-zone A record via sys.AddSubdomain, and moves the usage counter (core.AddSubdomain). <label> is what the subdomain adds in front of the parent zone: usually one label (blog), but several are allowed for a nested subdomain (dev.blog under example.comdev.blog.example.com), which other panels create and an import has to reproduce. As a whole label, www and webmail are reserved (they are the parent’s own hostnames); nothing auto-provisions www.blog.example.com, so www.blog is accepted. A nested name is not covered by the parent’s wildcard certificate — a wildcard spans one label — so it gets a certificate of its own.
  • delete removes a subdomain by its fully-qualified name (core.DeleteSubdomain): the vhost, the parent-zone A record, the certificate and the document root.
  • optimizations reads and writes the subdomain’s web optimizations — WebP, CSS/JS minification, Early Hints and the dynamic page cache. Same flags, same defaults and the same edition gating as corepanel domain optimizations in Domains above; see that section for what each flag does.

A subdomain is a site of its own — its own document root, usually its own application — and inherits nothing from its parent domain: switching the page cache on for example.com does not touch blog.example.com, and vice versa. Each is tuned separately. (The alias refusal does not apply here: a subdomain serves its own content, not a copy of another site’s.)

Flags for add:

FlagDescription
--web-mode <mode>Web serving mode plain|cms|spa (empty ⇒ plain)
Terminal window
corepanel subdomain list 12
corepanel subdomain add 12 blog example.com
corepanel subdomain add 12 shop example.com --web-mode cms
corepanel subdomain add 12 dev.blog example.com
corepanel subdomain delete blog.example.com
corepanel subdomain optimizations show blog.example.com
corepanel subdomain optimizations set blog.example.com --page-cache shadow
corepanel subdomain optimizations set blog.example.com --webp off --minify-css on

Turning the dynamic page cache on or off is part of a site’s optimizations, above. These two commands cover what is not configuration: emptying the cache, and reading what an evaluation measured.

Sites are named by hostname — a domain, an alias or a subdomain — because that is how the cache itself is keyed.

Terminal window
corepanel cache purge <hostname> [path...]
corepanel cache stats [hostname]

purge with no paths empties the whole site; otherwise each argument is an absolute path, optionally ending in * to purge a prefix. A path that is not absolute, or with a * anywhere but at the end, is rejected rather than silently matching nothing.

Every hostname the site answers on is purged, not just the one you typed: a page is stored once per hostname a visitor arrived through, so example.com and www.example.com hold separate copies. The command prints the hostnames it reached — a missing one is a separate site. Emptying the cache is never blocked by edition: pages cached while the server was licensed are still on disk, and clearing a wrong page must always be possible.

stats only reports sites in evaluation mode (--page-cache shadow). A site with the cache switched on produces no counters at all — the question the report answers is “what would this site gain” — and the tallies reset when the web server restarts. HIT% counts hits over the requests that were cacheable at all, so a bypassed request is not a miss; BYPASS% is reported separately for exactly that reason.

Terminal window
corepanel cache purge example.com
corepanel cache purge blog.example.com / /feed
corepanel cache purge example.com "/2026/*"
corepanel cache stats
corepanel cache stats blog.example.com

.htaccess compatibility needs no configuration: the web server compiles the rules it supports and picks a changed file up within a second, however it was written. These commands answer the two questions a migrated site raises — what did the server make of my file, and can I see the change now.

Terminal window
corepanel htaccess report <hostname> [--problems] [--json]
corepanel htaccess reload <hostname> [directory...]

report reads every .htaccess under the site’s document root and says what happened to each directive:

StatusMeaning
appliedThe rule is in force.
no-opIt asks for what already happens here.
unsupportedOut of scope by design — the explanation says why.
errorIt could not be compiled, and the line is skipped.

There is deliberately no “pending”. unsupported is a decision, not a queue position: most of those lines configure something this server does its own way — browser cache policy, response headers, what runs as PHP — and the site is better off without them. The ones worth acting on say so in their explanation.

The report reads the files on disk, which is what would apply; --problems hides the applied lines, and --json prints the whole thing for scripting. The same view lives in the panel under an account’s Apache rules section.

Terminal window
corepanel htaccess report example.com
corepanel htaccess report example.com --problems

It drops the site’s compiled rules and the pages stored for them, in that order and in the same call — a cached page is answered before any rule is consulted, so dropping the rules alone would leave a visitor being served exactly what you just changed.

With no directories the whole site is covered; otherwise each argument is a directory relative to the document root (/ for the root itself). Name the directory, not the .htaccess file inside it. Dropped: 0 means nothing was compiled for those directories: normal on a site nobody has visited, a wrong path on a busy one — and the document root printed above it is what settles that. Never blocked by edition.

Terminal window
corepanel htaccess reload example.com
corepanel htaccess reload example.com wp-content wp-content/uploads

Manage WordPress installations end to end: install, adopt, update, harden, verify and roll back sites. Every operation runs WP-CLI as the account user (never as root) with the account’s own PHP version. Accounts are identified by their numeric account id; instances by the instance id shown in corepanel wp list.

Terminal window
corepanel wp list <account>
corepanel wp scan <account>
corepanel wp install <account> --domain <fqdn> --title <t> --admin-user <u> --admin-email <e> [flags]
corepanel wp attach <account> <docroot-path>
corepanel wp detach <account> <instance-id>
corepanel wp login <account> <instance-id> [--user <login>]
corepanel wp components <account> <instance-id>
corepanel wp update <account> <instance-id> [--core] [--all-plugins] [--all-themes] [--plugins a,b] [--themes x,y]
corepanel wp plugin|theme <action> <account> <instance-id> <slug>
corepanel wp policy <account> <instance-id> <none|minor|all>
corepanel wp hardening <account> <instance-id> [--set key=on|off ...]
corepanel wp integrity <account> <instance-id> [--reinstall]
corepanel wp maintenance|debug|indexing <account> <instance-id> [on|off]
corepanel wp set-url <account> <instance-id> <url>
corepanel wp restore-point create|list|rollback|delete

Highlights:

  • 1-click installwp install provisions the database and DB user, downloads core as the account, writes a hardened wp-config.php (fresh salts, system-cron takeover, direct filesystem access), creates the native */5 cron job that replaces WordPress’s visitor-paid pseudo-cron, and sets the domain’s web mode to cms. When --admin-password is omitted a strong password is generated and shown once.
  • Adopt existing siteswp scan finds installations anywhere under the account home (including sites migrated with the cPanel importer) and manages them without touching their behavior. wp detach leaves a .corepanel-wp-ignore marker so the site stays unmanaged.
  • One-time login linkswp login prints a single-use admin URL that expires in 60 seconds. The token is minted inside WordPress and only its hash is stored; no passwords are involved.
  • Safe updateswp update --core automatically snapshots the site (files + database) first; wp restore-point rollback brings it back.
  • Reversible hardening — file-editor lockdown, XML-RPC blocking, version hiding, user-enumeration blocking, and application-password disabling, each individually switchable (on = protection enabled).
  • Integritywp integrity verifies core files against wordpress.org checksums; --reinstall replaces core files in place (keeping wp-content and wp-config.php), the standard malware-cleanup step.
  • Maintenance mode — a server-level 503 page served by the web server itself, so it keeps working even when WordPress is broken mid-upgrade.

Action verbs for wp plugin / wp theme: install (--version, --activate), activate, deactivate (plugins only), delete, autoupdate-on, autoupdate-off.

Hardening keys for --set: file-editor, xmlrpc, version-hiding, user-enumeration, app-passwords.

Terminal window
# Install and open the dashboard
corepanel wp install 12 --domain example.com --title "My Blog" --admin-user admin --admin-email me@example.com
corepanel wp login 12 3
# Keep it updated, safely
corepanel wp components 12 3
corepanel wp update 12 3 --core --all-plugins
corepanel wp restore-point list 12 3
# Harden and verify
corepanel wp hardening 12 3 --set xmlrpc=on --set user-enumeration=on
corepanel wp integrity 12 3
# Upgrade window
corepanel wp maintenance 12 3 on
corepanel wp update 12 3 --all-themes
corepanel wp maintenance 12 3 off

Create and manage an account’s databases and the users that connect to them.

Terminal window
corepanel database list <account> [--users]
corepanel database create <account> <name> [--user <suffix>]
corepanel database delete <account> <database> [--yes]
corepanel database user list <account> [<database>]
corepanel database user create <account> <database> <name> [--host <host>]
corepanel database user password <account> <user>
corepanel database user privileges <account> <user> --privileges <list>
corepanel database user delete <account> <user> [--yes]

A schema is always named <account_user>_<suffix>, and so is a user. That is why every command takes the account first and the suffix after it: the prefix is not yours to choose, and repeating it on the command line is noise. A database or a user can be named in full (example_blog), by its suffix (blog) or by its numeric id — and an id that belongs to a different account is refused here, not sent on.

database is also available as db.

example_blog@localhost and example_blog@% are two different MySQL users with two different passwords, and an account can have both. New users are created at localhost, which is what a site or an application on this server connects as.

A bare name that matches two users is refused, listing both, rather than resolved to one:

$ corepanel database user password example blog
Error: "blog" names more than one user; a host is part of a user's identity,
so say which: example_blog@localhost (on example_blog), example_blog@% (on example_blog)

Dropping the wrong credential — or resetting a password that leaves the other user working, while you believe you changed it — is not something to decide by whichever row came back first.

Terminal window
# The schema and its user in one command, password prompted
corepanel database create example blog --user blog
# Scripted, reading the password from standard input
printf '%s' "$DB_PASS" | corepanel database create example shop --user shop --password-stdin

--user is what a new database is almost always for; without it the schema exists and nothing can connect to it yet. The password is resolved before the schema is created, so a mistyped prompt does not leave a database behind that you have to delete before retrying the same command. If the user fails once the schema exists, the schema is kept and the command prints the retry — dropping a database because a user could not be added is the more destructive answer to a retryable problem.

Creating one counts against the account’s database allowance, like any other.

Terminal window
corepanel database user create example blog reports --privileges select,show\ view
corepanel database user privileges example blog_reports --privileges select
corepanel database user password example blog_reports

A grant is always on one database, never on the server, and ALL PRIVILEGES is as wide as it goes. user privileges replaces the grant rather than adding to it: the list you pass is what the user ends up with.

Accepted privileges: ALL PRIVILEGES, SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, INDEX, CREATE TEMPORARY TABLES, LOCK TABLES, CREATE VIEW, SHOW VIEW, EVENT, TRIGGER, REFERENCES. A mistyped one is refused with that list in hand, rather than three services away as MariaDB’s “invalid privilege”.

--host opens a user to an address other than localhost, and the command says so when you do. Whether it is reachable at all is a matter of the server’s bind address and its firewall, both of which are the operator’s decision.

Terminal window
corepanel database user delete example blog_reports # asks first
corepanel database delete example blog --yes # for scripts

Dropping a user leaves the database and its data alone; what goes is the credential and its grant, so anything still connecting as it starts being refused. Dropping a database takes its tables and its users with it, and nothing in CorePanel brings it back — only the account’s backup can. Both ask for confirmation, and both refuse outright when there is no terminal to ask on, so --yes is a deliberate act rather than a default.

While an application holds a database, these commands refuse to break it:

  • corepanel database delete refuses that schema, naming the application.
  • corepanel database user delete refuses that user, naming the application.
  • corepanel database user password re-injects the new password into the application and restarts it, so the process is never left holding a dead credential.

corepanel app database detach is the way out: it drops the application’s user and keeps the schema.

Run a long-lived program for a hosting account and publish it under one of the account’s domains — a Go API behind /api, a service on its own subdomain, anything that listens for HTTP. Accounts are identified by their username, any domain they own, or their numeric account id (see Naming an account); an application by that id plus its slug.

Each application gets a systemd unit of its own (corepanel-app-<user>.<app>.service) running as the account user in a private network namespace, with memory, CPU and task limits applied as cgroup properties. It never listens on a TCP port the rest of the server can reach: CorePanel binds a unix socket for it (/run/corepanel/apps/<user>/<app>.sock) and the web server proxies to that socket, so two applications can never collide on a port.

The code is shipped as a .tar.gz the account uploads (over FTP, to ~/apps/<name>.tar.gz by default). CorePanel extracts it as a new release, starts it and checks it answers before publishing it; a release that does not answer is rolled back automatically and the previous one keeps serving. Three releases are kept.

Terminal window
corepanel app list [<account>]
corepanel app show <account> <name>
corepanel app create <account> <name> --domain <fqdn> --port <n> [flags] -- <program> [args...]
corepanel app update <account> <name> [flags] [-- <program> [args...]]
corepanel app deploy <account> <name> [--artifact <path>]
corepanel app rollback <account> <name> [--to <release>]
corepanel app start|stop|restart <account> <name>
corepanel app releases <account> <name>
corepanel app logs <account> <name> [-n 200] [--follow]
corepanel app env list|set|unset <account> <name> [KEY=VALUE|KEY] [--secret]
corepanel app database show|create|attach|detach <account> <name> [<database>]
corepanel app webhook show|rotate|revoke <account> <name>
corepanel app delete <account> <name> [--purge-data]
corepanel app runtime status
corepanel app runtime prepare [--yes]

Applications belong to a hosting account, identified by their username, any domain they own, or their numeric account id (see Naming an account) — list accounts with corepanel account list to find it. Every command below takes it as <account>, followed by the application’s name.

The start command goes after a -- separator, as a program relative to the release root followed by its arguments:

Terminal window
corepanel app create <account> api --domain example.com --path /api --port 8080 \
--health /healthz --mem 512 --cpu 100 -- server --addr :8080

create reserves the name and derives the unit and the socket from it, but it does not publish the application yet — the route is added by the first deploy that answers. Pointing the site at a socket nothing is listening on would 502 the path, and for an application mounted at / that is the whole site.

FlagDescription
--domain <fqdn>Domain or subdomain of the account to publish under (required). A domain alias is refused: it has no site file of its own
--path </sub>Path under the site (default /); a sub-path mount gives the application its own root
--port <n>Port the application listens on inside its network namespace, 1024-65535 (required)
--health </healthz>Path requested after a deploy to decide whether the release is serving; without it the check is a plain connection to the socket
--link <dir>Release-relative directory symlinked into the data volume, for applications that write inside their own tree; repeatable
--mem <mb>Memory ceiling in MB (default 256)
--cpu <pct>CPU quota as a percentage; 100 is one whole core (default 50)
--tasks <n>Maximum number of tasks (default 64)
--network outbound|noneOutbound access, outbound by default. none leaves the application with loopback only — no internet, no DNS — and saves the network backend’s ~25 MB. Neither value exposes services on the server’s own 127.0.0.1, and neither publishes the application’s port
--runtime goApplication runtime; only go is supported in this release

update takes the same flags and applies them to the release already running. Omitted fields are left as they are, the previous settings are restored if the application stops serving under the new ones, and an application that was stopped stays stopped. --clear-links removes every linked directory.

Terminal window
# Deploy the conventional ~/apps/api.tar.gz
corepanel app deploy <account> api
# Or an artifact uploaded somewhere else in the account home
corepanel app deploy <account> api --artifact uploads/api-2026-07-27.tar.gz
# Go back
corepanel app releases <account> api
corepanel app rollback <account> api --to 20260727T101500Z

The artifact must live inside the account’s home — it is extracted as root, so a path outside the home is refused. A release that does not answer is rolled back to the previous one, and deploy exits non-zero printing the reason from the application’s journal.

Terminal window
corepanel app show <account> api
corepanel app logs <account> api -n 500
corepanel app logs <account> api --follow

show prints the specification alongside the live systemd status — active state, main pid, last exit, restarts and whether the socket exists. What CorePanel recorded and what systemd answers are different questions: an application the panel believes is running can be crash-looping, which is exactly the case worth diagnosing.

It also prints Health, which is a third question again: the periodic check asks every running application whether its endpoint still answers, and this is its last verdict with the time behind it. not checked means the application is stopped or has never been deployed.

logs prints a snapshot of the journal, which holds both the application’s own output and the gateway’s. With --follow (-f) it keeps printing what arrives until you interrupt it, the same way journalctl -f does — the line count applies to the first screenful, and from there on it only shows what is new.

Terminal window
corepanel app env set <account> api LOG_LEVEL=debug
corepanel app env set <account> api API_TOKEN=s3cr3t --secret
corepanel app env set <account> api NEXT_FLAG=on --no-restart
corepanel app env list <account> api
corepanel app env reveal <account> api API_TOKEN
corepanel app env import <account> api ./.env
corepanel app env unset <account> api LOG_LEVEL

The variables live in a file systemd reads as root before dropping privileges, so the application receives them while the account cannot read the file that holds them.

A --secret value is hidden from env list; env reveal prints it and records that it was asked for in the server log. Naming a variable reveals that one, leaving the name out reveals the whole environment as a single audit entry. No password is asked for: whoever can run this can already replace the value and deploy code that prints it.

--secret is only sent when you actually pass it, so changing a secret’s value does not quietly turn it into a plain one. Going the other way — a secret becoming plain text — is only accepted in a call that carries the value, because a demotion without it would list a variable in the clear that the caller never held.

FlagCommandEffect
--secretenv setHide the value from listings
--no-restartenv set, env importStore the value without restarting a running application
--replaceenv importAlso remove the variables the file does not mention
--plainenv importDo not mark credential-looking names as secret

env import reads a .env — comments, blank lines, an optional export prefix and quoted values — and applies the whole file in one restart, whatever its length. Setting the variables one at a time restarts a running application per line. Use - as the filename to read standard input.

Names that look like credentials (ending in KEY, TOKEN, SECRET, PASSWORD, DSN, plus DATABASE_URL) are marked secret on import unless --plain is given.

Setting or removing a variable restarts a running application unless --no-restart says otherwise. A value can be up to 16 KB, the environment as a whole up to 256 KB, and no value may contain a line break.

Terminal window
corepanel app database create <account> api # schema + user + password
corepanel app database show <account> api
corepanel app database attach <account> api blog # one the account already has
corepanel app database detach <account> api # keeps the data

create provisions the schema, a user dedicated to the application and a password, writes the connection into the environment and restarts the process — one command, and no credential passes through anybody’s clipboard. The application reads it from:

VariableWhat it holds
CP_DB_NAMEThe schema, <account_user>_<suffix>
CP_DB_USERThe user the application connects as, always @localhost
CP_DB_PASSWORDThe password, stored as a secret
CP_DB_SOCKETThe MariaDB unix socket to connect to
CP_DB_HOST, CP_DB_PORTPresent and empty: there is no TCP route (see below)
DATABASE_URLThe whole thing as a DSN, for a library that takes one

These are managed variables: app env set refuses to write one, and the panel shows them read-only. Detaching is what removes them. DATABASE_URL is the exception — set it yourself and your value wins, for a driver whose DSN dialect is not the one CorePanel writes.

--name <suffix> chooses the schema name; the full name is always <account_user>_<suffix>. Without it the suffix comes from the application’s name, with a number appended when that one is taken. A suffix you pass may hold letters, digits and _ only, and <account_user>_<suffix> has to fit in the 32 characters MySQL allows a user name.

attach links a schema the account already owns — named in full (example_blog), by its suffix (blog) or by its numeric id — and still creates a new user for the application. It cannot do anything else: CorePanel does not store MySQL passwords, so there is no existing credential to hand over, and resetting one would break every wp-config.php that shares it. The data is untouched and no other grant is changed.

detach drops the user CorePanel created and removes the variables, restarting a running application. The database and everything in it are kept.

While an application holds a database, the database commands protect it:

  • corepanel database delete refuses to drop that schema, naming the application.
  • corepanel database user delete refuses to drop that user, naming the application.
  • corepanel database user password re-injects the new password and restarts the application, so a rotation never leaves the process holding a dead credential.
  • Deleting the application drops its user and never the schema, --purge-data or not. The files under the application’s directory are its own; the database is the customer’s.

A build pipeline can deploy without a person in the loop:

Terminal window
corepanel app webhook rotate <account> api # issues the token and prints both URLs
corepanel app webhook show <account> api # prints them again
corepanel app webhook revoke <account> api # disables it

The token works two ways. The pipeline can POST the artifact itself, deploying it in the same call and needing no FTP account:

Terminal window
curl -fsS -X POST https://panel.example.com/api/apps/upload \
-H 'Authorization: Bearer cpd_...' --data-binary @api.tar.gz

Or it can upload to ~/apps/<name>.tar.gz as the account and POST to the deploy URL, which is what a GitHub or GitLab webhook — a caller that can be given nothing but an address — has to do.

Either way, what runs is the same deploy as corepanel app deploy, automatic rollback included. Two properties decide how the pipeline is written: the call answers 202 as soon as the deploy starts, not when it finishes, and a second call while one is running is refused with 409.

Full guide, with GitHub Actions and GitLab CI examples, signatures and every status code: Deploying from CI/CD.

Applications run natively on every supported release, and nothing on this page needs preparing. Running them in containers — a Podman backend that is still in development — additionally needs podman and a kernel booted with the unified cgroup hierarchy:

Terminal window
corepanel app runtime status

It reports podman’s version, the cgroup hierarchy the kernel booted with, the controllers delegated to per-user slices and how SELinux labels the container image store, and ends in one of three verdicts: ready, reboot pending or unavailable, each with the step that matches it.

On AlmaLinux 9 and 10 a stock server is ready. On AlmaLinux 8 the kernel boots with the older cgroup hierarchy, and that is not a detail: there, podman accepts a container’s memory and CPU limits, ignores them, and starts the container anyway. CorePanel would rather refuse than run a customer’s application on a limit that does not exist, so the backend is not offered until the server can enforce it.

Terminal window
corepanel app runtime prepare

prepare installs the systemd delegation drop-in and adds systemd.unified_cgroup_hierarchy=1 to every boot entry. It does not reboot the server — the argument only takes effect on the next boot, and when to take a hosting machine down is your decision, not the panel’s. Run it, then reboot when it suits you; status says reboot pending in the meantime. It asks for confirmation unless --yes is given, and running it twice changes nothing.

The same reading and the same button are on Settings → Container runtime in the panel.

Terminal window
corepanel app stop <account> api
corepanel app delete <account> api

delete unpublishes the application, stops it, and removes its unit, program tree and socket. Its data directory is kept unless --purge-data is given: it is the one thing a redeploy cannot recreate. Deleting a domain that still publishes an application is refused with a message naming them; corepanel domain delete --force removes them in cascade.

Manages the host firewall. CorePanel drives firewalld directly: a corepanel zone that drops inbound traffic by default, the ports of the installed stack opened explicitly, and two address lists — blocked addresses and a bastion allowlist. The concepts are explained in Host Firewall; this is the command reference.

Terminal window
corepanel firewall status
corepanel firewall seed
corepanel firewall enable [--yes]
corepanel firewall disable
corepanel firewall factory-reset [--yes] [--stop-firewall]
corepanel firewall ports list
corepanel firewall ports open <port|from-to> [--udp] [--source CIDR]... [--comment C]
corepanel firewall ports close <port|from-to> [--udp] [--force]
corepanel firewall ports edit <port|from-to> [--udp] (--source CIDR... | --any) [--comment C]
corepanel firewall deny <ip|cidr> [--ttl 24h] [--comment C]
corepanel firewall deny list [--offset N] [--limit N]
corepanel firewall deny remove <ip|cidr>
corepanel firewall deny clear [--yes]
corepanel firewall allow <ip|cidr> [--comment C]
corepanel firewall allow list [--offset N] [--limit N]
corepanel firewall allow remove <ip|cidr>
corepanel firewall autoblock
corepanel firewall autoblock set [--enable|--disable] [--sources L] [--ttl D] [--max-ttl D] [--cooldown D]

Four rules explain the whole group:

  • Managing the firewall requires Pro or Business; status and disable never do. A server whose licence lapsed while the firewall was enforcing must always be able to see that state and end it.
  • This group is the emergency exit. Locked out of the panel? SSH in and run corepanel firewall disable: firewalld stops immediately and nothing is destroyed.
  • The seed cannot cut SSH. The port sshd listens on is read from the running daemon, not guessed from a config file, so enabling the firewall over an SSH session does not end it. Closing that port, or 443, is refused unless --force is passed.
  • Expiry belongs to CorePanel, not the kernel. Blocks are stored flat and released by a sweeper that runs once a minute and once at startup, so a block survives a reboot and still expires on time.

Reports whether firewalld is installed, running and managed by CorePanel, the default zone and its inbound policy, the backend, the rule and list counts, and anything that would stop management from working. When the server is unprotected, or firewalld is running unmanaged, the output says which command fixes it.

Prints the ports enable would open — port, protocol and the service each one serves — without opening them. Worth reading before adopting a server you reach over the network.

Adopts the firewall: builds the zone, creates the address lists, applies the seed, and only then makes the zone default, so the step that starts dropping traffic happens last. Prints the seed and asks for confirmation; a non-interactive run must pass --yes. Running it again on an adopted server repairs anything missing.

Adoption is refused, with an explanation, when firewalld is not installed, when it is on the deprecated iptables backend, or when the host has direct rules.

Stops firewalld. The host accepts everything again immediately; every rule stays in firewalld’s permanent configuration and enable puts it back. Asks nothing, and no edition may refuse it.

Removes CorePanel’s zone, its rules and both address lists, and hands the default zone back to what it was before adoption. Blocked and allowed addresses are destroyed with the lists. This is not how the firewall is turned off — disable is, and it is reversible. --stop-firewall also stops firewalld afterwards.

A port is a single number (8080) or a range (40000-50000), TCP unless --udp is passed. --source restricts it to source networks (repeatable, or comma-separated); with none, the port is open to everyone.

edit replaces a rule’s sources with the new ones in place before the old ones are withdrawn, so an address that keeps access never loses it for an instant. The port and protocol are the rule’s identity and cannot change. Dropping every restriction is explicit (--any), and omitting --comment keeps the comment the rule already had.

Blocks an address or network, permanently or until a deadline (--ttl, from 60s to 168h). deny list shows the time left, the provenance and the comment for each entry; an address the kernel holds that CorePanel has no record for is listed as untracked rather than hidden. Unblocking something that is not blocked succeeds, so deny remove is safe in a script that does not check first.

The bastion allowlist. An allowed address is accepted before every other rule, so it reaches ports that are closed to everyone else and cannot be blocked. For the office or the jump host access must never be lost from — and nothing else.

The automatic brute-force policy. CorePanel’s own detectors report the addresses that cross their threshold, and this decides what happens to them: the first ban lasts --ttl (30m by default), every repeat offence doubles it, and --max-ttl (7 days) is where the doubling stops. --cooldown (10m) is how long an address is left alone between escalations, so the counter measures how often an attacker comes back rather than how fast they knock.

--sources replaces the whole list, so pass every source that may block; an empty value means none. set changes only the flags given.

Three addresses can never be banned, whatever the policy says: this server’s own, the loopback, and anything on the bastion allowlist. An address an administrator blocked by hand is never escalated either.

Automatic blocking requires Pro or Business. On Personal the detectors keep reporting, so the attack stays visible, and nothing is blocked; reading the policy works on every edition.

Terminal window
# What is the firewall doing right now?
corepanel firewall status
# See exactly what adopting this server would open, then do it
corepanel firewall seed
corepanel firewall enable
# Provisioning: adopt without a prompt
corepanel firewall enable --yes
# Locked out of the panel — SSH in and stop filtering
corepanel firewall disable
# Open the FTP passive range, and a staging port only to the office
corepanel firewall ports open 40000-50000
corepanel firewall ports open 8443 --source 203.0.113.0/24 --comment "staging"
# The office moved: replace the sources without closing the port
corepanel firewall ports edit 8443 --source 198.51.100.0/24
# Block an address for a day, and a whole network for good
corepanel firewall deny 203.0.113.7 --ttl 24h --comment "ssh brute force"
corepanel firewall deny 198.51.100.0/24 --comment "scanner"
# What is blocked, and for how much longer?
corepanel firewall deny list
# Never lock out the jump host
corepanel firewall allow 203.0.113.10 --comment "office"
# What gets banned automatically, and for how long?
corepanel firewall autoblock
# Ban harder and for longer, but only on panel logins
corepanel firewall autoblock set --enable --sources panel --ttl 1h --max-ttl 48h
# Keep seeing the attacks, stop acting on them
corepanel firewall autoblock set --disable

Reads the authentication activity of the server’s mail and FTP services. corepanel-auth brokers every credential check on the box — IMAP, POP3, SMTP, FTP, plus the certificate and mail-map operations around them — and records each one with the account, the source address, the country it resolved to and, when it failed, why. The store itself is described in Mail System.

Terminal window
corepanel auth events [--service S] [--user U] [--ip IP] [--country CC] [--result R] [--reason R] [--since D] [--limit N]
corepanel auth attackers [--since D] [--limit N]
corepanel auth stats [--since D]
corepanel auth user <account> [--since D] [--limit N]

Available on every edition. Seeing an attack is never gated; blocking one automatically is what requires Pro or Business (corepanel firewall autoblock).

Three things are worth knowing before reading the output:

  • No password material is ever recorded — not the value, not a hash, not a length. That is a rule of the store, not a display choice, so there is nothing of the kind to print.
  • Recording an event never delays a login. When the writer falls behind, the store drops the event and counts it. corepanel auth stats prints that count: when it is non-zero, every number beside it is a floor rather than a total.
  • --since takes a duration (30m, 24h, 168h) and defaults to 24 hours.

Lists attempts newest-first. --service accepts ftp, imap, pop3, smtp, extcert or sync, and the usual aliases (imaps, submission) resolve to the same rows. When the filter matches more than --limit, the footer says so rather than letting a truncated list read as the whole story.

The source addresses with the most failed logins in the window, worst first. The ACCOUNTS column is the one to read: one account failing repeatedly from a single address is usually a stale password on somebody’s phone, while the same address failing against many accounts is a dictionary run. That distinction is what decides whether an address is ever reported to the firewall.

The window against the one before it, followed by the health of the audit trail itself: how many events are stored, the retention window, whether the row cap was reached, how many events were dropped, and whether country lookup is working.

A third block reports the brute-force detector: how many addresses it is watching, how many responses it delayed, how many it reported to the firewall — and how many reports it deliberately withheld. Those refusals are listed separately rather than folded into a total, because “nothing was blocked” has several very different meanings:

LineWhat it means
Addresses with a recent sign-inSomebody authenticated successfully from them in the last seven days, so they cannot be blocked automatically at all
Withheld — address had signed inA report was ready and dropped for exactly that reason
Withheld — events did not confirmThe running counters proposed a block the stored events did not support
Slow campaigns foundAddresses working through accounts that do not exist here over the last 24 hours, however slowly — the tier that catches what real attacks look like. Printed even at zero, because “looked and found nothing” is a different answer from “never looked”, and normally higher than the number reported: the gap is the immunity and the cooldowns
Delays skippedThe concurrency ceiling was reached and a response was answered immediately instead of being held
Automatic blocking is pausedCore answered that this server will not block — the edition does not include it, or the operator turned it off. Detection and the delays carry on

The thresholds in force are printed with them, so the numbers can be read without consulting this page. See Access protection for what each one protects.

One account’s record. The header spans its whole retained history — last success, last failure, how many distinct addresses it was used from, which countries — while the list below covers only the requested window, so an empty list means “nothing in the last day”, not “never logged in”.

Terminal window
# Everything that failed in the last hour
corepanel auth events --result fail --since 1h
# Who is hammering the mail server this week
corepanel auth attackers --since 168h
# A customer says their mail client stopped working
corepanel auth user ana@example.com --since 48h
# Only FTP, only from one country
corepanel auth events --service ftp --country CN
# Is the audit trail keeping up?
corepanel auth stats

Issues and withdraws the machine credentials that let a program use CorePanel’s JSON-RPC API — a WHMCS install, a provisioning script, a monitoring probe. See API Tokens for what a token can reach and how to use one.

Terminal window
corepanel api-token create <name> --scope S [--scope S ...] [--expires DATE]
corepanel api-token list [--all]
corepanel api-token revoke <token-id>

Issuing a token requires Pro or Business. Listing and revoking work on every edition — a lapsed licence must never stand between an operator and a credential they want to withdraw.

Prints the token once and never again. --scope is repeatable and at least one is required:

ScopeWhat it allows
readonlyList and read. Safe for monitoring.
provisioningCreate accounts, suspend and unsuspend them, change packages and passwords. Implies readonly.
terminateDelete accounts. Never implied by anything.

--expires takes YYYY-MM-DD or a full RFC3339 timestamp; a bare date means the end of that day. Omitted, the token does not expire.

Shows active tokens with their prefix, scopes, status and last use. Revoked ones are hidden unless --all is given.

Withdraws a token immediately. The row is kept rather than deleted, so a token named in an old audit line can still be resolved. Revoking an already-revoked token succeeds.

Terminal window
# A token for WHMCS that provisions and suspends, but cannot delete
corepanel api-token create "WHMCS production" --scope provisioning
# Add cancellations as a second, separate credential
corepanel api-token create "WHMCS terminations" --scope provisioning --scope terminate
# A monitoring credential that expires at the end of the year
corepanel api-token create "status page" --scope readonly --expires 2026-12-31
# What can reach this server right now?
corepanel api-token list
# The laptop was stolen
corepanel api-token revoke 4

Issues and withdraws the credentials an MCP client presents to read this server on your behalf — your Claude Code, your Cursor, whatever you already work in. See MCP for AI agents for what an agent can ask and what it cannot do.

Terminal window
corepanel agent-token create <name> [--operate] [--expires DATE | --never-expires]
[--allow ADDR]...
corepanel agent-token list [--all]
corepanel agent-token sources <token-id> <address>... | --clear
corepanel agent-token activity [--since D] [--token ID] [--tool NAME] [--action A]
[--outcome O] [--ip ADDR] [--before ID] [--limit N]
[--details | --json]
corepanel agent-token revoke <token-id>

Available on every edition, Personal included. It is not an API token, and the differences are the whole design:

  • Read-only unless --operate is asked for. It answers questions about this one server and carries none of the rights that create, suspend or delete an account, at any setting. --operate adds the right to change what already exists, through a two-step confirmation.
  • It expires in 90 days unless you say otherwise. An API token lives on a server you control; this one lives in a configuration file on a workstation, in a dotfiles repository, in a backup.
  • It can be pinned to the addresses it may be used from. --allow, off unless you ask for it. It is the only measure that stops a credential somebody else got hold of; expiry, revocation and the activity log all deal with a leak once you know about it.
  • The two are not interchangeable. An agent token presented to the API is refused, and an API token presented to the MCP endpoint is refused.

Minting the first one also turns the MCP endpoint on: until a usable agent token exists, /mcp answers as though the path did not exist. Revoking the last one turns it off again.

Prints the credential once. Only its hash is stored, so nothing can print it again.

--expires takes YYYY-MM-DD or a full RFC3339 timestamp; a bare date means the end of that day. Omitted, the token expires in 90 days — omitting it does not mean “never”. --never-expires does, and has to be asked for by name.

--operate grants the write right. What it buys is the ability to change what already exists — restarting an application, backing up an account, flushing a site’s page cache — and every such change takes two calls: the agent’s first call returns a plan and changes nothing, and the change happens only on a second call carrying the confirmation that plan issued, which lasts 60 seconds and works once. It never reaches an account’s lifecycle, and it deletes nothing. See Letting it change things.

--allow pins the credential to an address or a CIDR range, IPv4 or IPv6, and is repeatable. A token presented from anywhere else is refused with the same “invalid token” a wrong secret produces — the response must not tell a caller which check it failed, or the failure confirms that the credential it holds is real. Only the log, and activity --action auth_refused, say what happened.

Omitted, the token works from wherever it is copied to. That is the default because a list breaks the day you work from somewhere new, and the answer to that is agent-token sources, not turning the list off.

If you use corepanel mcp on the server itself, put 127.0.0.1 on the list: the bridge reaches the endpoint over loopback and sends no address of its own. Loopback is never implicit — every local user on the machine can reach that address.

A token cannot be widened afterwards. Read-only is what you get by not choosing, and changing your mind means revoking and minting again.

The access: line reports the right the token actually got, not the one you asked for. --operate against a corepanel-core that predates the write right succeeds and mints a read-only token — an unknown parameter is ignored, not refused — so the command says so in as many words rather than letting you find out when a change is turned away. The sources: line is the same shape for the same reason: --allow against such a core mints a token pinned to nothing, and the command says so.

Replaces the list of addresses a token may be presented from. The list is a replacement, not an addition: whatever you pass is what the token ends up with.

Each entry is an address or a CIDR range. A bare address is stored as a single-host range, so 192.0.2.7 and 192.0.2.7/32 are the same rule — and what the command prints back is what the server stored, which is what it will compare against, not what you typed.

A range with host bits set is refused rather than widened. 10.0.0.5/8 is a typo for one address far more often than it is a request for sixteen million, and quietly storing 10.0.0.0/8 would grant them; the error names both readings.

--clear removes the restriction and has to be asked for by name. A command that read “no addresses given” as “allow from anywhere” would unpin a credential on a mistyped invocation, and nothing would look wrong afterwards — the token keeps working, from everywhere.

What this command cannot change is what the token may do. That is fixed when the token is minted; widening it is still a revoke and a new token.

FlagMeaning
--clearRemove the restriction, making the token usable from any address

Shows active tokens with their prefix, access, status, expiry, last use and the address it was last used from. API tokens are never in this list. Revoked ones are hidden unless --all is given.

The Access column answers “which of these could have changed my server”: read-only or read + operate. What one of them actually changed is activity --action tool_apply.

The Sources column says where each credential may be used from. any is a token that works from wherever it is copied to; anything else is one that does not.

The address column is the other one worth reading: a credential in use from an address you do not recognise. It is half the question — a credential somebody is guessing at never reaches this table, because it never resolves to a row. That half is activity.

What the /mcp endpoint was asked for, newest first, read from the machine itself. The panel shows the same trail at Server → Agent tokens → Activity; this is the answer for a terminal, which on a server is usually where you are.

ActionWhat it means
tool_callA tool was called. --details shows the arguments it was given
tool_applyA change was authorised and carried out on this server
initializeAn MCP client completed the handshake — somebody attached one
tools_listA client asked what this server offers
auth_refusedA token was presented and did not resolve

tool_apply is the short answer to “did my agent change anything”. A write tool takes two calls; the first returns a plan and touches nothing, and is recorded as an ordinary tool_call. Only the second is a tool_apply, so --action tool_apply is every change ever made through this endpoint and nothing else. A failed one is still a tool_apply — the outcome says whether it landed.

auth_refused is what the command is worth running for. A refusal names nobody: the credential did not resolve, so there is no token to attribute it to, and the string presented is never stored — the address is the whole of what is knowable, and the TOKEN column says (refused) rather than going blank. A request that carried no credential at all is deliberately not recorded; those are port scans and stale bookmarks.

FlagWhat it does
--sinceA duration: 30m, 24h, 168h. Omitted, everything the store still holds
--tokenOne agent token, by the id agent-token list shows
--toolOne tool, e.g. read_logs
--actiontool_call, tool_apply, initialize, tools_list or auth_refused
--outcomeok, tool_error, refused or error
--ipOne source address
--beforeOnly events older than this id — how you page backwards
--limitRows to show, default 25, maximum 1000
--detailsOne block per event, with the arguments a tool was called with
--jsonThe page as it came off the wire, for a script

Paging is by id rather than by offset, because the trail only grows at the top and an offset would repeat rows. When older events match, the command prints the id to pass to --before.

Whatever the store dropped is printed under the list without being asked for. The /mcp endpoint never waits for the audit trail, so a burst of traffic — or a core that was unreachable — costs events rather than latency, and a trail that is quietly losing rows looks exactly like a quiet one.

Two answers are not faults, and the command says so rather than reporting a failure: a corepanel-core that predates the trail, and one whose store did not open. Both leave your tokens working; what is missing is the record of what they did.

Withdraws a token immediately. The id of an API token is not found here — those are revoked with corepanel api-token revoke.

Terminal window
# A token for the laptop you work from, expiring in 90 days
corepanel agent-token create "laptop"
# One that may also change things, through the two-step confirmation
corepanel agent-token create "deploy box" --operate
# One that never expires, for a workstation that never leaves the office
corepanel agent-token create "office desktop" --never-expires
# One that only works from the office, and from this server itself
corepanel agent-token create "office desktop" --allow 192.0.2.0/24 --allow 127.0.0.1
# Change where an existing token may be used from
corepanel agent-token sources 3 192.0.2.0/24 198.51.100.0/24
# Let it be used from anywhere again
corepanel agent-token sources 3 --clear
# Which agents can read this server, and from where?
corepanel agent-token list
# What has been asked of the endpoint lately?
corepanel agent-token activity
# Has anybody been trying credentials that are not mine?
corepanel agent-token activity --action auth_refused --since 168h
# Everything an agent has ever CHANGED on this server
corepanel agent-token activity --action tool_apply --since 720h --details
# What did the laptop's token do today, arguments included?
corepanel agent-token activity --token 101 --since 24h --details
# That address is not mine
corepanel agent-token revoke 7

Speaks MCP on stdin and stdout, for a client running on this same server.

Terminal window
corepanel mcp [--token-file PATH] [--url URL]

It is a pipe to the endpoint the panel already serves at /mcp: every message is forwarded, unchanged, to corepanel-api on the loopback address, and the answer is written back. Nothing else changes — the same tools, the same two-step confirmation before anything is altered, and the same audit trail, which corepanel agent-token activity shows exactly as it would a call over HTTPS.

Use it when the client runs on the box itself and pointing it at https://<panel>/mcp would mean that hostname resolving there and that certificate being trusted by that client. This talks to http://127.0.0.1:16087/mcp: no hostname, no certificate, no proxy. Some MCP clients also accept only a command, never a URL.

It needs an agent token and does not create one. Mint it with corepanel agent-token create, then put it in COREPANEL_AGENT_TOKEN or in a file named by --token-file. There is deliberately no --token flag: an argument is visible in ps to every user on the server.

FlagMeaning
--token-fileRead the token from this file instead of $COREPANEL_AGENT_TOKEN. It must contain the token and nothing else; a warning is printed if other users can read it
--urlThe endpoint to forward to (default http://127.0.0.1:16087/mcp)

Nothing but protocol goes to stdout. Everything a human needs to read is on stderr, where an MCP client will not mistake it for a message.

Terminal window
# Mint the credential once
corepanel agent-token create "local agent"
# What a client's configuration looks like
# {
# "mcpServers": {
# "corepanel": {
# "command": "corepanel",
# "args": ["mcp"],
# "env": { "COREPANEL_AGENT_TOKEN": "cpa_..." }
# }
# }
# }
# Or keep the credential in a root-only file instead of the configuration
corepanel mcp --token-file /root/.corepanel/agent-token

See MCP for the whole picture.

Controls whether this server answers the WHM API on :2087 — the compatibility layer that lets a WHMCS which has been provisioning this machine through cPanel keep working after a transform, with nothing changed on the WHMCS side. See WHMCS for what it is and when you want it.

Terminal window
corepanel whmcompat status [--json]
corepanel whmcompat enable [--json]
corepanel whmcompat disable [--json]
corepanel whmcompat sso enable
corepanel whmcompat sso disable
corepanel whmcompat redirects enable
corepanel whmcompat redirects disable
corepanel whmcompat import-credential [--user U] [--kind token|accesshash] [--label L]
corepanel whmcompat credentials
corepanel whmcompat revoke-credential [--user U] (--digest PREFIX | --all)
corepanel whmcompat selftest

Answers two questions together, because either alone is misleading: whether the server is configured to serve the WHM API, and whether a daemon is actually listening. A switch that is on with no package installed serves nothing; an installed package with the switch off is running and holding no port. Both are normal, and both look like a fault if you only see half of it.

The exit status is 0 in every one of those states — not serving the WHM API is a supported configuration, not an error. Use --json to branch on it.

Open and close :2087. Both take effect within half a minute and neither needs a restart, which matters most in the OFF direction: closing the port must not wait for somebody to remember there is a service to bounce. Requests in flight are allowed to finish.

They also open and close the port in the host firewall, and print which zone they touched:

WHM API compatibility: enabled
Passwordless sign-in: disabled
Daemon: listening on [::]:2087
Credentials held: 1
Firewall: port 2087/tcp opened in the "public" firewall zone

The zone is whichever one is in force — the corepanel zone on a server that adopted the host firewall, the distribution’s default zone on one that did not, which is where the installer already seeds the stack’s ports. On a server with firewalld stopped or not installed, nothing is filtering the port and the line says so instead of claiming a change.

A second switch, over the one WHM command that signs a customer into their panel without their password — create_user_session, the “Log in to cPanel” button in a WHMCS client area. Off by default, and off on servers that were already serving :2087 before this switch existed.

It is separate from the layer’s own switch because it is a different kind of thing to grant. Everything else the layer does is loud: a password change stops the customer’s own password working, a suspension takes their site down. A minted session is silent — it is the customer’s own session, which is what makes it the right thing for that button and what makes it worth deciding on. Most billing setups only provision and suspend and never need it.

Both directions take effect within half a minute, without a restart. Turning it on requires Business; turning it off never does.

This does not affect the native CorePanel module for WHMCS. That module reaches the same feature through an API token this server issued, over the port the panel already owns, and its login button works whatever this switch says.

corepanel whmcompat redirects enable / disable

Section titled “corepanel whmcompat redirects enable / disable”

A third switch, over the legacy cPanel ports a browser lands on: 2082, 2083, 2086, 2095 and 2096. Off by default, including on servers that already serve the WHM API.

This half of the compatibility layer is not for a billing system at all. On a cPanel server, :2083 is the URL every customer has bookmarked and :2096 is how they reach their webmail. After a transform, those are a refused connection — no page, no explanation — for everybody at once, and it is the most visible breakage of the whole migration.

With it on, this server answers those ports with a redirect and nothing else:

PortTLSA browser is sent to
2082nothe panel
2083yesthe panel
2086nothe panel
2095nowebmail.<domain>, for the domain it asked for
2096yeswebmail.<domain>, for the domain it asked for

Available on every edition, unlike the two switches above — that is why it is a switch of its own. Enabling the WHM API requires Business, so a transformed Personal or Pro server could otherwise never reach a redirect that costs nothing and grants nothing, while its customers’ bookmarks are just as broken.

What these ports will not do:

  • They never redirect an API client. A /json-api/ path, or any request carrying an Authorization header, gets a 404 — or, on :2086 alone (cPanel’s cleartext API port), a parseable refusal naming https://<hostname>:2087. Redirecting a provisioning call to a login page is a failure that reports success.
  • They never redirect to an address from the request. The webmail target is built from the Host header only after the panel confirms this server serves that domain; anything else falls back to the panel. Nothing from the path or query string is ever used.
  • They never send a 301, and never HSTS.

The ports are opened and closed in the host firewall along with the switch.

status prints the switch in both states and, when it is on, names which of the five ports are actually bound. Fewer than five means something else on this server holds the rest.

Teaches this server the credential the billing system already uses, read from stdin.

FlagDefaultMeaning
--userrootThe WHM user the credential authenticates as
--kindtokentoken (a WHM API token) or accesshash (the contents of /root/.accesshash)
--labelA note for whoever revokes it later

The secret is read from stdin and never from a flag — a credential in a flag is a credential in the shell history and in every ps on the box. Importing the same credential twice changes nothing.

Lists what the server accepts: user, kind, label, import date, and the digest that identifies each credential. There are no secrets to print — the store keeps digests, which is what makes holding a hosting provider’s token safe.

The digest is what names a credential when revoking one. A label cannot: several credentials can carry the same note.

Stops this server accepting a credential.

FlagDefaultMeaning
--userrootThe WHM user whose credential is being revoked
--digestDigest prefix from credentials; names exactly one credential
--alloffRevoke every credential this user holds
--yes, -yoffDo not ask for confirmation

One of --digest or --all is required, and they cannot be combined. Revoking everything has to be spelled out: an empty store accepts nothing, so --all is how a server stops answering its billing system entirely — there is no way to ask for that by leaving a flag out, and it asks for confirmation unless you pass --yes.

A revoke that matched nothing says so rather than reporting a revocation. And since only the digest was ever stored, a revoked credential comes back only by importing the secret again.

Drives a real WHM API request against this server over loopback — the port, TLS, the credential header, the response envelope and the call into the panel, with nothing stubbed — using a one-shot credential the daemon mints for it.

It proves this side. Only WHMCS proves WHMCS: when it passes, press Test Connection in WHMCS → System Settings → Servers.

Terminal window
# Is this server serving the WHM API, and is anything listening?
corepanel whmcompat status
# After a transform: open the port the hosting provider's WHMCS is already pointed at
corepanel whmcompat enable
# Teach it the token WHMCS already holds
corepanel whmcompat import-credential --user root --kind token < token.txt
# The hosting provider still authenticates with the old access hash
cat /root/.accesshash | corepanel whmcompat import-credential --kind accesshash
# What does this server accept, and since when?
corepanel whmcompat credentials
# That one belonged to a WHMCS that is gone
corepanel whmcompat revoke-credential --digest a9e70ee8
# Prove our side end to end before touching WHMCS
corepanel whmcompat selftest
# Close it again
corepanel whmcompat disable

The PHP this server runs: which versions are installed, which can be, what extensions each one loads, and the per-request limits every account is held to. See How PHP Runs for the version model and PHP Isolation and Limits for what each limit does.

corepanel php versions
corepanel php install <version> [--yes]

versions lists every PHP version on the server — how many accounts run each, and how long PHP upstream still supports it — and underneath, the versions available to install.

“End of life” means end of life upstream. A package repository may go on building a version PHP itself no longer fixes; the accounts on it stop receiving security fixes whatever the repository does.

install adds a version and brings it under CorePanel’s configuration, so it gets the same per-account pools as every other version. Only versions versions lists are accepted. Nothing is moved onto it: accounts change one at a time, with corepanel account update <account> --php.

Which versions are offered depends on the operating system: 7.4 and 8.0 to 8.5 everywhere, plus 5.6 and 7.0 to 7.3 on EL8, the only release Remi still publishes those for. See PHP versions.

It runs a package transaction and can take several minutes; the package manager’s output is printed when it finishes, including on failure. Installing an end-of-life version asks for confirmation — allowed, since 7.4 is what most cPanel migrations arrive on, but never silent. --yes skips the prompt and is required where there is no terminal to ask on.

# What this server runs, and what it could run
corepanel php versions
# Add the version a cPanel migration needs, then move one account onto it
corepanel php install 8.1
corepanel account update acme --php 8.1
# In a provisioning script, where nothing can answer a prompt
corepanel php install 7.4 --yes
corepanel php extensions [--php <version>]
corepanel php extensions install <version> <extension> [--yes]
corepanel php extensions remove <version> <extension> [--yes]

extensions lists what each installed version loads, from the sixteen CorePanel manages — imagick, redis, memcached, soap, ldap, imap, bcmath, posix, ioncube-loader, sodium, gmp, tidy, apcu, igbinary, mongodb, mcrypt. Anything an administrator installed over SSH is listed separately as installed outside CorePanel: it works, it is simply not managed here. See PHP Extensions and ionCube.

An extension belongs to a PHP version, not to an account: there is one php-fpm master per version, so installing one gives it to every account running that version.

The state of each one:

StateMeaning
installedThe package is here and the interpreter loads it
NOT LOADEDThe package is here but PHP does not load it — its ini was disabled by hand
availableNot installed; it can be installed from here
unavailableYour package repository does not offer it for that version
unknownThe repository could not be asked at all — not the same as unavailable
not for this versionIt does not exist for that PHP branch. Normal, not a fault

install and remove take the extension’s logical name; which package that is depends on the version and is decided by the daemon, since the repository spells the same extension differently from one branch to the next.

Either direction restarts that version’s php-fpm master, interrupting requests in flight for every account on it — the packages carry a trigger that does it whenever their configuration directory changes. Both ask before running; --yes skips the prompt and is required where there is no terminal.

A removal is refused when another installed package depends on the one being removed, and says which: redis requires igbinary, so removing igbinary would take redis with it and break caching for somebody who was never asked.

The package manager’s output is printed as it arrives. Interrupting the command stops the watching, not the transaction — that runs in corepanel-sys, and killing rpm halfway would leave the package database half-applied.

# What every version loads
corepanel php extensions
# Just one version
corepanel php extensions --php 8.3
# WooCommerce needs imagick; object caching needs redis
corepanel php extensions install 8.3 imagick
corepanel php extensions install 8.4 redis
# Take it away again, having checked nothing else needs it
corepanel php extensions remove 8.4 redis
corepanel php limits [--json]
corepanel php limits set [--memory-limit MB] [--max-execution-time S] [--max-input-time S]
[--max-input-vars N] [--upload-size MB] [--post-size MB]
[--terminate-after S] [--adopt-defaults] [--json]

These are per request, not per account: the worst case one account can reach is pm.max_children (8) multiplied by the memory limit, and the output prints that product for you.

The header tells you whether the values were configured or are CorePanel’s defaults, and that difference decides what happens to pools. While nothing is configured the limits are a floor: a pool that already names a value keeps it. Once you set anything here, every CorePanel-managed pool is rewritten to match.

FlagSetsCan a site override it?
--memory-limitmemory_limit, in MB (64–16384)No
--terminate-afterrequest_terminate_timeout, in seconds (30–3600)No
--max-execution-timemax_execution_time, in seconds (10–3600)Yes, with a .user.ini
--max-input-timemax_input_time, in seconds (10–3600)Yes
--max-input-varsmax_input_vars (100–100000)Yes
--upload-sizeupload_max_filesize, in MB (1–16384)Yes
--post-sizepost_max_size, in MB (1–16384)Yes

Only the flags you pass change, and with no flags it does nothing. On a server where nothing has been configured the values shown are defaults acting as a floor; holding every pool to them is a real change and takes --adopt-defaults. Applying reloads one php-fpm master per PHP version, not one per account.

Two rules are enforced, because breaking either produces a silent wrong answer rather than an error:

  • --terminate-after must exceed --max-execution-time, or php-fpm kills the worker before PHP can write the error and every runaway script becomes a 502 with an empty error log.
  • --post-size must be at least --upload-size, or PHP rejects a large upload on the POST size first and the upload limit can never be reached.

Requires super-admin: the limits are server-wide, so a reseller cannot read or change them.

Terminal window
# What is every account held to right now?
corepanel php limits
# A WooCommerce shop needs more room; everything else stays as it is
corepanel php limits set --memory-limit 512
# Bigger uploads for a media-heavy server
corepanel php limits set --upload-size 256 --post-size 256
# Free stuck workers sooner, keeping PHP as what reports the error
corepanel php limits set --max-execution-time 60 --terminate-after 120
# For scripting
corepanel php limits --json

What every hosting account is using, and how much it is allowed. See PHP Isolation and Limits for the mechanism.

corepanel resources list [--json]
corepanel resources set --limit N|0|none [--account NAME] [--json]

The limit is concurrent PHP requests per account. An account already running as many as it is allowed has its next request answered 508 while its neighbours are untouched — that is what turns the per-request memory_limit into a per-account bound: an account can hold at most its limit multiplied by that memory limit.

list prints one row per account with memory, CPU, process count, PHP requests in flight, the peak, and how many were refused; the header adds the ceiling for all of PHP together and how much of it is in use.

--limitMeaning
NAt most N concurrent PHP requests
0Unlimited, deliberately — how one account is exempted on a server that limits the others
noneClear it: the server stops limiting, or the account follows the server default again

0 and none differ on the server: none means nobody has decided anything and no limits are written at all, which is how a freshly upgraded server behaves. Nothing is limited until you set a default here.

Applying reloads the web server. Requests already running are never killed by lowering a limit — they finish, and the new ceiling applies to what arrives next.

Requires super-admin.

Terminal window
# Who is using what?
corepanel resources list
# Hold every account to 4 concurrent PHP requests
corepanel resources set --limit 4
# One busy shop gets more room
corepanel resources set --account shop.example --limit 12
# ...and one account is exempt entirely
corepanel resources set --account internal.example --limit 0
# Back to following the server default
corepanel resources set --account internal.example --limit none
# For scripting
corepanel resources list --json

The CLI connects to corepanel-core through a Unix socket. By default it uses:

/run/corepanel-core/corepanel-core.sock

Override the socket path with the COREPANEL_CORE_SOCKET environment variable — useful in non-standard deployments or when testing against a different Core instance:

Terminal window
COREPANEL_CORE_SOCKET=/run/corepanel-core/corepanel-core.sock corepanel status