Getting started
Files, directories and services
Reference for everything the scalws package puts on a system and for the day-to-day
mechanics: where configuration and state live, the systemd units and their sandbox,
logs, reloads, backups and upgrades. Installation itself is covered in
installation.
Configuration: /etc/scalws
/etc/scalws is 0750 root:scalws: the server (group scalws) can read it, only root can
write it.
| Path | Owner, mode | Content |
|---|---|---|
/etc/scalws/scalws.yaml | root:scalws 0640 | Server configuration: listeners, limits, ACME, tenants and applications. Installed with ports 80 and 443, ACME and the welcome page (first start); never replaced on upgrade. See configuration. |
/etc/scalws/controller.yaml | root:scalws 0640 | scalws-controller configuration: state directory, control socket, instance count and CPUs, autoscaling. Never replaced on upgrade. Reference: docs/adaptive-runtime.md. |
/etc/scalws/sites.d/ | root:scalws 0750 | One file per site, included by include: sites.d/*.yaml (site files). |
/etc/scalws/console/ | root:root 0700 | credentials: console accounts (0600). |
/etc/scalws/tls/ | not created by the package | Conventional place for certificate and key files you manage yourself (used by the example configuration). |
scalws.yaml
The packaged file:
# Default configuration installed by the scalws package: ready to publish sites.
#
# 1. Point your domain's DNS (A, and AAAA if the server has IPv6) at this server.
# 2. sudo scalwsctl site add example.com
# (static files in /srv/www/example.com; --php-fastcgi, --proxy, --redirect: see
# `scalwsctl site add --help` and https://scalws.com/docs/sites/)
#
# Each site gets a Let's Encrypt certificate automatically (server.acme below). Ports 80
# and 443 must be reachable from the internet; with ufw: sudo ufw allow 'ScalWS'.
# Full reference: /usr/share/doc/scalws/examples/scalws.yaml and https://scalws.com/docs/
include: sites.d/*.yaml
server:
listeners:
- name: http
address: "0.0.0.0:80"
- name: https
address: "0.0.0.0:443"
tls:
acme: true
# IPv6 (the package comments these out on hosts without IPv6).
- name: http6
address: "[::]:80"
- name: https6
address: "[::]:443"
tls:
acme: true
metrics:
address: "127.0.0.1:9900"
# Certificates from Let's Encrypt for every site with `tls: auto` (the default while this
# is enabled). Using it accepts the Let's Encrypt Subscriber Agreement
# (https://letsencrypt.org/repository/). Add `contact: ["mailto:[email protected]"]` to get
# expiry notices.
acme:
enabled: true
terms_of_service_agreed: true
# Admin API (scalwsctl): /run/scalws/admin.sock, group scalws.
tenants:
# Answers every host name that no site serves (the server's IP address, a domain whose
# site is not added yet) with a short page. Remove it to answer such requests with 404.
- name: default
applications:
- name: welcome
domains: ["*"]
root: /usr/share/scalws/welcome
runtime: {type: static, index: [index.html]}The welcome page is /usr/share/scalws/welcome/index.html. On a host without IPv6 the
package comments out the http6 and https6 listeners on installation.
Every section rejects unknown keys, so a typo fails the check instead of being ignored.
The fully commented reference is /usr/share/doc/scalws/examples/scalws.yaml.
Site files
include: takes a path or a list of paths, relative to the directory of scalws.yaml;
an entry may have one * in the file name. Only the main file may include. The rules the
loader enforces:
- files are read in name order; names starting with
.and files not matching the pattern are ignored (editor backups,site removebackups); - each site file and the directory holding it must belong to root (or to the owner of the main configuration file) and must not be writable by group or others. Whoever can write there can serve any domain from any directory the server can read;
- a domain may be served once across all files.
scalwsctl site add writes files as root, mode 0640, group scalws. site remove renames
a file to .<name>.removed-<time>; migrate replaces a file only after keeping it as
.<name>.replaced-<time>.
Certificate files
Certificates you manage yourself are referenced from a TLS listener
(tls: {certificates: [{cert, key, server_names, default}]}, see TLS). The server
runs as scalws, so it needs read access to them, for example:
sudo install -d -m 0750 -o root -g scalws /etc/scalws/tls
sudo install -m 0640 -o root -g scalws example.com.pem example.com.key /etc/scalws/tls/State: /var/lib
| Path | Owner, mode | Content |
|---|---|---|
/var/lib/scalws | scalws:scalws 0750 | Home of the scalws user; default parent of the ACME state. |
/var/lib/scalws/acme | written by the server | server.acme.state_dir: account.json, certs/<tenant>/<app>/{cert.pem,key.pem,meta.json}, challenges/, order.lock. Shared by all instances under the controller (one of them orders, the others wait). |
/var/lib/scalws-controller | scalws:scalws | Controller state (controller.state_dir): generations/ (every configuration it applied, with site files expanded into it), instances/, logs/ (one log per instance, web-<n>.log), shares/ (limit-share, tenant-limits.json: what the instances read), quic-lb/ (the QUIC demux socket). |
/var/lib/scalws-console | root:root 0700 | Console state: audit.jsonl (append-only audit log, 0600), history/ (every configuration applied or found), tls/ (generated certificate and key, 0600). |
Because the controller stores each generation with the site files expanded, a rollback to an earlier generation restores exactly the sites of that generation, even after site files changed.
Runtime directories
| Path | Content |
|---|---|
/run/scalws | server.runtime_dir (default /run/scalws): sockets and generated configuration of managed runtimes. Under scalwsd.service it is mode 0750 and holds admin.sock. |
/run/scalws/admin.sock | Admin API of scalwsd.service (mode 0660 scalws:scalws), used by scalwsctl. Members of group scalws may use it; every change is logged under scalws::audit with the caller’s uid, gid and pid. |
/run/scalws/web-<n>/ | Under the controller: each instance’s own runtime directory, with its admin.sock. |
/run/scalws-controller/control.sock | Controller control API (mode 0600, user scalws): `scalws-controller status |
scalwsctl finds the admin socket from scalws.yaml (server.admin.socket, default
<runtime_dir>/admin.sock). Under the controller that path does not exist; name an
instance’s socket for read commands, for example:
sudo scalwsctl --socket /run/scalws/web-0/admin.sock statusscalwsctl reload, site, migrate, limit and high discover a running controller
themselves through /etc/scalws/controller.yaml and its control socket
(--controller-config to change the file, except for reload) and reload or apply limits
through it.
Metrics and health
| Server | Address |
|---|---|
scalwsd.service | server.metrics.address: 127.0.0.1:9900 in the packaged configuration (127.0.0.1:9901 when the key is omitted). |
scalws-controller | Instance i listens on 127.0.0.1:(metrics_base_port + i), default 9910, 9911, … |
Each serves /metrics (Prometheus), /healthz, read-only views and a read-only
dashboard at /dashboard. Keep these on loopback.
Binaries
| Path | Purpose |
|---|---|
/usr/bin/scalwsd | The web server. scalwsd --config FILE [--check]. |
/usr/bin/scalwsctl | Operator CLI (CLI). |
/usr/bin/scalws-limitcpu, scalws-limitram, scalws-limitio | `scalwsctl limit cpu |
/usr/bin/scalws-highcpu, scalws-highram, scalws-highio | `scalwsctl high cpu |
/usr/bin/scalws-controller | Adaptive Runtime controller. |
/usr/bin/scalws-console | Web console. |
systemd units
Two units run the web server and exclude each other; enable one. The console is independent. When to use which: installation.
scalwsd.service
User=scalws
Group=scalws
ExecStartPre=/usr/bin/scalwsd --config /etc/scalws/scalws.yaml --check
ExecStart=/usr/bin/scalwsd --config /etc/scalws/scalws.yaml --restrict-suid-sgid
ExecReload=/bin/kill -HUP $MAINPID
TimeoutStopSec=45
Restart=on-failure
LimitNOFILE=1048576
Delegate=yes
KillMode=control-group
RuntimeDirectory=scalws
RuntimeDirectoryMode=0750
StateDirectory=scalws
StateDirectoryMode=0750- The configuration is checked before every start; a broken file keeps the unit from starting instead of failing half-way.
Delegate=yes: the server manages a cgroup v2 subtree for tenant limits. It moves itself to<unit>/serverand places applications in<unit>/tenants/<tenant>/<app>. Stopping the unit kills the whole control group, including application processes.- Application processes never inherit the server’s capabilities and receive SIGTERM if
scalwsddies.
scalws-controller.service
User=scalws
Group=scalws
ExecStart=/usr/bin/scalws-controller run --config /etc/scalws/controller.yaml --restrict-suid-sgid
Conflicts=scalwsd.service
KillMode=process
Restart=on-failure
LimitNOFILE=1048576
Delegate=yes
DelegateSubgroup=controller
RuntimeDirectory=scalws-controller scalws
RuntimeDirectoryPreserve=yes
StateDirectory=scalws-controller scalws
StateDirectoryMode=0750-
KillMode=process: stopping or restarting the unit stops only the controller. Itsscalwsdinstances keep serving and a restarted controller adopts them. To take the whole service down:sudo systemctl stop scalws-controller sudo pkill -TERM -f 'scalwsd .*--instance-id'(
scalws-controller scale 0is not allowed.) -
DelegateSubgroup=controller(systemd 254 or later): the controller runs in<unit cgroup>/controller, so instances can enable the cgroup controllers for tenant limits in the unit’s group. Older systemd (EL8, EL9, Debian 12, Ubuntu 22.04) starts the controller in the unit’s group itself, which cgroup v2 refuses while instances there use those controllers. On such a system the package adds/usr/lib/systemd/system/scalws-controller.service.d/10-systemd-before-254.confwithKillMode=control-group: the instances stop with the controller, and a restart starts new ones (a short interruption instead of a handover). -
Instances are started with
taskseton their CPU slice and inherit the right to bind ports 80 and 443.
scalws-console.service
ExecStart=/usr/bin/scalws-console run \
--controller-config /etc/scalws/controller.yaml \
--credentials /etc/scalws/console/credentials \
--state-dir /var/lib/scalws-console \
--listen 0.0.0.0:8090 \
--controller-restart-command "systemctl restart scalws-controller"
After=network-online.target scalws-controller.service
Restart=on-failure
UMask=0077
Nice=10
CPUWeight=20
MemoryMax=256MIt runs as root, because it must reach the controller socket and replace
/etc/scalws/scalws.yaml atomically with its owner and mode, with a reduced capability set
and a read-only file system except /etc/scalws and /var/lib/scalws-console. It starts
after the controller but does not depend on it. Its low CPU weight and memory limit keep it
from competing with the web server. Options: console operations.
Sandbox summary
| Setting | scalwsd | scalws-controller | scalws-console |
|---|---|---|---|
| User | scalws | scalws | root |
| Capabilities | CAP_NET_BIND_SERVICE (ambient and bounding) | CAP_NET_BIND_SERVICE (ambient and bounding) | bounding set CAP_DAC_OVERRIDE CAP_CHOWN CAP_FOWNER |
NoNewPrivileges | yes | yes | yes |
ProtectSystem | strict | strict | strict (ReadWritePaths=/etc/scalws /var/lib/scalws-console /srv/www /home) |
ProtectHome | read-only | read-only | read-only (/home writable, /root and /run/user not) |
PrivateTmp | yes | yes | yes |
PrivateDevices | yes | – | yes |
ProtectKernelTunables | yes | – | yes |
ProtectKernelModules | yes | yes | yes |
ProtectControlGroups | no (would break delegation) | – | yes |
RestrictNamespaces | yes | – | yes |
RestrictSUIDSGID | --restrict-suid-sgid instead (below) | --restrict-suid-sgid instead (below) | yes |
LockPersonality | yes | yes | yes |
MemoryDenyWriteExecute | no (Node.js V8 and PHP’s opcache JIT need it) | – | yes |
SystemCallArchitectures=native | yes | yes | yes |
SystemCallFilter | – | – | @system-service and pidfd_open; other calls fail with ENOSYS |
ReadOnlyPaths=/srv | yes | yes | – |
Consequences:
-
Document roots under
/home(for example/home/<user>/public_html) can be served:/homeis read-only for the server. Userscalwsneeds search (x) permission on every directory on the way and read permission on the files;scalwsctl site addwarns when the mode bits do not allow it./rootand/run/userare not visible. -
/srvis read-only for the server and for managed applications. If an application started byscalwsd.servicemust write there, add a drop-in:# /etc/systemd/system/scalwsd.service.d/writable.conf [Service] ReadWritePaths=/srv/www/blog/wp-contentthen
sudo systemctl daemon-reload && sudo systemctl restart scalwsd. -
scalwsd.serviceandscalws-controller.servicestart their binaries with--restrict-suid-sgidinstead of settingRestrictSUIDSGID=: the same seccomp rules (no set-user-ID or set-group-ID mode on a file or directory created by open, openat, creat, mkdir, mkdirat, mknod or mknodat, or set by chmod, fchmod, fchmodat or fchmodat2), inherited by the instances, the QUIC demux and the applications, but openat2 stays available. systemd cannot read openat2’s mode and refuses the whole call, which would leave the static files without the kernel’sRESOLVE_BENEATHlookups (every instance would logopenat2 unavailable). The one creation the rules cannot see is openat2 withO_CREATand such a mode (see security). -
Running tenants’ processes as their own Unix users (
tenants[].user) needs four more capabilities in a drop-in; see security and operations.
Never edit the unit files in /usr/lib/systemd/system; upgrades replace them. Use
drop-ins (sudo systemctl edit scalwsd).
Kernel setting: /usr/lib/sysctl.d/90-scalws-adaptive-runtime.conf
net.ipv4.tcp_migrate_req = 1When a scalwsd instance leaves a SO_REUSEPORT group, connections still queued on its
listening sockets move to another instance instead of being reset (Linux 5.14 or later).
It matters for the controller’s listen: reuseport mode. The package applies it on
install and systemd loads it at every boot. Override it, if needed, with a file of the
same name in /etc/sysctl.d/.
Logs
All three units write to the journal:
journalctl -u scalwsd -f
journalctl -u scalws-controller -f
journalctl -u scalws-console -fUnder the controller, each instance’s output goes to its own file instead:
/var/lib/scalws-controller/logs/web-<n>.log. The packages rotate these files with
/etc/scalws-logrotate-instances.conf: daily, and within the hour once a file passes
100 MB, keeping 7 (compressed). Only the hourly run reads that file, with a state file of
its own (it is not in /etc/logrotate.d, so the system’s daily run leaves these logs
alone): scalws-logrotate.timer on systemd hosts and /etc/periodic/hourly/scalws-logrotate
on Alpine (crond’s periodic jobs; rc-update add crond if it is not in a runlevel). The
package enables the timer once; systemctl disable --now scalws-logrotate.timer stays
across upgrades. The deb and rpm recommend logrotate, the apk depends on it; without it
the files grow until the file system is full. Under another controller.state_dir, change
the path in /etc/scalws-logrotate-instances.conf (it keeps su scalws scalws). The file
lies outside /etc/scalws on purpose: logrotate runs as root and runs the scripts of its
configuration, and the console (root) may write everything in /etc/scalws.
scalwsd logs JSON lines by default, including one access-log line per request. Settings
in server.logging:
| Key | Default | Meaning |
|---|---|---|
format | json | json or text. |
level | info | tracing filter, e.g. info,scalws_proxy=debug. |
access_log | true | Log every request. |
Useful log targets: scalws::access (requests), scalws::audit (admin API changes),
scalws::acme (certificates). The console keeps its own audit log in
/var/lib/scalws-console/audit.jsonl.
How reload works
A reload validates the configuration (with all site files), prepares it (certificates, document roots, runtimes) and swaps it in atomically. If anything fails, the active configuration keeps serving and the error is reported and logged. Run the same check without touching the server with:
sudo scalwsctl check # alias: scalwsctl config-test| Server | Reload | What happens |
|---|---|---|
scalwsd.service | scalwsctl reload, sudo systemctl reload scalwsd (SIGHUP) | In-process swap. Changes to listeners, the metrics/admin endpoints, connection limits, cgroups, cache memory, per-IP limits or the AI advisor are refused with “requires a restart”: sudo systemctl restart scalwsd. |
scalws-controller | sudo scalws-controller reload | The controller validates scalws.yaml as a new generation, binds any new listening addresses first, then rolls instances onto it and drains the old ones. A bind failure rejects the reload and leaves the current sockets as they were. |
| Console | Apply in the browser | The console validates the draft (with the site files), writes scalws.yaml and reloads through the controller, then waits up to 120 s (--health-timeout-secs) for the new generation. |
scalwsctl site add|remove, migrate and limit --persist reload for you, through the
controller when it runs.
Under scalwsd.service the server keeps earlier configurations for comparison and
rollback:
scalwsctl history
scalwsctl diff # previous -> active
scalwsctl rollback # back to the previous one (or --generation G)Backup and restore
scalwsctl backup copies the configuration file, its site files and the files they
reference (TLS certificates and keys, tls_ca_file bundles) into a new directory with a
manifest.json:
sudo scalwsctl backup /var/backups/scalws-$(date +%F)
sudo scalwsctl restore /var/backups/scalws-2026-10-05 # dry run: what would change
sudo scalwsctl restore /var/backups/scalws-2026-10-05 --apply- The target directory must not exist or must be empty. Files are stored under
files/<absolute path>. restorewithout--applyonly lists each file asunchanged,changedornew. With--applyit keeps every replaced file as<file>.bak.<unix time>, writes the restored files with their original mode (and the replaced file’s owner), then checks the restored configuration. Apply it withscalwsctl reloadorsudo scalws-controller reload.
What scalwsctl backup does not contain: ACME certificates and the
account in /var/lib/scalws/acme, controller.yaml, console state and credentials,
document roots and application data. For a full configuration backup, also archive the
directories:
sudo tar -C / -czf /var/backups/scalws-files-$(date +%F).tar.gz \
etc/scalws var/lib/scalws var/lib/scalws-consoleUpgrades
Upgrading the package, what happens to running services, and rollback: Upgrading.
See also
- Operations: admin API commands, tenant limits, remote administration, performance settings.
- Troubleshooting and FAQ.