Getting started
Installation
This guide installs ScalWS from the signed apt, dnf, zypper or apk repository, explains what the package puts on the system, and gets the server answering on ports 80 and 443. For your first site continue with the quick start; for every file and unit the package installs see files, directories and services.
Kubernetes: container image and Helm chart, see Kubernetes.
Requirements
- One of the supported systems below, on x86_64 (amd64).
- systemd, or OpenRC on Alpine Linux. With systemd 254 or later (and on
OpenRC) a restart of
scalws-controllerhands the running instances over; with older systemd the instances restart with it (systemd before 254). - cgroup v2, if you want per-tenant resource limits (cgroup v1).
- For managed PHP: PHP-FPM 7.2 or later (PHP on EL8) on
PATH:php-fpm, else the newest versioned binary (php-fpm8.4; on Alpinephp-fpm84). For Node.js or Python applications: the runtime and, for Python, the application server (uvicorn, hypercorn or gunicorn).
Supported systems
| System | Versions | Package | Notes |
|---|---|---|---|
| AlmaLinux | 8, 9, 10 | .rpm, dnf repository | 8: cgroup v1 by default. 8, 9: systemd before 254. 10: the x86_64_v2 build is fully tested; the standard (x86-64-v3) build: see EL10. |
| Rocky Linux | 8, 9, 10 | .rpm, dnf repository | 8: cgroup v1 by default. 8, 9: systemd before 254. 10: see EL10. |
| Oracle Linux | 8, 9, 10 | .rpm, dnf repository | 8: cgroup v1 by default. 8, 9: systemd before 254. 10: see EL10. |
| Fedora | 44 | .rpm, dnf repository | See Fedora 44 and systemd 259.5. |
| Debian | 12, 13 | .deb, apt repository | 12: systemd before 254. |
| Ubuntu | 22.04, 24.04, 26.04 | .deb, apt repository | 22.04: systemd before 254. 26.04: see the panel matrix. |
| openSUSE Leap | 16.0, 15.6 | .rpm, zypper repository | 16.0: SELinux enforcing. 15.6: runs unconfined (SUSE). |
| SUSE Linux Enterprise Server | 16.0, 15 SP7 | .rpm, zypper repository | 16.0: SELinux enforcing. 15 SP7: runs unconfined. See SUSE. |
| Alpine Linux | 3.24 | .apk, apk repository | OpenRC instead of systemd, musl instead of glibc: see Alpine Linux. No control panel supports Alpine. |
One build serves all of them except Alpine: the binaries are linked against glibc 2.28. The .deb
depends on libc6 (>= 2.28) and adduser. The .rpm depends on glibc >= 2.28,
shadow-utils or shadow (SUSE), /usr/bin/setpriv (util-linux) and /usr/bin/awk, and
recommends policycoreutils (for the SELinux policy module).
On a server with cPanel, DirectAdmin or Plesk, see Control panels for the panel and system matrix.
SUSE
openSUSE Leap 16.0 and SLES 16.0 enable SELinux on new installations: ScalWS runs confined under the same policy module as on the RHEL family (SELinux). Leap 15.6 and SLES 15 SP7 use AppArmor or no security module: the package loads no SELinux module there, ScalWS ships no AppArmor profile and runs unconfined, as on Debian and Ubuntu. The same holds for a Leap 16 server switched to AppArmor. All four use cgroup v2 and systemd 254 or later. SLES registration (for SUSE’s own update repositories) is between you and SUSE: the ScalWS repository needs none. SLES was tested with SUSE’s free SLE Base Container Images, and SELinux on a Leap 16 virtual machine, which is built from the SLES 16 sources. On SUSE 15 the first-install firewall opening was not tested on a virtual machine (containers only).
EL10
The userlands of Rocky Linux 10, Oracle Linux 10 and the standard AlmaLinux 10 need an x86-64-v3 CPU (AVX2). That is the distribution’s requirement, not ScalWS’s. These three were tested under CPU emulation; full verification follows in the next release. AlmaLinux 10’s x86_64_v2 build is fully tested and also runs on older CPUs.
Install from the apt repository
Debian 12 and 13, Ubuntu 22.04, 24.04 and 26.04. Run as root:
FPR=0B49CE4488DC3A3E4F631218CBBBF99C69B99EF0
apt-get install -y curl gnupg
key=$(mktemp)
curl -fsSL https://apt.scalws.com/scalws-archive-keyring.gpg -o "$key"
# The file must hold exactly one primary key, the pinned one: stop if this says WRONG.
test "$(gpg --show-keys --with-colons "$key" \
| awk -F: '$1=="pub"{p=1;next} p&&$1=="fpr"{print $10;p=0}')" = "$FPR" \
&& echo "repository key OK" || echo "WRONG repository key: stop here"
# Install only that key, re-exported from a scratch keyring (never the downloaded file).
export GNUPGHOME=$(mktemp -d)
gpg --quiet --import "$key"
gpg --export "$FPR" > /usr/share/keyrings/scalws-archive-keyring.gpg
rm -rf "$GNUPGHOME" "$key"; unset GNUPGHOME
echo 'deb [signed-by=/usr/share/keyrings/scalws-archive-keyring.gpg] https://apt.scalws.com stable main' \
> /etc/apt/sources.list.d/scalws.list
apt update && apt install scalwsThe repository is signed with the “ScalWS Archive Signing Key”, fingerprint
0B49 CE44 88DC 3A3E 4F63 1218 CBBB F99C 69B9 9EF0. ScalWS is not in the distributions’
own archives.
Install from the dnf repository
AlmaLinux, Rocky Linux and Oracle Linux 8, 9 and 10, Fedora 44. Run as root:
curl -o /etc/yum.repos.d/scalws.repo https://rpm.scalws.com/scalws.repo
dnf install scalwsThe repository file turns on both checks: gpgcheck=1 (every package is signed) and
repo_gpgcheck=1 (the repository metadata, repomd.xml, is signed). The key is
https://rpm.scalws.com/RPM-GPG-KEY-scalws, the same “ScalWS Archive Signing Key” as the
apt repository. dnf asks once to import it: compare the fingerprint it shows with
0B49 CE44 88DC 3A3E 4F63 1218 CBBB F99C 69B9 9EF0 before you answer y.
Install from the zypper repository
openSUSE Leap 16.0 and 15.6, SLES 16.0 and 15 SP7: the same repository and packages as for dnf. Import the key first, after checking its fingerprint, so zypper never has to ask about it. Run as root:
FPR=0B49CE4488DC3A3E4F631218CBBBF99C69B99EF0
key=$(mktemp)
curl -fsSL https://rpm.scalws.com/RPM-GPG-KEY-scalws -o "$key"
# The file must hold exactly one primary key, the pinned one; anything else is not imported.
if test "$(gpg --show-keys --with-colons "$key" \
| awk -F: '$1=="pub"{p=1;next} p&&$1=="fpr"{print $10;p=0}')" = "$FPR"; then
rpm --import "$key" && echo "repository key OK"
else
echo "WRONG repository key: not imported, stop here"
fi
rm -f "$key"
zypper addrepo https://rpm.scalws.com/scalws.repo
zypper install scalwsThe repository file turns on both signature checks (packages and metadata) and
autorefresh, so zypper up sees new releases. Do not answer a key question from zypper
with “trust always”, and do not use --gpg-auto-import-keys: with the key imported as
above, zypper asks nothing. If it does ask, the key is not the one above: stop.
Alpine Linux
Alpine Linux 3.24 (x86_64), installed to disk (setup-alpine, “sys” mode). The .apk
carries a static build of the same four programs (linked against musl, so it needs no C
library on the system) and OpenRC services instead of the systemd units. Run as root:
KEY=[email protected]
SUM=d809d59bdec7e7e440558fd4b648a48dfbc69c96ee0bbc21dc853808d516abc1
key=$(mktemp)
wget -qO "$key" "https://apt.scalws.com/alpine/$KEY"
# The key is trusted only if its SHA-256 is the pinned one; anything else is not installed.
if echo "$SUM $key" | sha256sum -c -s; then
mv "$key" "/etc/apk/keys/$KEY" && chmod 0644 "/etc/apk/keys/$KEY" \
&& { grep -qxF https://apt.scalws.com/alpine/v3.24/main /etc/apk/repositories \
|| echo https://apt.scalws.com/alpine/v3.24/main >> /etc/apk/repositories; } \
&& apk update && apk add scalws
else
rm -f "$key"
echo "WRONG repository key: not installed, nothing added; stop here"
fiapk checks the signature of the repository index and of every package against the keys in
/etc/apk/keys: an index or a package that was not signed with this key, or was changed
afterwards, is refused. The key file must keep its name (apk finds a key by the name
recorded in the signature). The packages and the index are signed with RSA and SHA-256.
apk add scalws also installs openrc, setpriv, util-linux-misc (taskset),
iproute2-minimal, iproute2-ss and ca-certificates-bundle. As on the other systems, the
first install starts the server on ports 80 and 443 with the welcome page and ACME, and
the console on port 8090: it adds scalws-controller and scalws-console to the default
runlevel and starts them (Alpine’s own packages start nothing; ScalWS does, as everywhere
else). To undo that:
rc-service scalws-controller shutdown && rc-update del scalws-controller
rc-service scalws-console stop && rc-update del scalws-consoleAlpine has no sudo by default; the messages say doas.
OpenRC services
| Service | Commands |
|---|---|
scalws-controller (default) | rc-service scalws-controller start, stop, restart, status; rc-service scalws-controller shutdown stops the controller and drains the instances |
scalwsd (single process, for managed runtimes) | rc-service scalwsd start, stop, reload (validated, applied without a restart) |
scalws-console | rc-service scalws-console start, stop, restart |
scalws-controller and scalwsd refuse to start while the other one runs. To switch to
scalwsd:
rc-service scalws-controller shutdown && rc-update del scalws-controller
rc-update add scalwsd default && rc-service scalwsd startSettings (configuration file, the console’s address, the drain time) are in
/etc/conf.d/scalws-controller, /etc/conf.d/scalwsd and /etc/conf.d/scalws-console.
What the systemd units do, and how the OpenRC services do it:
| systemd feature | On Alpine (OpenRC) | Status |
|---|---|---|
| Socket handover across controller restarts (ADR-0037) | Same code: stop and restart signal only the controller; the instances keep serving and the restarted controller adopts them. The QUIC demux (HTTP/3) is stopped with the controller, on systemd too (ExecStopPost=), so HTTP/3 is no longer answered; the running instances keep their UDP 443 sockets (bound, not answering QUIC) until the controller starts again or they end (rc-service scalws-controller shutdown) | full |
| Stopping everything | rc-service scalws-controller shutdown, and stop when the system shuts down or reboots: every process in ScalWS’s cgroup (/sys/fs/cgroup/scalws, whatever group it is in) and every instance the controller recorded drains (SIGTERM, then SIGKILL, cgroup.kill on the whole group, after 30 seconds), as KillMode= on the unit’s group; apk del names any process still in that group | full |
| cgroup v2 delegation for tenant limits | The service creates /sys/fs/cgroup/scalws, hands it to user scalws and starts the controller in scalws/controller; scalwsd uses /sys/fs/cgroup/scalwsd. Both need OpenRC’s cgroups service, which they start | full |
Restart=on-failure | supervise-daemon restarts a service 2 seconds after it fails, at most 5 times in 10 seconds | full |
User, capabilities, NoNewPrivileges | The controller and scalwsd run as scalws with only CAP_NET_BIND_SERVICE and no_new_privs; the console runs as root with only CAP_DAC_OVERRIDE, CAP_CHOWN and CAP_FOWNER, no_new_privs (with the service sandbox it starts with CAP_SYS_ADMIN and CAP_SETPCAP too, to make its read-only view, and drops both before it serves) | full |
LimitNOFILE, Nice, CPUWeight, MemoryMax | rc_ulimit, --nicelevel 10, the console’s cgroup gets cpu.weight 20 and memory.max 256M | full |
| journald | No journal: the output goes to /var/log/scalws/controller.log, scalwsd.log and console.log, rotated weekly (and at 100 MB) by /etc/logrotate.d/scalws; logrotate is a dependency of the apk. The instances’ logs are rotated hourly from /etc/scalws-logrotate-instances.conf (files and services) | degraded: files instead of a journal |
ProtectSystem=strict, ProtectHome=read-only, ReadWritePaths= | The services start with --service-sandbox (service_sandbox=yes in /etc/conf.d/<service>): a Landlock write fence leaves only the service’s own directories writable (/run/scalws*, /var/lib/scalws*, /var/log/scalws, /var/cache/scalws, /sys/fs/cgroup, /tmp, /var/tmp, /dev, /proc; for the console /etc/scalws, /var/lib/scalws-console, /srv/www, /home, /tmp, /var/tmp, /dev/shm, /dev/null, /dev/zero, /dev/full and the control file of the controller’s supervisor, never /proc, the rest of /dev or the rest of /run/openrc: it keeps uid 0, and it also makes a read-only mount view of its own, so mode, owner, ACL, extended attribute and time changes outside its fence fail as under the unit), inherited by the instances and the applications. rw_paths="..." in the conf.d file is the twin of a ReadWritePaths= drop-in (an application’s uploads directory) | full (kernels with Landlock; Alpine 3.24’s has it) |
ProtectKernelModules, LockPersonality, RestrictSUIDSGID, SystemCallArchitectures=native | The same --service-sandbox: a seccomp deny-list of module loading, persona changes, set-user/group-ID modes and foreign-architecture calls | full |
PrivateTmp, PrivateDevices, ProtectKernelTunables, RestrictNamespaces, the console’s SystemCallFilter and MemoryDenyWriteExecute | No OpenRC equivalent (they need mount namespaces or a per-service filter that the applications would inherit). scalwsctl status says service sandbox: OpenRC: write fence and system call deny-list in force; no private /tmp, devices or namespaces (without Landlock: system call deny-list only, no write fence (no Landlock)) | degraded |
| Panel units | Not installed: no control panel supports Alpine; scalwsctl panel detect says none | not supported |
Firewall on Alpine
A fresh Alpine install runs no firewall, and the install says so (no firewall is running: ports 80 and 443 and the console's port 8090 are reachable). On the first install the package opens ports 80 and 443
in:
- ufw (community repository), when it is active: the profile
ScalWS, as on Ubuntu; - nftables, when the
nftablesservice runs a ruleset with the stocktable inet filterinput chain that includes/etc/nftables.d/*.nft(Alpine’s/etc/nftables.nftdoes): it writes/etc/nftables.d/50-scalws.nft(tcp dport { 80, 443 } accept) and loads it at once. Any other ruleset is left alone and the output prints the rule.
With awall in use (policies enabled, its rules loaded by the iptables service) the
package changes nothing and prints the commands for its policy scalws
(/usr/share/awall/optional/scalws.json):
doas awall diff # see what else is pending first
doas awall enable scalws && doas awall activateIt does not run them itself because awall activate replaces the whole live iptables
ruleset: rules that other services added at run time (fail2ban’s bans, Docker’s chains) are
gone until those services add them again, and changes to the awall configuration that were
not activated yet go live with it.
With plain iptables rules nothing is changed and the output prints the command. The
console port 8090 is never opened, a stopped firewall is never started. Removal leaves the
nftables file (and prints rm /etc/nftables.d/50-scalws.nft && rc-service nftables reload); if you enabled the awall policy scalws, removal disables it (its file goes with
the package) and the ports stay open until your next awall activate.
Logs, cgroups and diskless systems
- Logs:
/var/log/scalws/.controller.logandscalwsd.logare readable by root and groupscalws;console.logby root only (0600, like the console’s other files). Each instance of the controller also writes/var/lib/scalws-controller/logs/web-<n>.log, as elsewhere. - Tenant limits need cgroup v2, Alpine’s default (
rc_cgroup_mode="unified"in/etc/rc.conf). WithlegacyorhybridScalWS serves without limits and the first install says how to switch. - Diskless and “data” installs (system in RAM,
lbu commit) are not supported: ScalWS keeps its state in/var/liband/etc, which such a system loses at reboot unless you add them to lbu. - Upgrade:
apk upgrade(orapk add -u scalws). Running services are restarted; the controller’s instances are adopted, as on systemd 254 and later. A configuration file you changed stays, and the new default is written next to it as<file>.apk-new. - Uninstall:
apk del scalwsstops the services (the instances drain), takes them out of every runlevel and keeps/etc/scalws,/var/lib/scalws*and your document roots. Remove the repository line from/etc/apk/repositoriesand the key from/etc/apk/keysto remove the repository.
From a package file
apt install ./scalws_<version>-<release>_amd64.deb # Debian, Ubuntu
dnf install ./scalws-<version>-<release>.x86_64.rpm # RHEL family, Fedora
zypper install ./scalws-<version>-<release>.x86_64.rpm # SUSE (a signed package)
apk add ./scalws-<version>-r<release>.apk # Alpine (with the key in /etc/apk/keys)Alpine versions have no ~ or -: 1.0.0-rc.1 is 1.0.0_rc1 there
(scalws-1.0.0_rc1-r12.apk), and 1.0.0 is 1.0.0.
Only the repositories bring updates. DEB and RPM packages are built from the source tree
with scripts/package.sh (see operations).
Control panel servers (cPanel, DirectAdmin, Plesk)
On a server that runs cPanel, DirectAdmin or Plesk, the first install of the package lets ScalWS
take over the panel’s web server and serve the panel’s domains: the package runs
scalwsctl panel enable <panel> once and prints its result (undo:
sudo scalwsctl panel disable). Upgrades never do this. If switching fails, everything is
undone, the package stays installed and the panel’s web server keeps serving; retry with
sudo scalwsctl panel enable <panel>.
To leave the panel’s web server alone, install with --panel=none (the installer passes
SCALWS_PANEL=none to the package):
curl -fsSL https://raw.githubusercontent.com/xsmtx/scalws/main/install.sh | sudo sh -s -- --panel=noneInstalling the package directly, set it after sudo (sudo drops variables set before
it):
sudo SCALWS_PANEL=none apt-get install scalwsScalWS is then installed but not started on ports 80 and 443 next to the panel’s web server; the message names the command that switches over.
See control panels.
What the package installs
| Path | What |
|---|---|
/usr/bin/scalwsd | The web server (data plane). |
/usr/bin/scalwsctl | Operator CLI (CLI reference). |
/usr/bin/scalws-limitcpu, -limitram, -limitio, -highcpu, -highram, -highio | Symlinks to scalwsctl for tenant resource commands. |
/usr/bin/scalws-controller | Adaptive Runtime controller: runs and supervises scalwsd instances. |
/usr/bin/scalws-console | Web administration console (port 8090). |
/usr/lib/systemd/system/scalwsd.service, scalws-controller.service, scalws-console.service | systemd units. |
/etc/scalws/scalws.yaml | Server configuration (by default ports 80 and 443 with ACME and the welcome page, see first start; never replaced on upgrade). |
/etc/scalws/controller.yaml | Controller configuration (never replaced on upgrade). |
/etc/scalws/sites.d/ | One file per site (site files). |
/etc/scalws/console/ | Console accounts (credentials). |
/var/lib/scalws, /var/lib/scalws-console | State: ACME account and certificates; console history and audit log. |
/usr/lib/sysctl.d/90-scalws-adaptive-runtime.conf | net.ipv4.tcp_migrate_req = 1. |
/usr/share/doc/scalws/examples/scalws.yaml | Full reference configuration. |
/etc/ufw/applications.d/scalws | ufw application profiles ScalWS (80, 443/tcp), ScalWS HTTP3 (443/udp) and ScalWS Console (8090). |
/usr/share/selinux/packages/scalws/ | SELinux policy module scalws.pp and scalws_extra.cil (.rpm only, see SELinux). |
| Shell completions | bash, zsh and fish completions for scalwsctl. |
The install scripts also:
- create the system group and user
scalws(home/var/lib/scalws, no login shell); - set
/etc/scalwsto0750 root:scalwsand the two configuration files to0640 root:scalws; - apply the sysctl setting immediately (it is also loaded at every boot);
- where SELinux is enabled (RHEL family, Fedora, openSUSE Leap 16, SLES 16), load the policy module and label the package’s files;
- with systemd before 254, write a drop-in for
scalws-controller; - on the first install, open ports 80 and 443 in an active ufw or a running firewalld (firewall);
- create the console administrator account once and enable and start
scalws-console.service(see ScalWS Console); - print a short message with the next steps.
On a first install without a control panel they also enable and start scalws-controller
(first start). On an upgrade, units that were running are restarted.
The three services
| Unit | Runs | Use it when |
|---|---|---|
scalws-controller | scalws-controller run --config /etc/scalws/controller.yaml, which starts and supervises one or more scalwsd instances sharing the listening sockets | Default. Static sites, reverse proxies, PHP through an existing PHP-FPM pool. Required for the console’s management actions. |
scalwsd | One scalwsd --config /etc/scalws/scalws.yaml process | Your configuration uses managed runtimes: php: true (ScalWS starts PHP-FPM), node, python or container applications. |
scalws-console | The web console on 0.0.0.0:8090 | Always (enabled by the package). It is a separate process; stopping it never affects the web server. |
scalws-controller and scalwsd exclude each other (Conflicts= in the controller
unit): enable exactly one of them.
Why the split: the controller can run several scalwsd instances, and each instance would
otherwise start its own copy of every application pool (ADR-0036). The controller
therefore refuses a configuration with managed PHP-FPM, Node.js, Python or container
runtimes:
<tenant>/<app>: managed php runtimes are not supported with several instances (ADR-0036); use an external endpoint (php: fastcgi, others: proxy)Under the controller, run PHP through an existing PHP-FPM pool (php: {fastcgi: "unix:/run/php/php8.3-fpm.sock"}) and start Node.js or Python applications yourself
(for example as their own systemd units) behind proxy. If you want ScalWS to start and
supervise those processes, use scalwsd.service. The console then shows “controller
unavailable” and its management actions are disabled; scalwsctl works as usual.
HTTP/3 is part of the packaged binaries and off by default. It works under the controller
too (through its QUIC demux): set http3: true on a TLS listener and open UDP 443, see
HTTP/3 (UDP 443).
First start
The package starts the server by itself on a fresh installation: scalws-controller is
enabled and started with the default configuration, which
- listens on ports 80 and 443 (IPv4, and IPv6 where the host has it),
- obtains a Let’s Encrypt certificate for every site you add (
server.acme, which accepts the Let’s Encrypt Subscriber Agreement), and - answers every host name that no site serves (the server’s IP address, for example) with a short welcome page.
The end of the installation output says what happened:
ScalWS is running on ports 80 and 443 (http://203.0.113.10/ shows its welcome page).
Publish a site: point the domain's DNS at this server, then
sudo scalwsctl site add example.com
Files go to /srv/www/example.com; HTTPS comes from Let's Encrypt automatically.The server is not started when another program already listens on port 80 or 443 (nginx,
Apache): the output names it. Stop that program, or move its sites over with
scalwsctl migrate, then
start ScalWS yourself:
sudo systemctl enable --now scalws-controllerFor managed PHP-FPM, Node.js or Python runtimes, which scalws-controller does not run (it uses existing pools and upstreams), run the single-process unit instead:
sudo systemctl disable --now scalws-controller
sudo systemctl enable --now scalwsdLet operators use scalwsctl without root: members of the group scalws may use the
admin socket (sudo usermod -aG scalws alice).
Publishing a site
-
Point the domain’s DNS records at the server:
Ato its IPv4 address and, if it has one,AAAAto its IPv6 address. Forexample.com, alsowww.example.com(it redirects toexample.com). -
Add the site:
sudo scalwsctl site add example.comadded example.com → /etc/scalws/sites.d/example.com.yaml created /srv/www/example.com with a placeholder index.html reloaded: {"generation":2} -
Put the site’s files into
/srv/www/example.com. The certificate is requested right after the reload and is used as soon as it arrives (usually within seconds);scalwsctl certsshows its state.
PHP through an existing PHP-FPM pool, a reverse proxy and the other options are in site files.
Checking that it works
curl -I http://127.0.0.1/ # the welcome page (200)
scalwsctl status # version, generation, listeners
scalwsctl certs # certificates and their state
sudo scalws-controller status # instances of the Adaptive Runtime
journalctl -u scalws-controller -eUnder scalws-controller the read commands (status, apps, certs, top, …) use the
first instance’s admin socket on their own.
Health and metrics stay on loopback: under scalwsd.service
http://127.0.0.1:9900/healthz (also /metrics and a read-only /dashboard); under
scalws-controller each instance has its own port, metrics_base_port + i (default
9910 for the first instance).
Firewall
Ports 80 and 443 must be reachable from the internet, also for the Let’s Encrypt validation. On the first install of the package, on a server without a control panel, the package opens them in the host firewall if one is running:
-
ufw, when
ufw statussaysactive: it adds the application profileScalWS(ufw allow ScalWS: 80/tcp and 443/tcp). The output says:opened ports 80 and 443 in ufw (rule: ScalWS) the console (port 8090) was not opened: the default policy of ufw blocks it and no rule for port 8090 was found; to open it: sudo ufw allow 8090/tcpThe package says so only when ufw’s default incoming policy is deny or reject and no rule naming port 8090 exists. It does not evaluate port ranges, source rules or other tables, so otherwise it says that the console may be reachable from the network and asks you to check your firewall.
-
firewalld, when
firewall-cmd --statesaysrunning: it adds the serviceshttpandhttps, permanently and at runtime, to the zone of the interface that carries the default route, or else to the default zone. The output names the zone:opened services http and https in firewalld (zone public) the console (port 8090) was not opened: the default policy of firewalld (zone public) blocks it and no rule for port 8090 was found; to open it: sudo firewall-cmd --permanent --zone=public --add-port=8090/tcp && sudo firewall-cmd --reloadThat needs a zone target other than
ACCEPT(the zonetrustedaccepts everything) and no port rule for 8090; services, rich rules and zone sources are not evaluated.If the runtime change fails, the permanent rules stay and the output says they take effect after
sudo firewall-cmd --reload.
Whenever the package starts or enables the console without such a line (no firewall, a
panel server, a reinstall), the output says that it listens on port 8090 of every address
and whether a firewall’s default policy blocks it, for example
ScalWS Console listens on port 8090 of every address and may be reachable from the network (no firewall is running); check your firewall, ....
A masked console (systemctl mask --now scalws-console) is left alone and not mentioned.
The package never enables or starts a stopped firewall, never opens the console port 8090, and does nothing on upgrades or on a panel server (the panel manages the firewall there). If a command fails, the output prints the command to run yourself; the installation succeeds either way. Removing the package leaves the rules in place and prints how to remove them:
The ufw rule for ports 80 and 443 stays; to remove it: sudo ufw delete allow ScalWS
The firewalld services http and https stay open (zone public); to remove them: sudo firewall-cmd --permanent --zone=public --remove-service=http --remove-service=https && sudo firewall-cmd --reloadTo open the ports yourself (a firewall enabled after the install, for example):
sudo ufw allow 'ScalWS' # ufw
sudo firewall-cmd --permanent --add-service=http --add-service=https && sudo firewall-cmd --reload # firewalldOn a cloud server, also allow the ports in the provider’s firewall or security group. The metrics ports and the admin socket stay local. Port 8090 is the console; see below before opening it.
HTTP/3 (UDP 443)
HTTP/3 is off until you set http3: true on a TLS listener (tls.md),
and the package never opens UDP 443, not even when it opens 80 and 443 over TCP. Packages
before 1.0.0 had 443/udp in the ufw profile ScalWS: upgrading narrows such a rule to TCP
(narrowed the ufw rule ScalWS to TCP ports 80 and 443). After
enabling HTTP/3, open UDP 443 yourself:
# ufw: the profile ScalWS HTTP3 is 443/udp
sudo ufw allow 'ScalWS HTTP3'
# firewalld (the zone the install named, here public)
sudo firewall-cmd --permanent --zone=public --add-port=443/udp && sudo firewall-cmd --reload
# nftables with Alpine's stock ruleset (table inet filter, /etc/nftables.d included)
doas tee /etc/nftables.d/51-scalws-http3.nft <<'EOF'
table inet filter {
chain input {
udp dport 443 accept comment "ScalWS HTTP/3"
}
}
EOF
doas nft -f /etc/nftables.d/51-scalws-http3.nft
# nftables with another ruleset: add the rule to your input chain, for example (this
# changes only the live ruleset: also add it to your ruleset file, e.g. /etc/nftables.conf,
# or it is gone at the next reload or reboot)
sudo nft add rule inet filter input udp dport 443 acceptWith awall, write a policy of your own and activate it (the same caution as for the policy
scalws below: awall activate replaces the whole live ruleset):
doas tee /etc/awall/optional/scalws-http3.json <<'EOF'
{
"description": "ScalWS HTTP/3: UDP 443 from every zone",
"service": {"scalws-http3": {"proto": "udp", "port": 443}},
"filter": [{"out": "_fw", "service": "scalws-http3", "action": "accept"}]
}
EOF
doas awall enable scalws-http3 && doas awall activateAlso allow UDP 443 in the provider’s firewall or security group. While UDP 443 is closed,
browsers stay on HTTP/2 over TCP: HTTP/3 only adds a way in. To check from another host,
curl --http3-only -sI https://example.com/ (a curl built with HTTP/3) answers
HTTP/3 200.
SELinux
On the RHEL family, Fedora, openSUSE Leap 16 and SLES 16, ScalWS runs with SELinux
enforcing, under its own policy module. There is no need to change the SELinux mode. The
.rpm ships the module /usr/share/selinux/packages/scalws/scalws.pp and its companion
scalws_extra.cil, which labels port 8090 as scalws_console_port_t and UDP port 443
as http_port_t for HTTP/3 (EL 10 and Fedora label it so already; EL 8 and 9 do not). The package loads
both when it is installed and labels its files before any service starts. On SUSE 16 this
is the same module, loaded the same way (semodule from policycoreutils, which a new
installation has); SUSE’s targeted policy has every type it uses. The .deb carries no
module.
The UDP 443 label (HTTP/3) on EL 8 and 9, where the module adds it:
- It applies to the whole system, as on EL 10 and Fedora: other confined services may bind
UDP 443 too. Removing the package removes the label again (UDP 443 is
reserved_port_tthen), also for those services. semanage port -l | grep http_port_tshowsudp 443. The port is already defined, sosemanage port -a ... -p udp 443answersPort udp/443 already defined, modifying insteadand adds a local rule; usesemanage port -mfor that on purpose.semanage port -d -p udp 443deletes only such a local rule, never the module’s label.- A local rule of your own wins over the module: after
semanage port -m -t <type> -p udp 443with another type, a listener withhttp3: truecannot bind UDP 443. The controller (or scalwsd) then fails to start the listener withPermission denied, andsudo ausearch -m avc -ts recentshows a deniedname_bindonudp_socket.semanage port -l -Clists your local rules;sudo semanage port -m -t http_port_t -p udp 443restores it. - Another policy module that labels UDP 443 with a different type conflicts with
scalws_extra: loading the second of the two fails. The package then says it could not load the module.
| Domain | Runs |
|---|---|
scalws_t | scalwsd and scalws-controller |
scalws_console_t | scalws-console |
scalws_app_t | Applications the server starts (managed PHP-FPM pools, Node.js, Python), through setpriv --selinux-label |
Document roots need a web content type. /srv/www and /var/www have
httpd_sys_content_t already, and ~/public_html has httpd_user_content_t. The policy
lets the server read those types and lets applications write to directories labelled
httpd_sys_rw_content_t (uploads, caches). The server can also reach the distribution’s
PHP-FPM sockets (/run/php-fpm/), and on SUSE php8-fpm’s default 127.0.0.1:9000
(--php-fastcgi tcp:127.0.0.1:9000). For a document root somewhere else, label it once:
sudo semanage fcontext -a -t httpd_sys_content_t '/data/www(/.*)?'
sudo restorecon -R /data/wwwThe boolean scalws_can_network_connect (on by default) lets the server and its
applications connect to any TCP port: reverse proxy upstreams, databases, APIs. Turned off,
they may only connect to HTTP ports (http_port_t, which ACME needs):
getsebool scalws_can_network_connect
sudo setsebool -P scalws_can_network_connect offTo see whether SELinux denied anything:
sudo ausearch -m avc -ts recentIf SELinux is enforcing and semodule is missing, the package installs but cannot load the
module, and says so:
ERROR: SELinux is enforcing but semodule is missing: ScalWS runs unconfined. Install it and load the module: sudo dnf install policycoreutils && sudo semodule -i ...(On SUSE the message says zypper install policycoreutils.)
The message ends with the full commands that load the module, relabel the files and restart the services. Removing the package unloads the module and relabels the files that stay.
Known limits in this release:
runtime: {type: container}(podman) is not supported under SELinux enforcing.- Applications in
scalws_app_tdo not get the parent-death signal. Ifscalwsddies, the unit stops them (KillMode=control-group), so nothing is left running (operations).
On cPanel and DirectAdmin servers SELinux is set as the panel vendor requires; see Control panels.
cgroup v1 (EL8)
AlmaLinux, Rocky Linux and Oracle Linux 8 boot with cgroup v1 by default. ScalWS serves
normally there, but tenant limits (CPU, memory, processes) are off; the limit commands
answer tenant limits need cgroup v2, which is not active on this server .... The first
install says so:
This host uses cgroup v1: tenant limits (CPU, memory, processes) need cgroup v2.
Switch with: sudo grubby --update-kernel=ALL --args=systemd.unified_cgroup_hierarchy=1 (then reboot); without grubby add systemd.unified_cgroup_hierarchy=1 to the kernel command line.To switch to cgroup v2:
sudo grubby --update-kernel=ALL --args=systemd.unified_cgroup_hierarchy=1
sudo reboot
stat -fc %T /sys/fs/cgroup # cgroup2fs after the rebootEL9, EL10, Fedora, Debian 12 and 13, Ubuntu 22.04, 24.04 and 26.04 and the SUSE systems use cgroup v2 by default.
PHP on EL8
A plain dnf install php-fpm on AlmaLinux, Rocky Linux or Oracle Linux 8 installs PHP 7.2,
the default php module stream. Managed PHP pools run with PHP-FPM 7.2 and later: ScalWS
asks the binary for its version (php-fpm -v) and writes only directives it knows. On 7.2
that leaves out decorate_workers_output (added in PHP 7.3), so lines a PHP worker writes to
stderr reach the log with PHP-FPM’s child <pid> said into stderr prefix. PHP 7.2 is out of
upstream support since 2020; for applications use a newer stream from AppStream:
dnf module list php # the streams this release offers (8.0 from EL 8.6, 8.2 from EL 8.8)
sudo dnf module reset -y php
sudo dnf module enable -y php:8.2
sudo dnf install -y php-fpm php-cli php-mysqlnd
php-fpm -v # PHP 8.2.x (fpm-fcgi)or PHP 8.3 and later from the Remi repository:
sudo dnf install -y https://rpms.remirepo.net/enterprise/remi-release-8.rpm
sudo dnf module reset -y php
sudo dnf module enable -y php:remi-8.3
sudo dnf install -y php-fpm php-cli php-mysqlndBoth install /usr/sbin/php-fpm, which ScalWS finds by itself. A version switched later is
used when a pool next starts; sudo systemctl restart scalwsd starts every pool anew.
systemd before 254
EL8, EL9, Debian 12 and Ubuntu 22.04 have a systemd older than 254, which lacks
DelegateSubgroup=. There the package writes the drop-in
/usr/lib/systemd/system/scalws-controller.service.d/10-systemd-before-254.conf with
KillMode=control-group. A restart of scalws-controller, and so a package upgrade, then
stops the instances and starts new ones: a short interruption instead of the seamless
handover. scalwsd.service is not affected. Details:
files and services.
Fedora 44 and systemd 259.5
With systemd 259.5, the version Fedora 44 was released with, a restart of
scalws-controller with tenant limits failed with “Failed to spawn executor: Device or
resource busy” while cgroup controllers were still enabled in the unit’s group. ScalWS now
releases them when scalws-controller stops, so restarts and upgrades work. A controller
that crashed instead of stopping cannot do that, and its restart can still fail this way. Update systemd to fix it:
sudo dnf upgrade systemdIPv6 listeners
The default configuration listens on ports 80 and 443 of both 0.0.0.0 and [::]. How
[::] then takes IPv6 only, and when it stays dual-stack, is in the address rules of
configuration.
Changing the listeners
The listeners are in /etc/scalws/scalws.yaml. Both units have CAP_NET_BIND_SERVICE,
so the server binds ports below 1024 without running as root. After a change:
sudo scalwsctl check
sudo scalwsctl reload # under the controller: binds the new address, rolls instances
sudo systemctl restart scalwsd # under scalwsd.service: listener changes need a restartUnder the controller use reload, not a restart: a restarted controller adopts the
running instances with the configuration they have. The controller binds new addresses
before it activates the new generation; a bind failure (port in use, permission) rejects
the reload and leaves the current sockets as they were.
ScalWS Console
On the first install the package creates /etc/scalws/console/credentials (mode 0600,
root) with the user admin and a random 24-character password, and prints:
ScalWS Console administrator account created
address: https://<server-address>:8090
user: admin
password: sudo grep '^admin:' /etc/scalws/console/credentials | cut -d: -f3
file: /etc/scalws/console/credentials (0600)The password itself is never printed, because package managers log the installer’s terminal. Read it with:
sudo grep '^admin:' /etc/scalws/console/credentials | cut -d: -f3Open https://<server-address>:8090 and sign in as admin. The console uses a generated
self-signed certificate (in /var/lib/scalws-console/tls/), so the browser warns once.
For production, give it a real certificate (--tls-cert, --tls-key) and restrict the
client addresses (--allow), either with a firewall rule for port 8090 or by overriding
ExecStart in a drop-in. Options, accounts and roles are described in
console operations.
The console manages the server through scalws-controller. Under scalwsd.service it
shows “controller unavailable”.
Upgrades keep the credentials file. If the file is missing (tarball installs, containers), create it with:
scalws-console init-credentials --credentials /etc/scalws/console/credentials \
--url "https://$(hostname -I | awk '{print $1}'):8090"
systemctl enable --now scalws-consoleUpgrade
sudo apt update && sudo apt upgrade # Debian, Ubuntu
sudo dnf upgrade scalws # RHEL family, Fedora
sudo zypper up scalws # SUSEWhat the upgrade changes, how instances move to the new binary and how to roll back: upgrading.
/etc/scalws/scalws.yamland/etc/scalws/controller.yamlare never replaced once you changed them; the console credentials and site files are kept.- The package restarts
scalwsd,scalws-controllerandscalws-consoleif they were running. Restarting the controller does not stop its instances: the new controller adopts them and then rolls them onto the newscalwsdbinary itself, one at a time. It uses a copy of the current generation and does not read the configuration file again.sudo scalws-controller reloadis needed only withinstances.roll_on_upgrade: false, or when every slot up toinstances.maxis taken (then the controller logs a warning). With systemd before 254 the instances restart with the controller instead (systemd before 254). - Console sessions end on upgrade; drafts are lost, history and audit log stay.
- Check the version with
scalwsctl status(orscalwsd --version).
New defaults that only exist in the packaged configuration (for example include: sites.d/*.yaml on installations that predate site files) are not merged into your file;
see site files for the one line to add.
Uninstall
sudo apt remove scalws # stops and disables scalwsd, scalws-controller, scalws-console
sudo apt purge scalws # additionally removes the package's configuration files
sudo dnf remove scalws # RHEL family, Fedora
sudo zypper remove scalws # SUSERemoval also stops the scalwsd instances the controller started: they get SIGTERM to
drain and SIGKILL after 30 seconds. With dnf or zypper, a configuration file you changed
is saved as <file>.rpmsave. Where SELinux is on (RHEL family, Fedora, SUSE 16) the module
is unloaded. Firewall rules the first install added stay; the output says how to remove
them (firewall).
Stopping the controller unit stops the controller and its QUIC demux (ExecStopPost=),
not the instances (KillMode=process, except with systemd before 254).
With HTTP/3 on, the instances keep the UDP 443 sockets they were handed: the port stays
bound but QUIC is not answered until the controller starts again (TCP keeps serving). To
free UDP 443, or to switch from the controller to scalwsd.service, stop the instances
too:
sudo systemctl stop scalws-controller
sudo pkill -TERM -f 'scalwsd .*--instance-id'A controller started while a demux of an earlier one still runs ends it before binding the
UDP sockets, so a later systemctl start scalws-controller does not fail on them.
Left in place on purpose: the scalws user and group, /var/lib/scalws (ACME account and
certificates), /var/lib/scalws-controller, /var/lib/scalws-console, the console
credentials, your site files and your document roots. Delete them by hand if you want a
clean system. Remove the repository with
rm /etc/apt/sources.list.d/scalws.list /usr/share/keyrings/scalws-archive-keyring.gpg
(apt), rm /etc/yum.repos.d/scalws.repo (dnf) or zypper removerepo scalws (zypper).
Building from source
Requires Rust 1.88 or later (edition 2024).
cargo build --release
./target/release/scalwsctl check examples/scalws.yaml # needs the referenced files
./target/release/scalwsd --config examples/scalws.yaml
kill -HUP $(pidof scalwsd) # reload
curl -s 127.0.0.1:9901/metrics | grep scalws_http_requests_totalThe release build produces scalwsd, scalwsctl, scalws-controller and
scalws-console in target/release/. Optional features: --features scalwsd/http3
(HTTP/3, which the packages are built with; scalwsd --features lists what a binary has)
and --features scalwsd/ebpf (eBPF network accounting). Release packages
are profile-guided (tools/pgo-build.sh); a plain cargo build --release works too.
Running from source does not create the scalws user, the systemd units or
/etc/scalws; install the package for that, or copy the units from packaging/systemd/.