Operations
Operations guide
Install
From the ScalWS apt repository (Ubuntu 24.04 or later, Debian 13 or later, amd64):
curl -fsSL https://scalws.com/apt/scalws-archive-keyring.gpg -o /usr/share/keyrings/scalws-archive-keyring.gpg
echo 'deb [signed-by=/usr/share/keyrings/scalws-archive-keyring.gpg] https://scalws.com/apt 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). Upgrades come with apt upgrade.
ScalWS is not in the distributions’ official archives yet.
From a package file:
apt install ./scalws_<version>_amd64.deb # Debian/Ubuntu
dnf install ./scalws-<version>-1.x86_64.rpm # Fedora/RHEL family (glibc ≥ 2.36)The package installs /usr/bin/scalwsd, /usr/bin/scalwsctl, the systemd unit, shell
completions, the system user/group scalws and a loopback-only /etc/scalws/scalws.yaml
(0640 root:scalws, kept on upgrade). The full reference configuration is in
/usr/share/doc/scalws/examples/scalws.yaml.
scalwsctl check # validate + prepare exactly like a reload
systemctl enable --now scalwsd
usermod -aG scalws alice # operators use scalwsctl via the admin socketPackages for amd64 and arm64 (ARCH=arm64) are built with scripts/package.sh and verified with
scripts/package.sh --verify (clean Debian and Fedora containers: user, files,
permissions, configuration check, server start, scalwsctl over the socket, access denied
outside group scalws).
Privileges
The unit runs as scalws with only CAP_NET_BIND_SERVICE, Delegate=yes for cgroup v2
(tenant limits) and a hardened sandbox. scalws never needs root; it logs a warning when
started as root.
Application processes never inherit scalws’s capabilities (setpriv --inh-caps=-all --ambient-caps=-all --no-new-privs) and receive SIGTERM if scalwsd dies, even when it is
killed outside systemd (--pdeathsig); processes left in cgroup leaves by a crashed
instance are killed at the next start.
Tenant users (ADR-0026, optional)
tenants[].user: web1 runs that tenant’s PHP-FPM, Node.js and Python processes as the
Unix account web1 (not root; container applications are refused for such tenants).
scalwsd then needs four more capabilities — this makes it root-equivalent if
compromised, the same trade-off as a root PHP-FPM master:
# /etc/systemd/system/scalwsd.service.d/tenant-users.conf
[Service]
AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_SETUID CAP_SETGID CAP_CHOWN CAP_KILL
CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_SETUID CAP_SETGID CAP_CHOWN CAP_KILLEach such tenant gets its own socket directory (<runtime_dir>/t/<hash>, owner
web1:scalws, mode 2770); other tenants cannot list it. Document roots must be readable by
the tenant (PHP, Node, Python) and by group scalws (static files served by scalwsd), e.g.
chown -R web1:scalws /srv/web1 && chmod -R g+rX,o-rwx /srv/web1. Changing a tenant’s user
restarts its applications on reload.
Admin API and scalwsctl (ADR-0017)
/run/scalws/admin.sock (mode 0660 scalws:scalws, directory 0750) accepts everything; every
change is logged under scalws::audit with the caller’s uid/gid/pid. The loopback metrics
listener (127.0.0.1:9900 in the default configuration) serves /metrics, /healthz,
read-only views and a read-only dashboard at /dashboard.
| Task | Command |
|---|---|
| Overview | scalwsctl status, scalwsctl apps, scalwsctl tenants, scalwsctl top |
| Apply the edited file | scalwsctl reload (or systemctl reload scalwsd) |
| What changed | scalwsctl history, scalwsctl diff [--from G --to H] |
| Undo | scalwsctl rollback [--generation G] |
| Restart one application (no dropped requests) | scalwsctl restart tenant/app |
| Worker count of a Node.js pool | scalwsctl scale tenant/app N |
| Cache | scalwsctl purge --host H --path /p · --prefix /p · --tag T · --all |
| Problems | scalwsctl diagnose [tenant/app] (exit 2 on critical findings) |
| Optimizer | scalwsctl recommendations |
| AI explanation (if enabled) | scalwsctl explain tenant/app [--refresh] |
| Detect a framework | scalwsctl detect /srv/app |
A reload that changes listeners, the metrics/admin endpoints, connection limits, cgroups,
cache memory, per-IP limits or the AI advisor is refused with “requires a restart”:
systemctl restart scalwsd.
Tenant resource limits (ADR-0010)
With cgroups enabled (server.cgroups), the busiest tenants and their limits:
scalws-highcpu # CPU, percent of one CPU, over one second
scalws-highram # memory in use (and share of the host)
scalws-highio # disk read/write MiB/s and I/O pressure
scalws-highcpu -n 5 --jsonLimit a tenant, named by its tenant name or its Unix user (tenants[].user):
scalws-limitcpu ali 50% # half a CPU (200% = two CPUs)
scalws-limitram ali 30% # 30% of the host's memory, or a size: 2GiB
scalws-limitio ali 50% # half a default tenant's share of disk time
scalws-limitio ali 20MiB/s # at most 20 MiB/s of reads and 20 MiB/s of writes
scalws-limitcpu ali --clear # back to the configuration
scalws-limitram ali 2GiB --persistA limit applies at once and lasts until scalwsd restarts (under scalws-controller, until
its instances are replaced); it survives reloads. --persist also writes it to the
tenant’s resources in scalws.yaml (comments are kept; the previous file stays as
scalws.yaml.bak.<time>) and reloads. These are scalwsctl limit cpu|ram|io and
scalwsctl high cpu|ram|io under other names; they use the admin socket, so they run as
root or a member of the scalws group.
Remote administration (optional)
server:
admin:
remote: {address: "0.0.0.0:9443", cert: /etc/scalws/admin.pem, key: /etc/scalws/admin.key,
client_ca: /etc/scalws/operators-ca.pem}Only clients with a certificate issued by client_ca can connect (mutual TLS):
scalwsctl --remote host:9443 --ca server-ca.pem --client-cert alice.pem --client-key alice.key status.
Certificates (ACME)
server:
listeners:
- {name: http, address: "0.0.0.0:80"} # HTTP-01 challenges
- {name: https, address: "0.0.0.0:443", tls: {acme: true}}
acme:
enabled: true
contact: ["mailto:[email protected]"]
terms_of_service_agreed: true
tenants:
- name: acme
applications:
- {name: shop, domains: [shop.example.com, www.shop.example.com], acme: true, ...}scalwsctl certs shows each application’s names, days left and errors. Renewal starts 30
days before expiry (renew_before) and is activated without a restart. Try the
configuration against the Let’s Encrypt staging directory first
(directory: https://acme-staging-v02.api.letsencrypt.org/directory).
Containers
runtime: {type: container} runs images with podman (default) or docker; install
podman and catatonit on the host. With cgroup enforcement and podman, containers are
created under the tenant’s cgroup, so tenant limits include them. scalwsctl scale and
scalwsctl restart work as for Node.js pools.
Performance settings
server.event_loops(ADR-0029): unset = one event loop per CPU scalwsd may run on (honourstaskset/cgroup CPU limits),0= one shared work-stealing runtime.scalwsd --worker-threads Nsets the count.server.open_file_cache(ADR-0028):{max_entries: 10000}by default, every hit is checked against the file;valid: 60strusts cached files for that long (like nginx’sopen_file_cache_valid),max_entries: 0turns it off.server.sendfile(ADR-0027, default on): large static files on plain HTTP/1.server.ktls: true(ADR-0030): kernel TLS for HTTPS/1.1 so large files usesendfileover TLS too. Load the module first (modprobe tls, persist it in/etc/modules-load.d/tls.conf); without it scalwsd logs a warning and keeps TLS in user space.server.http1_server: hyper | experimental(ADR-0046, defaulthyper, read at start-up):experimentalserves HTTP/1.1 with scalws’s own server (strict framing, differential-tested against hyper, fuzzed, audited). Opt-in until it has passed the real-application soak.server.upstream_client: hyper | experimental(ADR-0045, defaulthyper) and, per proxy route,client:overriding it: the HTTP/1 client to upstreams and to managed Node/Python applications. Same opt-in status.- Release binaries are profile-guided (ADR-0047):
tools/pgo-build.shbuilds, trains and rebuildsscalwsd(PGO_BOLT=1adds a BOLT pass on hosts with last-branch records). A plaincargo build --releaseworks too, only slower.
.htaccess (ADR-0031)
applications[].htaccess: true reads .htaccess files from the application’s root
down to the requested directory (mod_rewrite, Redirect/RedirectMatch, Require/Order/
Allow/Deny, <Files>/<FilesMatch>, ErrorDocument, DirectoryIndex, Header, Expires).
Edits apply within 2 seconds. Unsupported or refused lines (php_value, [P] proxy
rules, SetHandler, look-around regexes) are skipped and logged with the file name
(journalctl -u scalwsd | grep htaccess).
Backup and restore
scalwsctl backup /var/backups/scalws-$(date +%F) # config + certificates/keys + manifest
scalwsctl restore /var/backups/scalws-2026-10-05 # dry run: what would change
scalwsctl restore /var/backups/scalws-2026-10-05 --apply && scalwsctl reloadRestore keeps every replaced file as <file>.bak.<unix time> and validates the restored
configuration. Document roots and application data are not part of the backup.
Upgrade
Install the new package; a running service is restarted by the package scripts. The
configuration file is never replaced. Check scalwsctl status for the version.