Plans are service presets that may be assigned to an account. ApisCP contains a default present named "basic" that is a good fit for non-power users to balance resource consumption and accessibility.

# Management helpers

A variety of helpers exist to add, delete, modify, suspend, activate, import, and export a domain. These helpers have corresponding API commands in the admin (opens new window) module paired with each topic. $flags denote any feature, besides -c service,param=value, passed to the helper. Example of such flags are:

Flag Scope Usage
-c edit, add, import Override default service values
--reset edit Resets site to plan defaults
--dry-run edit, delete Shows actions without performing
--backup edit Backup metadata prior to editing
--reason suspend Specify suspension reason
--force delete, edit Delete a domain in a partially edited state. Bypass good judgment.
--since=timespec delete Delete domains suspended since timespec ("now", "last month", unix timestamp)
--filter delete Delete domains matching (regex) suspension reason
--reconfig edit Reconfigure all services implementing ServiceReconfiguration
--all edit Target all domains
--help all Show help
--output=type all Set the output format. Values are "json" or unset

These flags may be passed to the API whenever $flags is an accepted parameter. Flags simply present are passed as '[name:true]' whereas flags that accept values are passed as '[name:value]'.

As an example, EditDomain -c cgroup,enabled=0 --all disables cgroup resource enforcement on all sites.

# AddDomain

AddDomain creates a site from command-line. Multiple parameters can be provided to alter the services assigned to an account. Nexus within the Administrative panel is a frontend for this utility.

# Basic usage

AddDomain -c siteinfo, -c siteinfo,admin_user=myadmin

Creates a new domain named with an administrative user myadmin. The email address defaults to and password is randomly generated.

Let's alter this and set an email address, which is used to contact the account owner whenever a Web App is updated or when the password is changed. Let's also prompt for a password. But first let's delete because domains must be unique per server.

AddDomain -c siteinfo, -c siteinfo,admin_user=myadmin -c siteinfo, -c auth,passwd=1

ApisCP will prompt for a password and require confirmation. Email address will be set to

# Tweaking services

ApisCP includes a variety of services that can be enabled for an account. Some services must be enabled whereas others can be optionally enabled. Let's create an account that allows up to 2 additional users, disables MySQL, disables email, disables shell access, and disables addon domains. A single-user domain with limited access that may only upload files.

AddDomain -c siteinfo, -c siteinfo,admin_user=secureuser -c users,enabled=0 -c users,max=3 -c mysql,enabled=0 -c mail,enabled=0 -c ssh,enabled=0 -c aliases,enabled=0

# Flags

--plan|-p=name: apply named plan to account
--force: hook failure does not constitute addition failure
--bootstrap | --bootstrap=[true|false]: attempt to issue SSL certificate upon creation. Setting [letsencrypt] => auto_bootstrap tuneable implies this option. When enabled, --bootstrap=false or -c ssl,enabled=0 disables this feature.
--notify: send a welcome email to the email address specified in siteinfo,email. This template may be overridden in resources/views/email/opcenter/account-created.blade.php (see

# API usage

admin:add-site(string $domain, string $admin, array $opts = [], array $flags = []) is the backend API call (opens new window) for this command-line utility. $admin corresponds to siteinfo,admin_user and must be unique.

# EditDomain

EditDomain is a helper to change account state without removing it. You can toggle services and make changes in-place in a non-destructive manner. From the above example, it's easier to change the email address without destroying the account.

EditDomain -c siteinfo, -D

# Rename domain

A simple, common situation is to alter the primary domain of an account. Simply changing the domain attribute under the siteinfo service will accomplish this.

EditDomain -c siteinfo,

# Changing password

Changing the password is another common operation:

EditDomain -c auth,tpasswd=newpasswd site12

A new password is set in plain-text, "newpasswd". The third password alternative is cpasswd, which is a crypt() (opens new window)'d password. An optimal crypted password may be generated with auth_crypt (opens new window). Alternatively, cpcmd auth_crypt newpasswd may be used to create the crypted password or To note, EditDomain accepts either the primary domain of an account, an aliased domain of an account (addon domain), or the site identifier. Aliases are discussed next.

# Aliases

Aliases are domains for which the primary responds. Any alias also serves as a valid authentication mechanism in the user@domain login mechanism. Any alias without a defined document root will serve content from /var/www/html, which is the document root (opens new window) for the primary domain.

EditDomain -c aliases,aliases=['']


aliases,aliases is dangerous! It is not an append-only operation, meaning that whatever aliases value is is what is attached, nothing more and nothing less. A safer option is aliases_add_domain, aliases_remove_domain part of the API, which adds or removes domains in a singular process. This is part of cpcmd discussed later on.

# Dry-runs

A dry-run proposes changes without applying them.

EditDomain --dry-run -c siteinfo, site1
# site1:
#   siteinfo: { domain: }
#   apache: { webserver: }
#   ftp: { ftpserver: }

In the above example, changing siteinfo,domain implicitly renames that service field as well as apache,webserver and ftp,ftpserver.

# Mass edits

Two options complement EditDomain, --all and --reconfig.

--all targets all domains on a server. --reconfig applies service reconfiguration to all services that support the feature. It may be used to forcefully regenerate configuration after overriding configuration (see

EditDomain --all
EditDomain --reconfig
# or do both!
EditDomain --all --reconfig

# Targeting specific accounts

admin:collect(?array $params = [], array $query = null, array $sites = []): array is an API command to collect service values from matching accounts. This can be mixed with JSON formatting + jq to perform complex scripting without clunky parsing. For example, to fetch all sites that have the plan "basic" and reconfigure to "advanced":

# Install jq first
yum install -y jq
cpcmd -o json admin:collect '[]' '[siteinfo.plan:basic]' | jq -r 'keys[]' | while read -r SITE ; do
 echo "Editing $SITE"
 EditDomain -c siteinfo,plan="advanced" "$SITE"

# Resetting account

A reset is specified with --reset. Depending upon context-specific options, a reset will either apply the unchanged differences between plans or reset an account to default plan settings.

When --plan=NEWPLAN --reset is specified, only unchanged defaults are applied.

When --reset exists without a plan specifier, then the default plan settings are applied. A plan setting is only applied if the new plan's default value is not an array or not null. Certain parameters distinct to accounts, such as paswords, addon domains, and invoice are never reset.

# Changing plans

Plans are covered later in this section. Note the behavior when changing plans is that only the unchanged differences are applied. To reset all unprotected service variables to their new plan value, use --reset in conjunction with --plan as noted above.

# Flags

--plan|-p=name: apply named plan to account. Same as setting -c siteinfo,plan=NAME.
--force: hook failure does not constitute addition failure.
--reset: reset service parameters. Context-specific, see above note on reset!
--dry-run: show proposed changes without applying them.
--backup: create a backup of the metadata prior to editing. This metadata is stored in siteXX/info/backup. Each successive run overwrites this data.
--force: force change that may put the site in an inconsistent/disabled state after edit such as quota reduction below usage.

# Listing services

AddDomain -h will list all available services. These services map to resources/templates/plans/.skeleton/, which infer support data from lib/Opcenter/Service/Validators/Service Name/Service Var/.php.

# API access

admin:edit_site(string $site, array $opts = [], array $flags = []): bool allows editing of sites. See for implementing API access.

# Double-throw safety switches

MySQL, PostgreSQL, and DNS permit disablement without removing database configuration (or zone databases) through a double-throw safety switch ("DTSS") feature. When mysql, postgresql, or dns services are disabled, DTSS restricts further usage of creating new databases or users while preserving existing data.

To remove these databases from the account while preserving the account, the following DTSS conditions must be met at the same time. Likewise to preserve existing data, the following value must be set at deletion time.

Service DTSS Condition Preservation Condition
MySQL enabled=0, dbaseprefix=None dbaseprefix=None
PostgreSQL enabled=0, dbaseprefix=None dbaseprefix=None
DNS enabled=0, provider=None provider=None

For example, consider the following code snippet:

AddDomain -c siteinfo,domain=mytestdomain.test -c siteinfo,admin_user=testuser -c dns,provider=powerdns -c dns,enabled=1

# Create a dummy record called foo.mydomain.test on PowerDNS
cpcmd -d mytestdomain.test dns:add-record mytestdomain.test 'foo' A ''
# Confirm record exists
cpcmd -d mytestdomain.test dns:get-records foo

# Preserve DNS, but disable DNS for now...
# "null" is the same as "None" - historical quirk
EditDomain -c dns,provider=null mytestdomain.test
# Confirm record is missing
cpcmd -d mytestdomain.test dns:get-records foo

# Revert DNS provider. Zone is preserved.
EditDomain -c dns,provider=powerdns mytestdomain.test
# Confirm record exists
cpcmd -d mytestdomain.test dns:get-records foo

# Remove DNS for good from site
EditDomain -c dns,provider=null -c dns,enabled=0 mytestdomain.test
EditDomain -c dns,provider=powerdns -c dns,enabled=1 mytestdomain.test
# Record now missing
cpcmd -d mytestdomain.test dns:get-records foo

Likewise setting provider=None prior to deletion can be used to preserve DNS upon account deletion. Databases are allocated within each site's filesystem, so while it's possible to preserve MySQL/PostgreSQL databases and grants upon account deletion the underlying databases are still removed.

# DeleteDomain

Domains may be deleted using DeleteDomain. DeleteDomain accepts a list of arguments that may be either the site identifier, domain, aliased domain, or invoice (billing,invoice OR billing,parent_invoice service value). Invoices allow you to quickly group multiple accounts. Invoices are discussed briefly below.

# Flags

--since=TIMESPEC: only delete domains suspended TIMESPEC ago ("last week", "now", 1579934058).
--force: delete a domain in a partial edit state.
--dry-run: show what will be deleted without deleting.
--match=IDENTIFIER: delete domains that match IDENTIFIER in the suspension reason (regular expression, delimiter implicitly added).

# Advanced matching

New in 3.2.5 Templates may contain hidden markup prefixed by ; or # that is not visible to the end user. These may contain special identifiers used by --match=IDENTIFIER. For example assuming the following suspension message in siteXX/info/disabled:

Your account has been suspended.

; Some other internal notes

cpcmd -d auth:inactive-reason will report "Your account has been suspended" masking any additional markup prefixed with "#" or ";".

Likewise to target this site suspended over 30 days ago, DeleteDomain --dry-run --match=REASON-XYZ-123 --since="last month"

# API access

admin:delete-site(?string $site, array $flags = []): bool allows deletion of sites. See for implementing API access.

Passing null to $site and [since:timespec] to $flags allows deletion of suspended sites suspended timespec ago. Specify a timespec of "now" to delete all suspended sites. For example to show what sites would be deleted from the shell,

cpcmd admin:delete-site null '[since:now,dry-run:true]'

# SuspendDomain

Domains may be deactivated from the command-line using SuspendDomain. It accepts a list of arguments, which may be the site identifier, domain, aliased domain, or billing invoice.


A suspended domain revokes access to all services, except panel, as well as page access. Panel access may be overridden by setting [auth] => suspended_login to true in config.ini.

When a billing invoice is specified any site that possesses this identifier either as billing,invoice or billing,parent_invoice will be suspended.

# Reasons

New in 3.2.5 --reason=reason may be used to specify a suspension reason.

--template=NAME, if specified, uses the template NAME to refer to a template under resources/templates/opcenter/suspension. The reason value is passed to the template as $reason. This may be customized.

If a suspension reason is provided, the customer, upon login to panel UI will be shown this reason. Reason may be adjusted by setting [auth] => show_suspension_reason to false (see

Likewise if a suspension reason is given, DeleteDomain accepts --match=word to match a regular expression in the suspension reason. See DeleteDomain for more information.

# API access

admin:deactivate-site(string $site, array $flags = []): bool allows suspension of sites. See for implementing API access. $site may be any site identifier or billing invoice.

# ActivateDomain

Likewise a domain may be activated by using ActivateDomain. It acts similarly to SuspendDomain except that it undoes what SuspendDomain does.

ActivateDomain apiscp-XYZ123

Where apiscp-XYZ123 is a billing invoice assigned to the account via -c billing,invoice=apiscp-XYX123

# API access

admin:activate-site(string $site): bool allows reactivation of suspended sites. See for implementing API access. $site may be any site identifier or billing invoice.

# Plans

Plans are related to AddDomain and EditDomain in that they assign preset settings to a domain.

# Creating plans

New plans may be created using artisan.

cd /usr/local/apnscp
./artisan opcenter:plan --new custom

The plan is created within resources/templates/plans/custom. Changes may be made at your leisure. Use ./artisan opcenter:plan --verify PLAN to confirm the plan named PLAN.

A plan may copy another plan's definitions by specifying --base OLDPLAN. For example,

./artisan opcenter:plan --new nossh --base custom

Plans may be created thinly in which services from the default plan are inherited unless explicitly named. This allows simplification such as the following "ssh" service definition which disables just SSH access, inheriting all other plan defaults.

Consider the service "nossh", which has 1 file in resources/templates/plans/nossh named ssh with the following line:

enabled = 0

This is a valid plan definition and will inherit all other plan values from the default plan.

Additionally, the familiar -c flag may be used to feed individual overrides to a plan,

./artisan opcenter:plan --new nossh -c ssh,enabled=0

# Managing plans

The following commands imply ./artisan opcenter:plan is used.

PLAN --default: set the default plan, e.g. to set the default plan for accounts going forward, use ./artisan opcenter:plan --default custom. Note this is equivalent to using the cp.config Scope to change [opcenter] => default_plan.

PLAN --diff=COMPARE: compare PLAN against COMPARE returning only the differences in PLAN that do not exist in COMPARE or are different.

PLAN --remove: remove named PLAN.

PLAN --verify: run internal verification against PLAN ensuring it can publish an account.

--list: list all plans.

# Assigning plans

These plans can be customized and assigned to an account using -p, AddDomain -p custom -c siteinfo, Likewise to assign a new plan to all accounts, EditDomain -c siteinfo,plan=advanced --all would apply the plan named "advanced" to all accounts. This is a destructive operation and instead encourage a simpler route of filtering accounts, then applying plans individually.

# Install jq first
yum install -y jq
cpcmd -o json admin:collect '[]' '[siteinfo.plan:basic]' | jq -r 'keys[]' | while read -r SITE ; do
 echo "Editing $SITE"
 EditDomain -c siteinfo,plan="advanced" "$SITE"

A plan applied to an account does not reset any service values changed beyond the plan base. For example, if ssh,enabled=1 were the setting on an account and SSH were deactivated by setting ssh,enabled=0 outside the plan settings, then changing to a new plan in which ssh,enabled=1 exists would not apply to the site.

This behavior may be altered by supplying --reset to EditDomain. See EditDomain above for more information.

# Complex plan usage

OverlayFS, part of BoxFS, may be used to create complex plans that add additional services.

A cPanelesque feature allows users to use cron while disabling ssh. Doing so still allows the user arbitrary access to SSH into the server by opening a tunnel within a cron task, so the only means to ensure an environment without shell access is to disable all ingress routes. Despite this advice, cron may be enabled while disabling terminal access with a new service layer that utilizes the whiteout feature of OverlayFS:

mkdir -p /home/virtual/FILESYSTEMTEMPLATE/nossh/etc/pam.d
mknod /home/virtual/FILESYSTEMTEMPLATE/nossh/etc/pam.d/sshd c 0 0
touch /home/virtual/site1/info/services/nossh
/etc/systemd/user/fsmount.init mount site1 nossh

Use OverlayFS' whiteout feature to mask the sshd pam service, so you'd have all the affordances of the ssh service layer/bins but without the ability to authenticate via SSH. Layers are left-to-right precedence and layers are mounted lexicographically. To have a service supercede "nossh" name it lower in the alphabet such as "negatednossh".

Any character device with major:minor 0:0 is hidden on upper layers. This a feature consistent with layered filesystems, OverlayFS and aufs notably. ApisCP uses OverlayFS presently but used aufs prior to 2016 (opens new window). A corresponding surrogate is created to mount the layer if the package name matches a package which lacks ssh but permits cron:

<?php declare(strict_types=1);

    class Ssh_Module_Surrogate extends Ssh_Module
        const SSHLESS_PLANS = ['basic'];
        public function _create()
            $plan = $this->getServiceValue('siteinfo', 'plan', \Opcenter\Service\Plans::default());
            if (!\in_array($plan, static::SSHLESS_PLANS, true)) {

            return $this->maskSsh();

        public function _edit()
            $newPlan = array_get($this->getNewServices('siteinfo'), 'plan', \Opcenter\Service\Plans::default());
            $oldPlan = array_get($this->getOldServices('siteinfo'), 'plan', \Opcenter\Service\Plans::default());
            if (\in_array($oldPlan, static::SSHLESS_PLANS, true) === ($post = \in_array($newPlan, static::SSHLESS_PLANS, true))) {
            return $post ? $this->maskSsh() : $this->unmaskSsh();

        private function maskSsh(): bool {
            $layer = new \Opcenter\Service\ServiceLayer($this->site);
            if (!$layer->installServiceLayer('nossh') || !$layer->reload()) {
                return error("Failed to mount `nossh' service");
            return true;

        private function unmaskSsh(): bool
            $layer = new \Opcenter\Service\ServiceLayer($this->site);
            if (!$layer->uninstallServiceLayer('nossh') || !$layer->reload()) {
                return error("Failed to unmount `nossh' service");

            return true;

Make sure the plan listed above in SSHLESS_PLANS exists (see artisan opcenter:plan) and you're off to the races!

You may confirm the service layer has been mounted via mount in procfs:

EditDomain -p basic site12
grep 'site12/fst' /proc/mounts | grep lowerdir=
# You should see "nossh" in the composite layer
# Additionally, confirm the layer marker has been installed
ls /home/virtual/site12/info/services/nossh