# Sites: adding a domain

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

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`](https://scalws.com/docs/reference/top-level/#include)):

```yaml
# /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:

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

## The quick way: `scalwsctl site add`

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

```sh
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`](https://scalws.com/docs/reference/site-file/#domain) and [`aliases`](https://scalws.com/docs/reference/site-file/#aliases); `www.<domain>`
  as an alias → [`www: alias`](https://scalws.com/docs/reference/site-file/#www); 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:

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

```yaml
# /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](https://scalws.com/docs/reference/site-file/). Points to know:

* [`domain`](https://scalws.com/docs/reference/site-file/#domain) is the main domain; [`aliases`](https://scalws.com/docs/reference/site-file/#aliases) are further domains of
  the same site. [`www: redirect`](https://scalws.com/docs/reference/site-file/#www) answers `www.<domain>` with a 301 to `domain`,
  path and query kept.
* [`tls`](https://scalws.com/docs/reference/site-file/#tls)`: auto` takes the certificate from Let's Encrypt (needs `server.acme`).
* The runtime is one of the shorthands [`static`](https://scalws.com/docs/reference/site-file/#static) (the default),
  [`php`](https://scalws.com/docs/reference/site-file/#php) (for example `{fastcgi: "unix:/run/php/php8.3-fpm.sock"}` or
  `{fpm: {workers: {min: 2, max: 16}}, front_controller: /index.php}`),
  [`node`](https://scalws.com/docs/reference/site-file/#node) (for example `{command: [...], workers: {min: 2, max: 4}}`),
  [`python`](https://scalws.com/docs/reference/site-file/#python) (for example `{mode: wsgi, app: "app:app", virtualenv: /srv/app/.venv}`),
  [`proxy`](https://scalws.com/docs/reference/site-file/#proxy) (`{upstream: ..., preserve_host: false, ...}`) and
  [`redirect`](https://scalws.com/docs/reference/site-file/#redirect) (`{to: ..., status: 302, keep_path: false}`), or the full
  [`runtime`](https://scalws.com/docs/reference/site-file/#runtime) syntax of `scalws.yaml`.
* [`https_redirect`](https://scalws.com/docs/reference/site-file/#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`](https://scalws.com/docs/reference/site-file/#tenant) name is derived from the domain by default
  (`shop.example.com` becomes `shop-example-com`). [`user`](https://scalws.com/docs/reference/site-file/#user) (the tenant's Unix
  account) and [`resources`](https://scalws.com/docs/reference/site-file/#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:

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