Users' guide

Sites: adding a domain

One short file per site, scalwsctl site add|list|remove, and moving sites from nginx or Apache with scalwsctl migrate.

Each site can live in its own short file under /etc/scalws/sites.d/, the way nginx uses sites-enabled/ (ADR-0048). The main configuration includes them with one line (include):

# /etc/scalws/scalws.yaml
include: sites.d/*.yaml
server:
  listeners: ...

New installations of the package have this line and the directory. On an older installation add the line yourself and create the directory:

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

The quick way: scalwsctl site add

# Static site in /srv/www/example.com (created with a placeholder page if missing);
# www.example.com redirects to example.com
sudo scalwsctl site add example.com
sudo scalwsctl site add example.com --root /var/www/example   # another directory

# --php, --node and --python start managed processes, which need scalwsd.service;
# under scalws-controller (the default unit) use --php-fastcgi or --proxy instead.

# WordPress or another PHP application
sudo scalwsctl site add blog.example.com --root /srv/www/blog --php
sudo scalwsctl site add blog.example.com --root /srv/www/blog --php-fastcgi unix:/run/php/php8.3-fpm.sock

# Node.js (the app listens on $PORT), Python ASGI, or any upstream
sudo scalwsctl site add app.example.com --root /srv/app --node "node server.js"
sudo scalwsctl site add api.example.com --root /srv/api --python main:app
sudo scalwsctl site add shop.example.com --proxy http://127.0.0.1:3000

# A domain that only redirects
sudo scalwsctl site add old-name.example --redirect https://example.com

sudo scalwsctl site list
sudo scalwsctl site remove old-name.example

Without --root, a static or PHP site’s files go to /srv/www/<domain>. A missing document root is created (mode 0755); a static site gets a placeholder index.html that names the directory, so the domain answers right away. If the check or the reload fails, both are taken back.

site add refuses a domain that is already served (by any site file or by scalws.yaml itself), writes sites.d/<domain>.yaml, checks the whole configuration as a reload would (certificates, document roots, runtimes) and removes the file again if the check fails. Only then does it reload the server: through scalws-controller when it runs, so every instance moves to the new configuration. --dry-run prints the file without writing it, --no-reload writes and checks but leaves the reload to you.

Options: --www none|alias|redirect (default: redirect for a name with one dot such as example.com, otherwise none), --alias other.example (repeatable), --tls auto|manual, --tenant NAME (put several sites in one tenant to share its limits), --htaccess, --https-redirect, --php-fastcgi unix:/run/php/php8.3-fpm.sock (an existing PHP-FPM pool; under scalws-controller this is the way to run PHP, which refuses managed pools).

site remove renames the file to a hidden backup next to it (.example.com.yaml.removed-<time>), checks and reloads.

Moving from nginx or Apache: scalwsctl migrate

sudo scalwsctl migrate nginx --dry-run          # /etc/nginx/nginx.conf and its includes
sudo scalwsctl migrate nginx
sudo scalwsctl migrate apache                   # /etc/apache2/apache2.conf or /etc/httpd/conf/httpd.conf
sudo scalwsctl migrate apache /path/to/httpd.conf --skip-existing

The source configuration is only read. Each server block (nginx) or <VirtualHost> (Apache) becomes one site file:

  • server_name / ServerName + ServerAlias → domain and aliases; www.<domain> as an alias → www: alias; a separate www. host that redirects to the domain → www: redirect.
  • A port-80 host that only redirects to HTTPS (including certbot’s if ($host = …) and RewriteRule ^ https://%{SERVER_NAME}…) and the HTTPS host of the same names → one site with https_redirect: true. HTTPS hosts get tls: auto when server.acme is enabled; otherwise the certificate file is listed as a TODO.
  • root / DocumentRoot, index / DirectoryIndex; AllowOverride other than None → htaccess: true.
  • PHP: fastcgi_pass / SetHandler "proxy:unix:…|fcgi://…" / ProxyPassMatch … fcgi:// → php: <that PHP-FPM pool>; try_files … /index.php?… → front_controller.
  • proxy_pass / ProxyPass (upstream groups resolved to their first server) → proxy; under another path → a route. proxy_set_header Host $host / ProxyPreserveHost On decide preserve_host, as the source sent it.
  • return 301 …, rewrite … permanent, Redirect → redirect.

Everything else (add_header, auth_basic, rewrite rules, regular-expression locations, …) is listed per site and written into the file as # TODO: lines with the source file and line. Rewrite rules can usually move to an .htaccess file (htaccess: true).

Domains ScalWS already serves are never written twice. For each one migrate asks:

shop.example.com is already served (shop.example.com by shop-example-com/shop-example-com (/etc/scalws/sites.d/shop.example.com.yaml)). Overwrite? [y]es / [n]o / [a]ll / n[o]ne:

Only a domain’s own site file can be replaced (it is kept as .<name>.replaced-<time>); a domain defined in scalws.yaml or in another site is skipped with a note. Without a terminal, --overwrite or --skip-existing decides; otherwise nothing is written. All files of a run are checked together like a reload; if the check or the reload fails, every file of the run is removed and replaced files are restored. --dry-run prints the plan and the files, --yes skips the final question, --no-reload leaves the reload to you.

Under scalws-controller the migrated PHP sites keep using their existing PHP-FPM pools, and proxied applications keep running as they did; stop nginx or Apache only after scalwsctl site list shows every site, then let ScalWS take ports 80 and 443.

The file

# /etc/scalws/sites.d/example.com.yaml
domain: example.com
www: redirect                 # none (default) | alias | redirect
root: /srv/www/example.com
php: true                     # or one of the runtimes below; static when none is given

Every field of a site file, with its syntax, default and reload behaviour: reference. Points to know:

  • domain is the main domain; aliases are further domains of the same site. www: redirect answers www.<domain> with a 301 to domain, path and query kept.
  • tls: auto takes the certificate from Let’s Encrypt (needs server.acme).
  • The runtime is one of the shorthands static (the default), php (for example {fastcgi: "unix:/run/php/php8.3-fpm.sock"} or {fpm: {workers: {min: 2, max: 16}}, front_controller: /index.php}), node (for example {command: [...], workers: {min: 2, max: 4}}), python (for example {mode: wsgi, app: "app:app", virtualenv: /srv/app/.venv}), proxy ({upstream: ..., preserve_host: false, ...}) and redirect ({to: ..., status: 302, keep_path: false}), or the full runtime syntax of scalws.yaml.
  • https_redirect: true still answers Let’s Encrypt challenges over plain HTTP.
  • profile, routes, cache, security and htaccess work as for an application in scalws.yaml (e.g. profile: wordpress).
  • The tenant name is derived from the domain by default (shop.example.com becomes shop-example-com). user (the tenant’s Unix account) and resources are tenant settings: set them in one file per tenant.

Only one runtime per site. Unknown fields are errors, and every message names the file:

/etc/scalws/sites.d/shop.example.com.yaml: runtime.upstream: <what is wrong with the upstream>

A file may instead contain a tenants: list in the full syntax of scalws.yaml, for a tenant with several applications.

Rules

  • Files are read in name order; names starting with . and files not matching the pattern are ignored (editor backups, site remove backups).
  • A domain may be served once. Two sites with the same domain, or a www. name that is another site’s alias, fail the check.
  • Site files and sites.d must belong to root and must not be writable by group or others: whoever can write there can serve any domain from any directory the server can read. scalwsctl site add writes them as root, mode 0640, group scalws.
  • scalwsctl check checks the main file together with its site files. Changes take effect on reload (scalwsctl reload, scalws-controller reload, or the console).
  • scalws-controller keeps each generation with the sites expanded into it, so a rollback restores exactly the sites of that generation even after site files changed.