Getting started

Quick start: your first site in five minutes

From a fresh installation to a static site, a PHP (WordPress-style) site, a Node.js application behind the server, and HTTPS with Let’s Encrypt. Every site lives in its own short file under /etc/scalws/sites.d/, managed with scalwsctl site add|list|remove. The full field reference is in site files.

The examples use example.com and /srv/www/...; replace them with your domains and directories. Commands that write to /etc/scalws run as root.

0. Before you start

You need:

  • the scalws package installed and one of the two server units running: scalws-controller (the default) or scalwsd (for managed runtimes, see the three services);
  • DNS A/AAAA records of your domains pointing at the server (needed for Let’s Encrypt; for local tests curl -H 'Host: …' is enough);
  • ports 80 and 443 reachable from the internet if you want certificates.

Check that the package’s configuration includes site files. A new installation has this line at the top of /etc/scalws/scalws.yaml:

include: sites.d/*.yaml

On an older installation, add it and create the directory:

sudo install -d -m 0750 -o root -g scalws /etc/scalws/sites.d

Which reload command?

Changes to scalws.yaml or to site files take effect on a reload. scalwsctl site add and site remove reload for you (through the controller when it runs). After editing a file by hand:

Server unitReload
scalws-controllersudo scalws-controller reload
scalwsdscalwsctl reload (or sudo systemctl reload scalwsd)

Always run sudo scalwsctl check first: it checks the main file together with all site files exactly as a reload would.

1. Ports 80 and 443

Nothing to do: the packaged configuration listens on ports 80 and 443 and gets a Let’s Encrypt certificate for every site you add. http://<server address>/ shows the welcome page. The first install opens both ports in an active ufw or a running firewalld; for a firewall you enable later, see firewall.

2. A static site

sudo mkdir -p /srv/www/example.com
echo '<h1>Hello from ScalWS</h1>' | sudo tee /srv/www/example.com/index.html
sudo scalwsctl site add example.com --root /srv/www/example.com

site add:

  1. refuses the domain if any site file or scalws.yaml already serves it;
  2. writes /etc/scalws/sites.d/example.com.yaml (owner root, group scalws, mode 0640);
  3. checks the whole configuration as a reload would and removes the file again if the check fails;
  4. reloads the server; if the server refuses the reload, the file is removed again.

The file it writes:

# Site example.com. Fields: https://scalws.com/docs/sites/
domain: example.com
root: /srv/www/example.com
www: redirect

No runtime given means static files, with index.html as the index. Because example.com has a single dot, www.example.com is served too and redirects with a 301 to example.com (path and query kept). Choose otherwise with --www none|alias|redirect; add more names with --alias other.example (repeatable).

Test it:

curl -H 'Host: example.com' http://127.0.0.1/
curl -sI -H 'Host: www.example.com' http://127.0.0.1/some/path   # 301 to example.com
sudo scalwsctl site list

Useful options: --dry-run prints the file and writes nothing; --no-reload writes and checks but leaves the reload to you. sudo scalwsctl site remove example.com renames the file to a hidden backup (.example.com.yaml.removed-<time>), checks and reloads.

The server runs as user scalws and must be able to read the document root. Both units mount /srv and /home read-only for the server, so a document root can live in either. Under /home, user scalws needs x on every directory on the way (for example chmod o+x [private host path]); site add prints a warning with the fix when it is missing.

3. A PHP site (WordPress-style)

How PHP runs depends on the server unit.

Under scalws-controller: an existing PHP-FPM pool

The controller refuses managed PHP-FPM pools (php: true), because every scalwsd instance would start its own pool (ADR-0036). Use the distribution’s PHP-FPM:

sudo apt install php8.3-fpm php8.3-mysql

The scalws user must be allowed to connect to the pool’s socket. Debian’s and Ubuntu’s default pool creates /run/php/php8.3-fpm.sock for user and group www-data, mode 0660; add scalws to that group, restart the controller so it picks up the group, and roll the instances (they keep running across a controller restart):

sudo usermod -aG www-data scalws
sudo systemctl restart scalws-controller
sudo scalws-controller reload

PHP-FPM runs your PHP code as its own pool user (www-data by default), so that user needs read access to the application and write access where the application writes (for WordPress: wp-content/). ScalWS serves static files itself and only needs read access.

Add the site:

sudo scalwsctl site add blog.example.com --root /srv/www/blog \
    --php-fastcgi unix:/run/php/php8.3-fpm.sock

This gives php: unix:/run/php/php8.3-fpm.sock: .php files go to the pool, with index.php and index.html as index files. WordPress also needs a front controller (pretty permalinks) and benefits from its profile. Edit /etc/scalws/sites.d/blog.example.com.yaml:

domain: blog.example.com
root: /srv/www/blog
profile: wordpress
php:
  fastcgi: unix:/run/php/php8.3-fpm.sock
  front_controller: /index.php

front_controller is the script that handles requests for paths that do not exist. profile: wordpress adds the profile’s deny rules (for example /wp-config.php, /wp-content/uploads/*.php) and its response-cache bypass rules (logged-in cookies, /wp-admin, …). See applications for profiles and scalwsctl detect /srv/www/blog to let ScalWS propose a configuration.

Apply:

sudo scalwsctl check
sudo scalws-controller reload

Under scalwsd.service: a managed PHP-FPM pool

With scalwsd.service, ScalWS can start and supervise PHP-FPM itself (it looks for php-fpm on PATH, else the newest versioned one such as php-fpm8.4 or Alpine’s php-fpm84):

# /etc/scalws/sites.d/blog.example.com.yaml
domain: blog.example.com
root: /srv/www/blog
profile: wordpress
php:
  front_controller: /index.php
  fpm:
    workers: {min: 2, max: 16}

scalwsctl site add --php writes the same site when scalws-controller is not running (it refuses managed runtimes only while the controller runs):

sudo scalwsctl site add blog.example.com --root /srv/www/blog --php

The PHP processes run as scalws (or as the tenant’s user, see security) inside the same sandbox, where /srv is read-only. If the application must write below /srv, allow that path in a drop-in, for example:

# /etc/systemd/system/scalwsd.service.d/writable.conf
[Service]
ReadWritePaths=/srv/www/blog/wp-content

then sudo systemctl daemon-reload && sudo systemctl restart scalwsd.

4. A Node.js application or any HTTP upstream

Under scalws-controller: proxy to a process you run

Start the application yourself, for example as its own systemd unit listening on 127.0.0.1:3000, then:

sudo scalwsctl site add app.example.com --proxy http://127.0.0.1:3000
# /etc/scalws/sites.d/app.example.com.yaml (written by site add)
domain: app.example.com
proxy: http://127.0.0.1:3000

The proxy forwards the client’s Host header (preserve_host: true by default) and tunnels WebSockets. Upstreams can also be unix:/path.sock. Timeouts and the other settings are in applications.

Under scalwsd.service: ScalWS runs Node.js

sudo scalwsctl site add app.example.com --root /srv/app --node "node server.js"

The command is split on whitespace and run without a shell (use the site file’s list form for arguments with spaces); the application must listen on process.env.PORT (by default a Unix socket path under /run/scalws). Python ASGI applications work the same way with --python main:app (uvicorn). Worker counts, environment and health checks are in applications.

5. HTTPS with Let’s Encrypt

ScalWS obtains and renews certificates with ACME HTTP-01, and the packaged configuration already has everything it needs: the listeners on ports 80 and 443 (acme: true on the TLS ones) and server.acme with enabled: true. A site you add gets its certificate a few seconds after the reload, once its DNS points at the server and port 80 is reachable. To receive Let’s Encrypt’s expiry notices, add a contact address in /etc/scalws/scalws.yaml:

server:
  acme:
    enabled: true
    contact: ["mailto:[email protected]"]
    terms_of_service_agreed: true

server.acme keys and defaults:

KeyDefaultMeaning
enabledfalseTurn ACME on.
directoryhttps://acme-v02.api.letsencrypt.org/directoryACME directory URL (must be https://).
contact[]Contact URIs; each must be mailto:….
terms_of_service_agreedfalseMust be true: the CA’s terms are accepted when the account is created.
state_dir/var/lib/scalws/acmeAccount, certificates and keys.
renew_before30d (30 days)Renew when the certificate expires within this time.
check_interval12hHow often expiry is checked; at least 1m.
ca_bundlenoneExtra root certificate (PEM) for the ACME directory (test CAs).

With ACME enabled, every site file without a tls: key gets tls: auto: a certificate for all its names, including www.<domain> when www is alias or redirect (so that name needs DNS too). Use tls: manual (or --tls manual) for a site whose certificate comes from the TLS listener’s certificates instead. Wildcard domains, IP addresses and localhost cannot get ACME certificates.

After a change to scalws.yaml: sudo scalwsctl check && sudo scalwsctl reload.

Follow the certificates:

scalwsctl certs

certs shows each application’s names, days left and errors. New certificates are activated without a restart, and renewal starts 30 days before expiry.

Test against the staging CA first to avoid Let’s Encrypt’s rate limits. Certificates are kept per site in state_dir and are not re-ordered just because directory changed, so give the staging test its own state directory:

  acme:
    enabled: true
    directory: https://acme-staging-v02.api.letsencrypt.org/directory
    state_dir: /var/lib/scalws/acme-staging
    contact: ["mailto:[email protected]"]
    terms_of_service_agreed: true

When staging works, remove the directory and state_dir lines and reload.

Redirect HTTP to HTTPS

Once the certificate is active, send plain-HTTP visitors to HTTPS. Add to the site file:

https_redirect: true

(or use --https-redirect with site add). Plain-HTTP requests then get a 301 to the same URL over HTTPS; ACME challenges are still answered over HTTP. Check and reload as above.

Where next