Users' guide

Control panels (cPanel, DirectAdmin, Plesk)

ScalWS can replace the web server of a hosting control panel and serve the panel’s domains itself. This is cPanel (with Apache as its web server), DirectAdmin (with the web server CustomBuild installed) and Plesk (its nginx; Apache keeps running behind it); the panel keeps running everything else, including mail, DNS, FTP and databases. The sections up to “Not supported in panel mode” describe cPanel; DirectAdmin and Plesk list what is different there. A server without a panel is not affected: panel mode is off until you turn it on, and nothing in a normal installation depends on it.

Panels and systems

This is every pair of panel and system as tested. Panel mode needs a licensed cPanel: without a licence panel enable refuses before it changes anything.

  • Tested: the whole checklist passed.
  • Tested in part: some rows passed; what is open is named.
  • Not supported by the vendor: the panel does not run on that system, or the vendor does not support it.
  • Deferred: not run yet.
SystemcPanelDirectAdminPlesk
AlmaLinux 8TestedTestedTested
AlmaLinux 9Tested in part: everything up to upcp passed; the reboot, repeated enable and disable, forced rollback and first-install takeover rows are open, because panel mode needs a licensed cPanel (whmapi1)Tested, with Apache and with nginx_apacheTested
AlmaLinux 10Deferred to a host with x86-64-v3: cPanel’s Perl needs it and refuses the x86_64_v2 buildTested (x86_64_v2 build)Tested under CPU emulation (Plesk’s installer needs x86-64-v3)
Rocky Linux 8Not supported by the vendorTestedTested
Rocky Linux 9Not supported by the vendor (cPanel refuses it)Tested, with Apache and with nginx_apacheNot supported by the vendor
Rocky Linux 10Not supported by the vendorDeferredNot supported by the vendor
Oracle LinuxNot supported by the vendorNot supported: on 9 the panel installs but CustomBuild failsNot supported: Plesk publishes no build for Oracle Linux 9
Fedora 44No panel supports itNo panel supports itNo panel supports it
Debian 12, 13DeferredDeferredDeferred
Ubuntu 22.04DeferredDeferredDeferred
Ubuntu 24.04TestedTestedTested
Ubuntu 26.04When the vendor supports it (it does not yet)TestedTested
openSUSE Leap, SLESNot supported by the vendorNot supported by the vendorNot supported by the vendor
Alpine LinuxNot supported by the vendorNot supported by the vendorNot supported by the vendor

On the RHEL family, Plesk was tested with SELinux enforcing. cPanel and DirectAdmin need SELinux disabled and their installers see to it, so ScalWS loads no SELinux policy there. Oracle Linux 8 and 10 were not run with any panel.

Let’s Encrypt in panel mode was verified live on Ubuntu (DirectAdmin and Plesk). The RHEL family virtual machines used certificates the panel provided or that were uploaded.

For the packages see installation; for the commands see the scalwsctl reference.

What panel mode does

  • ScalWS reads cPanel’s own data (the domain list, the account vhosts, the AutoSSL certificates, the suspended accounts) and writes one site file per vhost. It never writes into cPanel’s directories (/var/cpanel, /etc/userdatadomains, /opt/cpanel, /etc/apache2).
  • ScalWS takes ports 80 and 443 from Apache. Apache stays installed but is disabled in cPanel’s service manager, so that cPanel does not start it again.
  • PHP runs through the PHP-FPM pools that cPanel manages for each account. Every domain is converted to PHP-FPM when you turn panel mode on.
  • A sync keeps the site files in step with cPanel: when you add, change, suspend or remove a domain in cPanel, ScalWS follows.

Turn it on

Check what would happen first:

sudo scalwsctl panel detect
sudo scalwsctl panel enable cpanel --dry-run

Then switch, as root:

sudo scalwsctl panel enable cpanel

The first install of the package does this by itself on a panel server; install with --panel=none (SCALWS_PANEL=none for the package) to skip it (see installation).

enable refuses, and changes nothing, when:

  • cPanel is not installed (/usr/local/cpanel/cpanel is missing);
  • panel mode is already on;
  • another program than Apache (httpd) or ScalWS listens on port 80 or 443;
  • a TLS listener in scalws.yaml already has a certificates_file of its own (panel mode sets that key; move those certificates into a site’s tls or remove the line, then run it again);
  • scalws.yaml does not parse.

The steps, in this order:

  1. The current state is recorded in /var/lib/scalws-panel/state.json: whether Apache is enabled and monitored, whether new accounts get PHP-FPM, each vhost’s PHP handler, whether scalws-controller was enabled and running. /var/lib/scalws-panel/ belongs to root (mode 0711; the certificate copies in certs/, 0750 with group scalws; state.json and status.json, which list every account’s domains, 0600); a panel command refuses to write there when the directory has another owner or can be written by others.
  2. PHP-FPM becomes the default and every domain is converted to it.
  3. enablefileprotect runs (cPanel’s standard protection: document roots are user:nobody with mode 0750) and the scalws user joins the group nobody, so that ScalWS can read the document roots. When cPanel has no /var/cpanel/suspended yet (a server where no account was ever suspended), it is created as cPanel creates it (root:mail, mode 0710), so the sync’s path unit can watch it. cPanel converts the domains to PHP-FPM in the background: enable waits up to 30 seconds for every vhost’s pool before the first sync.
  4. scalws.yaml gets these changes and no others: the entry sites.d/panel/*.yaml is appended to include (after the existing entries, so scalwsctl site add keeps writing to sites.d/), each TLS listener gets certificates_file: "-/etc/scalws/panel/certificates.yaml", and, if the packaged welcome site is still configured, its document root becomes cPanel’s default document root /var/www/html. Comments and everything else in the file stay as they are.
  5. The first sync writes the panel’s sites, and the whole configuration is checked while Apache still serves.
  6. Apache is disabled in cPanel and stopped, and scalws-controller is enabled (so that it starts at boot instead of Apache) and restarted. The command waits up to 10 seconds for ScalWS to listen on ports 80 and 443, reloads it with the edited configuration (a restarted controller resumes the configuration it ran before; the reload is retried for up to 30 seconds while the controller’s instance has no admin socket yet), and waits up to 10 seconds more until it answers an HTTP request on port 80.
  7. The units scalws-panel-sync.path and scalws-panel-sync.timer are enabled.

Expect a short interruption between the moment Apache stops and the moment ScalWS listens, normally a second or two. If a step fails, the steps before it are undone in reverse order: scalws-controller is stopped together with its instances (they keep running when only the controller stops, so the command terminates them by process id) and left disabled unless it was enabled before, Apache is enabled in cPanel and then started (while cPanel has it disabled, /etc/apachedisable, systemctl start httpd does nothing; when port 80 is still free after that, Apache is started once more), and the configuration and the generated files are restored. When scalws-controller was running before panel enable (for example on ports 80 and 443 while Apache was disabled in WHM), it is started again at the end and reloaded with the restored configuration (it would otherwise resume its latest generation, the failed one with the panel sites); the rollback says that ScalWS serves again only once that generation is active, and reports the failure otherwise. This includes ScalWS not taking the ports, a refused reload and no HTTP answer (see journalctl -u scalws-controller). The command then looks at port 80: it reports that Apache (or ScalWS, when it served the ports before) serves only when it listens there, and otherwise names the process that holds the port. Two steps are not undone, because they are harmless under Apache, and the message lists them as kept: the PHP-FPM conversion (step 2) and FileProtect (step 3).

Panel commands (enable, disable and every sync, by hand or from the units) take a lock on /var/lib/scalws-panel/lock, so they never run at the same time; a second one waits up to 60 seconds, then fails with a message.

What is served

cPanelScalWS
Main, sub, addon and parked domains of every accountOne site per vhost: the account’s document root, its PHP-FPM pool and the vhost’s other names (www. and aliases).
Service subdomains (cpanel., whm., webmail., webdisk., cpcalendars., cpcontacts., autodiscover., autoconfig.)Proxied to the local cpsrvd ports: cpanel 2082, whm 2086, webmail 2095, webdisk 2077, cpcalendars and cpcontacts 2079; autodiscover and autoconfig go to 2082.
The server’s hostnameThe default page from /var/www/html.
Suspended accountsA “suspended” page (/var/lib/scalws-panel/suspended/index.html) instead of the site, for every path.

A vhost with keys that this version of ScalWS does not know is still served; unknown keys are ignored. A vhost whose document root does not exist yet (an account that is being created) is skipped until it exists. A vhost without a PHP-FPM pool still gets PHP, on a socket that never exists: its PHP scripts are refused (an error, never their source) until the pool exists, and panel status warns about it. cPanel writes a new vhost’s pool a moment after the domain itself, so a sync that sees a vhost without a pool for the first time starts one more sync 30 seconds later (scalws-panel-sync-retry, through systemd-run), and so does a sync that rewrote site files while a vhost still has no pool (cPanel’s queued conversion of a new domain to PHP-FPM rewrites the vhost a second before it writes the pool). A retry that still finds a vhost without its pool schedules the next one, at most three more times; after that, the timer picks up a pool that appears later. On DirectAdmin the same retry, 5 seconds later, serves a domain whose document root DirectAdmin made after its hook ran. The retry also leaves the sync request (/var/lib/scalws-panel/sync-requested), so a sync that runs at that moment runs once more.

Document roots and symbolic links: an account owns the directories in its home directory, so it could replace public_html with a link to another account’s files or to the certificate keys. The sync writes each account’s document root in canonical form (a link above the home directory that root set up, such as /home -> /home2, is resolved) and skips, with a message in panel status, a vhost whose document root has a symbolic link anywhere below the home directory. Each account site also gets root_symlinks: refuse, so the server itself opens the document root without following links, also when the link appears between two syncs. Links inside a document root never lead outside it, as on any ScalWS site.

New accounts: when cPanel creates a document root with the account’s own group (user:user 0750), the server could not read it. The sync gives such a document root the group nobody (FileProtect’s setting), only when the account owns it and never through a link; directories above it that the server cannot enter are reported in panel status and left as they are. The server opens document roots as the user scalws (a member of nobody), while the sync runs as root: a vhost whose document root scalws cannot open is skipped with a message in panel status instead of making the reload fail for every account. The sync asks the kernel: one short child process (scalwsctl panel probe-access) runs as scalws, with its groups, and opens each document root the way the server does (from /, one directory at a time, no symbolic link followed), so ACLs and every other rule count, not only the mode bits. If that process cannot run, panel status warns and the mode bits decide. A root that becomes unreadable between two syncs does not stop the server either: at the next start, or a reload that leaves its site file as it was, only that site answers 503 (the error is in the log), the other accounts are served, and the next sync leaves the site out. A new or changed site whose document root cannot be opened still makes the reload fail: scalws-controller gives the instances of the new generation the previous generation’s configuration (scalwsd --previous-config), they fail to start, the controller rolls the generation back, and the sync restores the previous files. Once a generation has replaced the previous one completely, instances that restart or are added get no previous configuration: nothing could take over from them, so a site whose root became unreadable answers 503 there as well instead of keeping the server down. The same applies to a reload on a single instance without a spare slot (instances.max equal to the desired count): the old instance drains before the new one starts, so a new site whose root cannot be opened answers 503 instead of failing the reload.

A refused reload is traced to its site: the controller’s rollback carries the failed instance’s start error, which names the application (tenants[i].applications[j]); the sync leaves that site out (it is listed under skipped in panel status) and reloads again, at most three times. Only a refusal that names no generated site restores the previous files of every account. So one account whose new document root changes after the sync looked at it (renamed, closed, replaced) holds back no other account’s change. A site the sync puts back to its previous file for such a refusal never takes back a name its account gave up in the same pass when another account’s site now serves it, and a site refused again after it was put back is left out for the pass: either way the site is left out, and the other accounts’ changes stay.

Each generated site has its own tenant and application name: the name derived from its domain plus 24 hex digits (96 bits) of the domain’s SHA-256 (shop-example-com-1a2b3c4d5e6f7a8b9c0d1e2f), so domains such as shop.example.com and shop-example.com never share one, and no account can practically pick a domain whose name equals another account’s. The service and hostname sites use reserved names (scalws-panel-cpanel-<service>, scalws-panel-hostname).

The configuration takes at most 10000 site files. Panel mode writes at most 9000 (the rest is kept for your own): past that, vhosts of the account with the most vhosts are skipped, each with a message in panel status.

The site files are in /etc/scalws/sites.d/panel/<vhost>.yaml. They are generated: do not edit them, the next sync overwrites them. Put your own sites in /etc/scalws/sites.d/ as usual. A file in sites.d/panel/ without the “Generated by scalwsctl panel sync” header is yours: the sync and panel disable leave it alone. A panel domain whose site file would have that name is skipped (listed in panel status), so your file is never replaced.

Certificates

cPanel’s AutoSSL certificates (and any certificate installed in cPanel) are reused; ScalWS does not order certificates for these domains. The sync copies each vhost’s certificate to /var/lib/scalws-panel/certs/<vhost>.pem (root:scalws, mode 0640) and lists the copies in /etc/scalws/panel/certificates.yaml, which the listeners read through certificates_file (see certificates from a file). A certificate that covers only some of the vhost’s names is used for the names it covers. An expired or self-signed certificate is still served. The server hostname’s certificate is also the default for names that match no site.

Keeping in step

Three units run scalwsctl panel sync:

UnitWhen
scalws-panel-sync.path/etc/userdatadomains, /var/cpanel/ssl/apache_tls or /var/cpanel/suspended changes.
scalws-panel-sync.timer2 minutes after boot, then every 5 minutes.
scalws-panel-sync.serviceThe oneshot service that both of them start.

The units are in the package but stay disabled, and they do nothing while /var/lib/scalws-panel/state.json is missing; panel enable turns them on and panel disable turns them off.

The path unit sees /var/cpanel/ssl/apache_tls itself change (a certificate installed for a new vhost), not the files inside its vhost directories: an AutoSSL renewal that only replaces apache_tls/<vhost>/combined is picked up by the timer, within 5 minutes. The old certificate is served until then.

A sync writes only what changed (unchanged files keep their modification time), checks the whole configuration and reloads once. If a site does not pass the check, that site is left out and the others are served; the same for a reload refused because of one site (see above). If the reload is refused for another reason, or the check fails for a reason that no generated file explains, the previous files are restored and the error is kept in the status. scalws-controller answers a reload before an instance started on the new configuration, so the sync waits (up to 60 seconds) until an instance of the new generation is active; a generation the controller rolled back counts as a refused reload. A check that runs out of file descriptors blames no site: the sync stops with that error and restores. scalwsctl raises its soft limit of open files to the hard one, and scalws-panel-sync.service sets LimitNOFILE=1048576. To run a sync by hand:

sudo scalwsctl panel sync

To see the result of the last one:

sudo scalwsctl panel status
sudo scalwsctl panel status --json

status shows the time of the last sync, the number of sites, whether the server was reloaded, the skipped domains and the warnings, and error: when the last sync failed. The same status file (/var/lib/scalws-panel/status.json, readable by root only) feeds the console.

In the console

When panel mode is on, the console’s Manage, Domains page lists the cPanel-managed sites read-only, each marked “cPanel”, under a banner with the last sync, the skipped domains, the warnings and the sync error if there is one. The console cannot add domains in panel mode (the Add button is hidden and the API answers 409): add cPanel domains in cPanel, and sites of your own with sudo scalwsctl site add. Removing a cPanel-managed domain is refused too; removing a site of your own from sites.d/ still works.

Turn it off

sudo scalwsctl panel disable
sudo scalwsctl panel disable --restore-php

disable stops the sync units, disables and stops scalws-controller and terminates its instances (they keep running when only the controller stops; SIGTERM, then SIGKILL after 10 seconds, by process id), enables and starts Apache again in cPanel and checks that it listens on port 80, removes the panel lines from scalws.yaml and deletes the generated files (the sites, the certificate list and copies, the status and the suspended page). Every step runs even when one fails; the state file stays then, so that you can run the command again after fixing the cause.

What it keeps:

  • PHP-FPM. It works under Apache too, so the conversion is undone only with --restore-php, which puts back the recorded default and each vhost’s recorded handler.
  • FileProtect and the group membership of scalws.
  • An include entry for sites.d/panel/*.yaml that was in scalws.yaml before panel mode was turned on.

ScalWS is stopped and disabled afterwards, whatever it was before panel mode: Apache takes ports 80 and 443 at boot, and ScalWS must not race it for them. To run ScalWS on other ports, change the listeners in scalws.yaml, then run sudo systemctl enable --now scalws-controller.

Removing the scalws package while panel mode is on runs scalwsctl panel disable first, so that Apache serves again. If that fails, the removal prints a warning with the commands that give the ports back to Apache (whmapi1 configureservice service=httpd enabled=1 monitored=1 and systemctl start httpd) and continues.

Not supported in panel mode (cPanel)

Apache features without a ScalWS equivalent are not carried over:

  • CGI scripts (/cgi-bin/);
  • Server Side Includes (SSI);
  • per-user directories (/~user/);
  • Apache include files under /etc/apache2/conf.d/userdata/.

Sites that depend on them need to stay on Apache or be changed. Real client addresses behind Cloudflare are not carried over either: cPanel configures them in Apache (mod_remoteip), and the matching ScalWS setting is generated in a later step.

DirectAdmin

On DirectAdmin ScalWS replaces the web server that CustomBuild installed (Apache, nginx, nginx with Apache, LiteSpeed or OpenLiteSpeed; the units httpd, nginx, lshttpd, lsws). The commands, the generated files, the certificate list, the sync units, the rollback and the console work as described above for cPanel; this section lists what is different.

sudo scalwsctl panel detect
sudo scalwsctl panel enable directadmin --dry-run
sudo scalwsctl panel enable directadmin

enable refuses, and changes nothing, when DirectAdmin is not installed (/usr/local/directadmin/directadmin is missing), when cPanel panel mode is on, when another program than the CustomBuild web server (httpd, nginx, litespeed, lshttpd, openlitespeed) or ScalWS holds port 80 or 443, and when PHP is not on PHP-FPM (below).

What enable does:

  1. It records the web server units (enabled, running) and their lines in /usr/local/directadmin/data/admin/services.status in /var/lib/scalws-panel/state.json.

  2. It adds the user scalws to the group access (where it exists), which opens the home directories, the document roots and the accounts’ PHP-FPM sockets (DirectAdmin leaves them at user:access 0710 and user:access 0660). A PHP-FPM socket directory in use (/usr/local/php<version>/sockets/, usually apache:apache 0710) that scalws cannot search otherwise adds its group too, but never root or another system group (id below 1000) except apache: such a directory stops enable. Then scalws must be able to pass through each socket directory and use the webapps socket (webapps.sock); if not (or a PHP version in use has no socket directory), enable stops and undoes its steps, naming the directory or the socket. An account’s own socket that scalws cannot use does not stop enable: each sync refuses that account’s PHP scripts instead and panel status warns. No document root mode is changed. disable removes the memberships it added.

  3. The first sync writes the sites and the configuration is checked while the old web server still serves.

  4. The web server is handed over. In services.status its line (httpd=, nginx=, lshttpd=, lsws=) is set to OFF, so that DirectAdmin’s service monitor does not start it again; this is the only change ScalWS makes in that file. The unit is stopped and disabled, and a systemd drop-in /etc/systemd/system/<unit>.service.d/scalws-panel.conf keeps it from starting while panel mode is on, also when a CustomBuild run (da build ...) restarts the web server:

    [Unit]
    ConditionPathExists=|!/var/lib/scalws-panel/state.json
    ConditionPathExists=|!/usr/bin/scalwsctl

    Both conditions are triggering (|): the unit starts as soon as either file is missing, so a drop-in left behind once ScalWS is gone does not keep the web server down. The unit is not masked: panel disable removes state.json and the drop-in.

  5. scalws-controller starts, scalws-panel-sync.timer and scalws-panel-sync-request.path are enabled and the hook is installed. The cPanel path unit (scalws-panel-sync.path) is not used on DirectAdmin.

Keeping in step: the hook

DirectAdmin runs a hook after it rewrites an account’s web configuration (domain, subdomain, pointer, SSL, PHP version, suspension), after it deletes an account and after it saves a certificate. enable writes the same small script for the three hooks:

/usr/local/directadmin/scripts/custom/user_httpd_write_post/scalws-panel-sync.sh
/usr/local/directadmin/scripts/custom/user_destroy_post/scalws-panel-sync.sh
/usr/local/directadmin/scripts/custom/ssl_save_post/scalws-panel-sync.sh

The script only creates the empty file /var/lib/scalws-panel/sync-requested, runs systemctl start --no-block scalws-panel-sync.service and exits 0. It starts systemctl with an empty environment and never reads or logs its own: DirectAdmin puts passwords there. A sync that is already running when the hook fires (the start does nothing then) finds the file when it finishes and runs once more, and the path unit scalws-panel-sync-request.path (enabled by enable on DirectAdmin only) starts a sync whenever the file exists and no sync runs. A change in DirectAdmin is served within seconds; the 5-minute timer is the fallback (a certificate renewal, for example). These scripts are the only files ScalWS writes below /usr/local/directadmin, and disable removes them. A hook directory that is a symbolic link is refused.

What is served

DirectAdminScalWS
Main and additional domains (domains.list)One site per domain: public_html of the domain, the account’s PHP-FPM pool, www. and the names of its alias pointers.
SubdomainsOne site per subdomain, with its own document root when DirectAdmin set one (it must stay below the account’s home directory).
Pointers of type aliasServed like the domain, also for its subdomains.
Pointers of type pointerA 301 redirect to the domain, keeping the path.
Force SSL (force_ssl=yes)Redirect from HTTP to HTTPS.
Suspended user or domainThe suspension page.
The server hostname and unknown namesThe default page from /var/www/html (no symbolic link in the path of a root is followed).

Webmail and phpMyAdmin: every site also serves DirectAdmin’s shared paths, and an account’s files never replace them:

  • /roundcube from the directory /var/www/html/roundcube links to, /phpMyAdmin, /phpmyadmin and /pma from the one /var/www/html/phpMyAdmin links to (the URL prefix maps to that directory: /pma/x is <phpMyAdmin>/x), with PHP in the webapps pool (/usr/local/php<version>/sockets/webapps.sock). The path alone (/roundcube) answers 301 to the path with a slash (/roundcube/, the query kept), so that the application’s relative links stay below it. The sync resolves the links each time; a target that is missing or not a directory, or where it, a directory above it or a directory above the link is not owned by root or webapps or is writable by group or others, or that scalws cannot open, is left out with a warning in panel status; if scalws cannot open /var/www/html itself, all of these paths are left out (and the hostname’s site). The sites stay served either way. phpMyAdmin’s log directory answers 403, as under Apache, also under the versioned directory’s own name on the hostname (/phpMyAdmin-<version>/log);
  • /config, rewritten to /redirect.php in /var/www/html and the same pool;
  • /.well-known/acme-challenge, static, from /var/www/html;
  • /.well-known/autoconfig/mail/config-v1.1.xml, proxied to unix:/usr/local/directadmin/shared/internal.sock (mail autoconfiguration).

PHP

Every PHP slot in use (phpN_release in /usr/local/directadmin/custombuild/options.conf is not no) must run as php-fpm. ScalWS does not convert PHP: if a slot runs as something else, enable stops before it changes anything and names the command, for example

da build set php1_mode php-fpm && da build php

Run it, then enable again. Each domain uses the pool of its PHP slot (php1_select), the socket named in the account’s php/php-fpm<version>.conf. The suffixes .php, .phtml, .inc and the other script suffixes always go to PHP-FPM and are never served as files. A domain with PHP on but without a usable pool gets a socket that never exists: its scripts are refused (an error, never their source) and panel status warns, as on cPanel.

Certificates

The certificates DirectAdmin stores for a domain (Let’s Encrypt or uploaded: the certificate, the key and, when there is one, the chain) are copied as one combined PEM to /var/lib/scalws-panel/certs/ (root:scalws, mode 0640) and listed in /etc/scalws/panel/certificates.yaml. The copy holds only the certificates and the key that parsed (and match), written again as PEM; nothing else of the files reaches it. The files a domain’s configuration names (SSLCertificateFile and its siblings) are used only when they are regular files in the account’s own /usr/local/directadmin/data/users/<user>/domains/ directory, reached without a symbolic link, or the server certificate and key; otherwise the domain’s own <domain>.cert, .key and .cacert are. Every certificate file is read as a regular file of at most 1 MiB, without following a symbolic link. A certificate serves the names of the site that it covers (Let’s Encrypt certificates from DirectAdmin usually cover www., uploaded ones may not). A subdomain uses its own certificate, otherwise the domain’s when that covers its name. The server certificate (/etc/httpd/conf/ssl.crt/server.crt.combined and /etc/httpd/conf/ssl.key/server.key) is the default for names that no site matches. ScalWS does not order certificates for these domains.

Turn it off

sudo scalwsctl panel disable

removes the hooks, stops the sync timer, takes ScalWS off ports 80 and 443, removes the drop-ins, enables and starts the recorded web server units again, restores their services.status lines, removes the generated files, the configuration lines and the group memberships it added, and checks that the old web server answers on port 80. PHP was not changed, so --restore-php has nothing to do. Removing the package while panel mode is on runs disable first.

Not supported on DirectAdmin

Per-domain custom snippets (CMD_CUSTOM_HTTPD), mod_security rules, CGI (cgi-bin) and nginx templates are not translated; sites that depend on them need to stay on the original web server. A domain with custom httpd or nginx configuration (a non-empty <domain>.cust_httpd or <domain>.cust_nginx in the account’s domains directory) is still served, and panel status warns that the configuration is not applied.

Plesk

On Plesk ScalWS replaces nginx (the unit nginx), which holds ports 80 and 443. The Apache behind it on 127.0.0.1:7080 and 7081 (the unit apache2) keeps running and is not touched: Plesk regenerates Apache’s configuration on every web change (a new domain, a PHP handler change, plesk repair web) and restarts it gracefully, which fails while Apache is stopped. ScalWS does not use it. Plesk’s own panel (sw-cp-server, ports 8443 and 8880), mail, DNS, FTP and databases are not touched. The commands, the generated files, the sync units, the rollback and the console work as described above for cPanel; this section lists what is different.

sudo scalwsctl panel detect
sudo scalwsctl panel enable plesk --dry-run
sudo scalwsctl panel enable plesk

enable refuses, and changes nothing, when Plesk is not installed (/usr/local/psa/version or /opt/psa/version is missing), when another panel’s panel mode is on, and when another program than nginx or ScalWS holds port 80 or 443. That includes Apache alone on port 80 (nginx turned off in Plesk): turn nginx on in Plesk first.

ScalWS only reads Plesk’s data: fixed SELECT queries through plesk db, the PHP-FPM pool files below /opt/plesk/php/*/etc/php-fpm.d/, the webmail configuration under /etc/nginx/plesk.conf.d/webmails/ and /etc/apache2/plesk.conf.d/webmails/ and the certificates in /opt/psa/var/certificates/. The only things it writes in Plesk’s territory are the systemd drop-in of the nginx unit and the event handlers it registers (below).

What enable does:

  1. It records the state of the unit nginx (enabled, running) in /var/lib/scalws-panel/state.json, and every PHP handler it changes in step 2.
  2. PHP is switched to PHP-FPM, as on cPanel. A domain whose handler is not FPM gets the FPM handler of the same PHP version (plesk-php<version>-fpm, with plesk bin site --update <domain> -php_handler_id ...), and the default handler of each service plan is switched the same way (plesk bin service_plan --update, with -owner for a reseller’s plan) so that new domains start on FPM. A domain with an operating system handler (not plesk-php...) is not switched; its scripts are refused and panel status warns. The previous handlers are recorded with the domain’s and plan’s ids. A domain or plan whose switch fails is a warning (in the report and panel status): it keeps its handler, is not recorded and enable goes on; only when every switch fails does enable stop.
  3. It adds the user scalws to the group psaserv (Plesk’s own model: nginx and Apache are members), where the group exists and scalws is not in it.
  4. The first sync writes the sites and the configuration is checked while nginx still serves.
  5. nginx is handed over: when it was enabled or running it is stopped and disabled; when it exists but neither ran nor was enabled it is held (it is not started or enabled, ever). Either way a drop-in /etc/systemd/system/nginx.service.d/scalws-panel.conf (the same two triggering conditions as on DirectAdmin) keeps it from starting while panel mode is on, also when Plesk’s watchdog or plesk repair web tries to. Apache is not stopped, disabled or given a drop-in.
  6. scalws-controller is enabled and restarted on ports 80 and 443. Then the webmail unit starts (below) and, once its socket exists, a second sync serves the webmail (when that sync fails, panel mode stays on with the first sync’s sites and panel status warns; the next sync serves the webmail). The sync timer and scalws-panel-sync-request.path are enabled, the hook script is copied to /var/lib/scalws-panel/plesk-hook.sh and the event handlers are registered.

Any failing step undoes the earlier ones in reverse order, handlers and PHP switches included, and the message says what was undone.

Keeping in step: event handlers

Plesk runs a command as root on its events. enable registers the hook script for 35 events with plesk bin event_handler --create -command /var/lib/scalws-panel/plesk-hook.sh -priority 50 -user root -event <event>:

  • domain_*, site_*, subdomain_*, site_subdomain_*, domain_alias_* and site_alias_* for create, delete and update (and status_update and move where Plesk has them), and phys_hosting_create, phys_hosting_update, phys_hosting_delete;
  • the certificate events ssl_web_binding_update, ssl_web_mail_binding_update, ssl_mail_binding_update and ssl_panel_binding_update;
  • client_status_update, client_delete, reseller_status_update and reseller_delete (a customer or reseller suspended, activated or deleted).

The script is the DirectAdmin hook script: it creates the empty file /var/lib/scalws-panel/sync-requested, runs systemctl start --no-block scalws-panel-sync.service with an empty environment and exits 0. The path unit scalws-panel-sync-request.path runs one more sync when a request arrives during a sync (at most 10 a minute); a sync that runs longer than 15 minutes is stopped. A change in Plesk is served within seconds; the 5-minute timer is the fallback.

Plesk gives a new handler an id. ScalWS reads the ids from Plesk’s database (the rows of event_handlers that are new and whose command is the hook script) and records them in state.json; disable deletes those that still run the hook script (an id Plesk has given to another handler since is left alone), and also any other handler that runs the hook script. A handler runs the hook script when its command is exactly the script’s path (spaces and quotes around it aside); a handler of your own whose command only names the script among other words is never deleted. When Plesk’s handlers cannot be listed, none is deleted. When a handler cannot be deleted, or the handlers cannot be listed, the hook script stays (a handler left behind still finds it) and the message says what may remain. A failed enable deletes the handlers the same way, also when their ids could not be read. The script is the only file ScalWS keeps there; it is root’s and not writable by the console.

What is served

PleskScalWS
Hosting of a domain, additional domain or subdomainOne site per domain: the document root (www_root, which must stay below the subscription’s home directory under /var/www/vhosts), the domain’s PHP-FPM pool, the names <domain>, www.<domain> and ipv4.<domain>.
Active domain aliases with web (web='true')Without seoRedirect: the names <alias> and www.<alias> of the site, and <label>.<alias> for a subdomain.
Aliases that redirect to the domain (seoRedirect='true', Plesk’s default)A site of their own for <alias> and www.<alias>: a 301 to the domain (over HTTPS when it redirects there), keeping the path; ACME challenges are answered. A subdomain’s <label>.<alias> redirects the same way to the subdomain.
Redirect from HTTP to HTTPS (sslRedirect)Redirect from HTTP to HTTPS (ACME challenges excepted).
Suspended domain, subscription or parent domainThe suspension page.
Forwarding hostings (standard or frame)Not served; panel status lists them as skipped.
The server’s hostname and unknown namesThe default site from /var/www/vhosts/default/htdocs, with the server certificate.
Webmail (webmail.<domain>)See below.

Per-domain additional nginx or Apache directives, Apache-only features (mod_fcgid handlers, CGI, Perl, Python, ASP) and .htaccess features ScalWS does not support are not translated; sites that depend on them need to stay on nginx and Apache.

PHP

Each domain uses the pool Plesk runs for it: the socket named in the pool file below /opt/plesk/php/<version>/etc/php-fpm.d/ (/var/www/vhosts/system/<domain>/php-fpm.sock), running as the subscription’s system user. The suffixes .php, .phtml, .inc and the other script suffixes always go to PHP-FPM and are never served as files. A domain with PHP on but without a usable pool gets a socket that never exists: its scripts are refused (an error, never their source) and panel status warns.

Webmail

Plesk serves Roundcube through Apache and mod_fcgid. ScalWS serves webmail.<domain> (and webmail.<alias> for each alias with mail) for each domain that has a webmail configuration and whose webmail is not turned off in Plesk, from /usr/share/psa-roundcube, and runs its PHP through the unit scalws-panel-webmail.service: a PHP-FPM master with the PHP binary Plesk’s webmail configuration names (for example /opt/plesk/php/8.3/sbin/php-fpm), the ini file /etc/psa-webmail/roundcube/php.ini and the user and group of the webmail configuration’s SuexecUserGroup (for example roundcube_sysuser). The sync writes its configuration below /var/lib/scalws-panel/webmail/ (root’s alone) and restarts the unit when it changes (and starts it when it does not run, also after a failure); a PHP binary that does not exist (that PHP version removed from Plesk) is a warning in panel status that names it. Webmail is served over HTTPS only, as Plesk does: plain HTTP redirects to HTTPS.

One pool runs the webmail of every domain: the user, group and PHP handler most domains’ Apache webmail configurations share (on a tie, those of the first domain by name). A domain whose configuration is missing, cannot be read, runs as root or names another user, group or handler loses only its own webmail names, and panel status names it. The user and group must exist and be neither uid 0 nor gid 0, else webmail is not served (a warning). An open_basedir, disable_functions or disable_classes value of the ini file that the pool cannot take (a quote, ; or $ in it) keeps the webmail from starting: no configuration is written and panel status says why.

A webmail name is never taken from another site: not from a domain or alias Plesk knows (also a forwarding domain or one without hosting), nor from a site file of your own outside /etc/scalws/sites.d/panel/ (panel status warns). When the check or a reload still blames the webmail site through one of its names, only that name is left out. Roundcube’s internals (/config, /logs, /vendor, /bin, /SQL, /program/actions, the enigma plugin’s key store /plugins/enigma/home, its documentation files and .ht files) are not served, and, as Roundcube’s own rules do, neither are files without an extension (/CHANGELOG, /INSTALL, /README and the like, answered with 403); /, its scripts, its assets and its installer stay. enable starts the unit, waits for its socket and syncs again; disable stops and disables it.

Certificates

The certificate of a domain (Let’s Encrypt or uploaded; the combined file that holds the key and certificate and, when there is one, the chain) is copied as one combined PEM to /var/lib/scalws-panel/certs/ and listed in /etc/scalws/panel/certificates.yaml, as on DirectAdmin. Only the files named in Plesk’s certificates table that are plain files below /opt/psa/var/certificates/ are used: no symbolic link is followed, and each file is read as a regular file of at most 1 MiB. The server certificate that nginx’s server.conf names is the default for names that no site matches; a domain without a certificate of its own uses it too, as on Plesk (the IP address’s default certificate). ScalWS does not order certificates: issue and renew them in Plesk (a renewal fires a certificate event).

Plesk’s Let’s Encrypt extension writes the HTTP-01 challenges of every domain to /var/www/vhosts/default/htdocs/.well-known/acme-challenge/. Every Plesk site (domains, webmail and the hostname) serves /.well-known/acme-challenge/<token> from there, ahead of its other paths and over plain HTTP also where the site redirects to HTTPS, so that issuing and renewing in Plesk works while panel mode is on. Files whose name starts with a dot are not served there. When the server user cannot open that directory, the route is left out and panel status warns.

Turn it off

sudo scalwsctl panel disable

deletes the event handlers it registered (and any other handler that runs the hook script) and the hook script, stops the webmail unit, stops the sync timer, takes ScalWS off ports 80 and 443, removes the drop-in, enables and starts nginx (a held nginx is left stopped and disabled) and waits until it holds port 80, removes the group membership it added and removes the generated files. PHP-FPM stays on, because it works under nginx and Apache; --restore-php puts the recorded handlers back on the domains and service plans that still exist with the recorded id (a deleted one is skipped; one deleted and created again under the same name is left alone with a warning). Removing the package while panel mode is on runs disable first. A state written by panel enable before 2026-10-27 also handed apache2 over: disable removes its drop-in too and enables and starts it before nginx.

SCALWS_PANEL=none (--panel=none at install) keeps the install from taking over; the package prints sudo scalwsctl panel enable plesk instead.

Taking over at the first install

The first install of the package on a server with cPanel, DirectAdmin or Plesk runs scalwsctl panel enable <panel> by itself, when the panel’s own web server (or nothing) holds ports 80 and 443, and prints the result. Only the first install does this: it needs the package manager’s first-install signal and the absence of the root-owned marker in /var/lib/scalws-pkg, so an upgrade, a reinstall over the installed package or an install after panel disable never turns panel mode on again. Removing the package deletes the marker: an install after the package was purged (deb) or removed (rpm) is a first install again and takes over, unless you pass --panel=none. If enable fails, it has undone its steps, the package stays installed and the message names the command to retry. On DirectAdmin a PHP slot that is not php-fpm stops the automatic enable: the package stays installed, ScalWS stopped, and the message names the da build command above. A server without a panel is unchanged and ScalWS never stops another web server there.

Install with --panel=none (the installer passes SCALWS_PANEL=none; set it yourself when you install the package directly, after sudo: sudo SCALWS_PANEL=none apt-get install scalws) to skip the takeover. On a panel server the package never starts ScalWS on ports 80 and 443 next to the panel’s web server: when it does not take over (SCALWS_PANEL=none, an upgrade, a failed enable) it prints the command that does. Details: installation.

Troubleshooting

  • sudo scalwsctl panel status shows the last sync, the skipped domains, the warnings and the last error; start there.

  • systemctl status scalws-panel-sync and journalctl -u scalws-panel-sync show the last sync run, whether the timer, the path unit (cPanel) or a hook (DirectAdmin, Plesk) started it.

  • DirectAdmin, a change is not served within seconds: check that the three hook files exist and are executable (ls -l /usr/local/directadmin/scripts/custom/*/scalws-panel-sync.sh), then run sudo scalwsctl panel sync by hand. The timer picks the change up within 5 minutes.

  • DirectAdmin, the old web server runs although panel mode is on: its line in services.status must be OFF (grep -E '^(httpd|nginx|lshttpd|lsws)=' /usr/local/directadmin/data/admin/services.status) and /etc/systemd/system/<unit>.service.d/scalws-panel.conf must exist. If either is missing, run panel disable and panel enable again.

  • DirectAdmin, the package was removed and panel disable failed (the removal prints the steps): DirectAdmin’s web server stays down until you give it the ports back:

    rm -f /etc/systemd/system/httpd.service.d/scalws-panel.conf   # and nginx, lshttpd, lsws
    systemctl daemon-reload
    # set httpd=ON (or nginx, lshttpd, lsws) in
    # /usr/local/directadmin/data/admin/services.status
    systemctl enable --now httpd

    Without /usr/bin/scalwsctl the drop-in no longer stops the unit, so the first two steps only tidy up.

  • Plesk, a change is not served within seconds: list the handlers (plesk bin event_handler --list; they run /var/lib/scalws-panel/plesk-hook.sh), check that the script exists and is executable, then run sudo scalwsctl panel sync by hand. The timer picks the change up within 5 minutes.

  • Plesk, nginx runs although panel mode is on (every sync warns): the drop-in /etc/systemd/system/nginx.service.d/scalws-panel.conf must exist. If it is missing, run panel disable and panel enable again. Apache runs behind it on 127.0.0.1 only, as it should.

  • Plesk, the package was removed and panel disable failed (the removal prints the steps): nginx stays down until you give it the ports back, and the event handlers still run the hook script (the apache2 lines only for a panel mode turned on before 2026-10-27, which handed Apache over too):

    plesk db -Ne "SELECT id,command FROM event_handlers" | grep /var/lib/scalws-panel/plesk-hook.sh
    plesk bin event_handler --delete <id>               # for each id listed
    rm -f /var/lib/scalws-panel/plesk-hook.sh            # once none is left
    rm -f /etc/systemd/system/nginx.service.d/scalws-panel.conf /etc/systemd/system/apache2.service.d/scalws-panel.conf
    systemctl daemon-reload
    systemctl enable --now apache2                       # only if its drop-in was there
    systemctl enable --now nginx
    systemctl disable --now scalws-panel-webmail.service
  • The PHP scripts of a site answer an error: its PHP-FPM pool is missing or not running; panel status names the domain.

  • enable fails: the message names the step and what was undone. panel enable <panel> --dry-run shows the preconditions without changing anything.

See also