# Installation

URL: https://scalws.com/docs/guide/installation/
Source: `docs/guide/installation.md` in the ScalWS repository

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](https://scalws.com/docs/guide/quick-start/); for every file and
unit the package installs see [files, directories and services](https://scalws.com/docs/guide/files-and-services/).

Kubernetes: container image and Helm chart, see [Kubernetes](https://scalws.com/docs/guide/kubernetes/).

## Requirements

* One of the supported systems below, on x86_64 (amd64).
* systemd, or OpenRC on [Alpine Linux](#alpine-linux). With systemd 254 or later (and on
  OpenRC) a restart of `scalws-controller` hands the running instances over; with older
  systemd the instances restart with it ([systemd before 254](#systemd-before-254)).
* cgroup v2, if you want per-tenant resource limits ([cgroup v1](#cgroup-v1-el8)).
* For managed PHP: PHP-FPM 7.2 or later ([PHP on EL8](#php-on-el8)) on `PATH`: `php-fpm`,
  else the newest versioned binary (`php-fpm8.4`; on Alpine `php-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](#install-from-the-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](#el10). |
| Rocky Linux | 8, 9, 10 | `.rpm`, dnf repository | 8: cgroup v1 by default. 8, 9: systemd before 254. 10: see [EL10](#el10). |
| Oracle Linux | 8, 9, 10 | `.rpm`, dnf repository | 8: cgroup v1 by default. 8, 9: systemd before 254. 10: see [EL10](#el10). |
| Fedora | 44 | `.rpm`, dnf repository | See [Fedora 44 and systemd 259.5](#fedora-44-and-systemd-2595). |
| Debian | 12, 13 | `.deb`, [apt repository](#install-from-the-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](https://scalws.com/docs/guide/control-panels/#panels-and-systems). |
| openSUSE Leap | 16.0, 15.6 | `.rpm`, [zypper repository](#install-from-the-zypper-repository) | 16.0: SELinux enforcing. 15.6: runs unconfined ([SUSE](#suse)). |
| SUSE Linux Enterprise Server | 16.0, 15 SP7 | `.rpm`, zypper repository | 16.0: SELinux enforcing. 15 SP7: runs unconfined. See [SUSE](#suse). |
| Alpine Linux | 3.24 | `.apk`, [apk repository](#alpine-linux) | OpenRC instead of systemd, musl instead of glibc: see [Alpine Linux](#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](#selinux)).

On a server with cPanel, DirectAdmin or Plesk, see [Control panels](https://scalws.com/docs/guide/control-panels/) for
the [panel and system matrix](https://scalws.com/docs/guide/control-panels/#panels-and-systems).

### 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](#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:

```sh
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 scalws
```

The 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:

```sh
curl -o /etc/yum.repos.d/scalws.repo https://rpm.scalws.com/scalws.repo
dnf install scalws
```

The 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:

```sh
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 scalws
```

The 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:

```sh
KEY=scalws-apk@scb.network-6ae1d45e.rsa.pub
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"
fi
```

apk 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:

```sh
rc-service scalws-controller shutdown && rc-update del scalws-controller
rc-service scalws-console stop && rc-update del scalws-console
```

Alpine 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`:

```sh
rc-service scalws-controller shutdown && rc-update del scalws-controller
rc-update add scalwsd default && rc-service scalwsd start
```

Settings (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](https://scalws.com/docs/guide/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 `nftables` service runs a ruleset with the stock `table inet
  filter` input chain that includes `/etc/nftables.d/*.nft` (Alpine's `/etc/nftables.nft`
  does): 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`):

```sh
doas awall diff                                   # see what else is pending first
doas awall enable scalws && doas awall activate
```

It 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.log` and `scalwsd.log` are readable by root and
  group `scalws`; `console.log` by 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`). With `legacy` or `hybrid` ScalWS 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/lib` and `/etc`, which such a system loses at reboot unless you add
  them to lbu.
* Upgrade: `apk upgrade` (or `apk 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 scalws` stops 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/repositories` and the key from
  `/etc/apk/keys` to remove the repository.

## From a package file

```sh
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](https://scalws.com/docs/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):

```sh
curl -fsSL https://raw.githubusercontent.com/xsmtx/scalws/main/install.sh | sudo sh -s -- --panel=none
```

Installing the package directly, set it after `sudo` (`sudo` drops variables set before
it):

```sh
sudo SCALWS_PANEL=none apt-get install scalws
```

ScalWS 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](https://scalws.com/docs/guide/control-panels/).

## What the package installs

| Path | What |
|---|---|
| `/usr/bin/scalwsd` | The web server (data plane). |
| `/usr/bin/scalwsctl` | Operator CLI ([CLI reference](https://scalws.com/docs/guide/cli/)). |
| `/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](#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](https://scalws.com/docs/sites/)). |
| `/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](#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/scalws` to `0750 root:scalws` and the two configuration files to `0640
  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](#selinux) and label the package's files;
* with systemd before 254, write a [drop-in](#systemd-before-254) for
  `scalws-controller`;
* on the first install, open ports 80 and 443 in an active ufw or a running firewalld
  ([firewall](#firewall));
* create the console administrator account once and enable and start
  `scalws-console.service` (see [ScalWS Console](#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](#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:

```text
<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)](#http3-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:

```text
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`](https://scalws.com/docs/sites/#moving-from-nginx-or-apache-scalwsctl-migrate), then
start ScalWS yourself:

```sh
sudo systemctl enable --now scalws-controller
```

For 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:

```sh
sudo systemctl disable --now scalws-controller
sudo systemctl enable --now scalwsd
```

Let operators use `scalwsctl` without root: members of the group `scalws` may use the
admin socket (`sudo usermod -aG scalws alice`).

## Publishing a site

1. Point the domain's DNS records at the server: `A` to its IPv4 address and, if it has
   one, `AAAA` to its IPv6 address. For `example.com`, also `www.example.com` (it
   redirects to `example.com`).
2. Add the site:

   ```sh
   sudo scalwsctl site add example.com
   ```

   ```text
   added example.com → /etc/scalws/sites.d/example.com.yaml
   created /srv/www/example.com with a placeholder index.html
   reloaded: {"generation":2}
   ```

3. 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 certs` shows its state.

PHP through an existing PHP-FPM pool, a reverse proxy and the other options are in
[site files](https://scalws.com/docs/sites/).

## Checking that it works

```sh
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 -e
```

Under 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 status` says `active`: it adds the application profile `ScalWS`
  (`ufw allow ScalWS`: 80/tcp and 443/tcp). The output says:

  ```text
  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/tcp
  ```

  The 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 --state` says `running`: it adds the services `http`
  and `https`, 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:

  ```text
  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 --reload
  ```

  That needs a zone target other than `ACCEPT` (the zone `trusted` accepts 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:

```text
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 --reload
```

To open the ports yourself (a firewall enabled after the install, for example):

```sh
sudo ufw allow 'ScalWS'                                                    # ufw
sudo firewall-cmd --permanent --add-service=http --add-service=https && sudo firewall-cmd --reload   # firewalld
```

On 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](https://scalws.com/docs/guide/tls/#http3-quic)),
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:

```sh
# 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 accept
```

With 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):

```sh
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 activate
```

Also 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_t` then), also for those services.
* `semanage port -l | grep http_port_t` shows `udp 443`. The port is already defined, so
  `semanage port -a ... -p udp 443` answers `Port udp/443 already defined, modifying
  instead` and adds a local rule; use `semanage port -m` for that on purpose.
  `semanage port -d -p udp 443` deletes 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
  443` with another type, a listener with `http3: true` cannot bind UDP 443. The
  controller (or scalwsd) then fails to start the listener with `Permission denied`, and
  `sudo ausearch -m avc -ts recent` shows a denied `name_bind` on `udp_socket`.
  `semanage port -l -C` lists your local rules; `sudo semanage port -m -t http_port_t -p
  udp 443` restores 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:

```sh
sudo semanage fcontext -a -t httpd_sys_content_t '/data/www(/.*)?'
sudo restorecon -R /data/www
```

The 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):

```sh
getsebool scalws_can_network_connect
sudo setsebool -P scalws_can_network_connect off
```

To see whether SELinux denied anything:

```sh
sudo ausearch -m avc -ts recent
```

If SELinux is enforcing and `semodule` is missing, the package installs but cannot load the
module, and says so:

```text
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_t` do not get the parent-death signal. If `scalwsd` dies,
  the unit stops them (`KillMode=control-group`), so nothing is left running
  ([operations](https://scalws.com/docs/operations/#privileges)).

On cPanel and DirectAdmin servers SELinux is set as the panel vendor requires; see
[Control panels](https://scalws.com/docs/guide/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:

```text
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:

```sh
sudo grubby --update-kernel=ALL --args=systemd.unified_cgroup_hierarchy=1
sudo reboot
stat -fc %T /sys/fs/cgroup           # cgroup2fs after the reboot
```

EL9, 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:

```sh
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:

```sh
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-mysqlnd
```

Both 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](https://scalws.com/docs/guide/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:

```sh
sudo dnf upgrade systemd
```

## IPv6 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](https://scalws.com/docs/guide/configuration/#value-formats).

## 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:

```sh
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 restart
```

Under 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:

```text
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:

```sh
sudo grep '^admin:' /etc/scalws/console/credentials | cut -d: -f3
```

Open `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](https://scalws.com/docs/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:

```sh
scalws-console init-credentials --credentials /etc/scalws/console/credentials \
    --url "https://$(hostname -I | awk '{print $1}'):8090"
systemctl enable --now scalws-console
```

## Upgrade

```sh
sudo apt update && sudo apt upgrade      # Debian, Ubuntu
sudo dnf upgrade scalws                  # RHEL family, Fedora
sudo zypper up scalws                    # SUSE
```

What the upgrade changes, how instances move to the new binary and how to roll back:
[upgrading](https://scalws.com/docs/guide/upgrading/).

* `/etc/scalws/scalws.yaml` and `/etc/scalws/controller.yaml` are never replaced once you
  changed them; the console credentials and site files are kept.
* The package restarts `scalwsd`, `scalws-controller` and `scalws-console` if they were
  running. Restarting the controller does not stop its instances: the new controller
  adopts them and then rolls them onto the new `scalwsd` binary itself, one at a time. It
  uses a copy of the current generation and does not read the configuration file again.
  `sudo scalws-controller reload` is needed only with `instances.roll_on_upgrade: false`,
  or when every slot up to `instances.max` is taken (then the controller logs a warning).
  With systemd before 254 the instances restart with the controller instead
  ([systemd before 254](#systemd-before-254)).
* Console sessions end on upgrade; drafts are lost, history and audit log stay.
* Check the version with `scalwsctl status` (or `scalwsd --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](https://scalws.com/docs/sites/) for the one line to add.

## Uninstall

```sh
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     # SUSE
```

Removal 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](#firewall)).

Stopping the controller unit stops the controller and its QUIC demux (`ExecStopPost=`),
not the instances (`KillMode=process`, except with [systemd before 254](#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:

```sh
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).

```sh
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_total
```

The 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/`.

## Next steps

* [Quick start: your first site](https://scalws.com/docs/guide/quick-start/)
* [Configuration](https://scalws.com/docs/guide/configuration/) and [site files](https://scalws.com/docs/sites/)
* [Applications](https://scalws.com/docs/guide/applications/), [TLS](https://scalws.com/docs/guide/tls/), [caching](https://scalws.com/docs/guide/caching/),
  [security](https://scalws.com/docs/guide/security/)
* [Control panels (cPanel, DirectAdmin, Plesk)](https://scalws.com/docs/guide/control-panels/)
* [Troubleshooting](https://scalws.com/docs/guide/troubleshooting/) and [FAQ](https://scalws.com/docs/guide/faq/)
