# Migrating from nginx

URL: https://scalws.com/docs/howto/from-nginx/
Source: `docs/howto/from-nginx.md` in the ScalWS repository

Move the sites of an nginx server to ScalWS with `scalwsctl migrate nginx`, test them next
to nginx, switch ports 80 and 443 over, and roll back if needed.

`migrate` only reads the nginx configuration. Each `server` block becomes one ScalWS site
file under `/etc/scalws/sites.d/`; what it cannot convert is written into the file as
`# TODO:` lines. Which directive becomes what is listed in
[nginx directives in ScalWS](https://scalws.com/docs/howto/nginx-directives/).

## Prerequisites

* ScalWS installed on the nginx host, with `include: sites.d/*.yaml` in
  `/etc/scalws/scalws.yaml` (see [Sites](https://scalws.com/docs/sites/)). nginx keeps running.
* ScalWS listening on ports nginx does not use. The packaged configuration listens on
  80 and 443, so while nginx holds them the package installs ScalWS without starting
  it. Change its listeners to test ports (step 1 below) before starting it, and keep them
  until the switch.
* Document roots readable by group `scalws`, and not under `/root`: the units make `/home`
  and `/srv` read-only for ScalWS and hide `/root` (see
  [Files and services](https://scalws.com/docs/guide/files-and-services/)).
* PHP sites: their PHP-FPM pool must let user `scalws` connect to its socket. On Debian
  and Ubuntu: `sudo usermod -aG www-data scalws`, then restart the ScalWS unit.
* Migrated sites use PHP-FPM pools and proxy upstreams that already run, so they work
  under scalws-controller (the default) as well as under `scalwsd.service`.

## 1. Enable ACME first (for HTTPS sites)

`migrate` gives HTTPS hosts `tls: auto` when `server.acme` is enabled in ScalWS's
configuration; otherwise they get `tls: manual` and a TODO naming nginx's certificate
file. To let ScalWS order its own certificates, enable ACME before migrating, with test
listeners next to nginx:

```yaml
include: sites.d/*.yaml
server:
  listeners:
    - {name: http, address: "127.0.0.1:8080"}
    - {name: https, address: "127.0.0.1:8443", tls: {acme: true}}
  acme:
    enabled: true
    contact: ["mailto:ops@example.com"]
    terms_of_service_agreed: true
```

```sh
sudo scalwsctl check
sudo scalws-controller reload        # under scalws-controller
sudo systemctl restart scalwsd       # under scalwsd.service (listener change)
```

Until ScalWS owns port 80, its certificate orders fail (nginx answers the challenges) and
are retried later; see step 6. Details: [Let's Encrypt](https://scalws.com/docs/howto/lets-encrypt/).

## 2. Dry run

```sh
sudo scalwsctl migrate nginx --dry-run
```

It reads `/etc/nginx/nginx.conf` and every file it includes (give another path as an
argument) and prints the plan and the files, writing nothing:

```text
nginx: 4 virtual host(s) → 2 site(s)
  note: /etc/nginx/sites-enabled/default:1: no domain name (default server?), skipped
  write     example.com → /etc/scalws/sites.d/example.com.yaml
            TODO: add_header X-Frame-Options SAMEORIGIN: not converted (/etc/nginx/sites-enabled/example.com:31)
  write     shop.example.com → /etc/scalws/sites.d/shop.example.com.yaml
```

What is converted:

* `server_name` → `domain` and `aliases`; `www.<domain>` as a name → `www: alias`; a
  separate `www.` server that only redirects to the domain → `www: redirect`.
* A port-80 server that only redirects to HTTPS (including certbot's
  `if ($host = ...)` blocks) and the HTTPS server of the same names → one site with
  `https_redirect: true`.
* `root`, `index`; `fastcgi_pass` → `php:` with that pool (`unix:` or `tcp:`);
  `try_files ... /index.php?...` → `front_controller`.
* `proxy_pass` (an `upstream` group: its first server) → `proxy`, or a route for another
  `location`. `proxy_set_header Host $host` gives `preserve_host: true`; without it the
  site keeps nginx's behaviour with `preserve_host: false`.
* `return 301 ...` and `rewrite ... permanent` → `redirect`.

Everything else (`add_header`, `auth_basic`, `limit_req`, `rewrite` rules, regular
expression locations, `if` blocks, ...) becomes a TODO with its file and line.

A written file looks like this:

```yaml
# Migrated from nginx: /etc/nginx/sites-enabled/example.com:1, /etc/nginx/sites-enabled/example.com:20
# TODO: add_header X-Frame-Options SAMEORIGIN: not converted (/etc/nginx/sites-enabled/example.com:31)
# Site example.com. Fields: https://scalws.com/docs/sites/
domain: example.com
https_redirect: true
php:
  fastcgi: unix:/run/php/php8.3-fpm.sock
  front_controller: /index.php
root: /var/www/example.com
tls: auto
www: alias
```

## 3. Migrate

```sh
sudo scalwsctl migrate nginx
```

**Conflicts.** A domain ScalWS already serves is 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:
```

`y` and `n` decide for this domain, `a` overwrites this and every further conflict, `o`
skips them all. Only a domain's own site file can be replaced; it is kept as a hidden
`.<file>.replaced-<time>` next to it. A domain defined in `scalws.yaml` or in another site
is skipped with a note.

Then `migrate` asks `Write N site file(s)? [y/N]`, writes the files, checks the whole
configuration like a reload and reloads. If the check or the reload fails, every file of
the run is removed and replaced files are restored.

| Option | Effect |
|---|---|
| `--dry-run` | Print the plan and the files; change nothing. |
| `--overwrite` | Replace conflicting site files without asking. |
| `--skip-existing` | Keep conflicting sites without asking. |
| `-y`, `--yes` | Do not ask before writing. |
| `--no-reload` | Write and check, but leave the reload to you. |

Without a terminal (a script, a pipe), conflicts must be decided with `--overwrite` or
`--skip-existing`; otherwise nothing is written.

## 4. Work through the TODO lines

Open every file that has `# TODO:` lines and decide for each:

* rewrite rules: usually into an `.htaccess` file with `htaccess: true` (see
  [.htaccess support](https://scalws.com/docs/howto/htaccess/));
* `add_header`, `expires`: in `.htaccess` (`Header set`, `ExpiresByType`) or in the
  application;
* `auth_basic`, `allow`/`deny`: a route with
  [`basic_auth`](https://scalws.com/docs/reference/site-file/#routes-.basic_auth) or
  [`allow`](https://scalws.com/docs/reference/site-file/#routes-.allow)/[`deny`](https://scalws.com/docs/reference/site-file/#routes-.deny),
  protect the area in the application, or close it with
  [`security.deny_paths`](https://scalws.com/docs/reference/site-file/#security.deny_paths);
* `limit_req`: [`security.rate_limits`](https://scalws.com/docs/reference/site-file/#security.rate_limits);
* `proxy_pass` with a URI part: a route sends the original path to the upstream unless
  it sets [`rewrite.prefix`](https://scalws.com/docs/reference/site-file/#routes-.rewrite).

Then check and reload:

```sh
sudo scalwsctl check
sudo scalws-controller reload        # or: sudo scalwsctl reload
```

## 5. Test next to nginx

```sh
sudo scalwsctl site list
curl -sI -H 'Host: example.com' http://127.0.0.1:8080/
curl -s  -H 'Host: shop.example.com' http://127.0.0.1:8080/ | head
```

A site with `https_redirect: true` answers `301` to `https://...` here: that confirms the
site matched. Its pages can be checked over HTTPS after the switch.

## 6. Switch ports 80 and 443

Change the listeners in `/etc/scalws/scalws.yaml`:

```yaml
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}}
  acme:
    enabled: true
    contact: ["mailto:ops@example.com"]
    terms_of_service_agreed: true
```

Stop nginx and let ScalWS bind the ports (a short interruption):

```sh
sudo scalwsctl check
sudo systemctl stop nginx
sudo scalws-controller reload        # under scalws-controller: binds the new addresses
sudo systemctl restart scalwsd       # under scalwsd.service
```

Then watch the certificates:

```sh
scalwsctl certs                                           # under scalwsd.service
scalwsctl --socket /run/scalws/web-0/admin.sock certs     # under scalws-controller
```

The back-off of failed orders is kept in the server processes' memory. The restart, or the
controller's rolling reload that starts new instances, retries at once. When every site
shows `ok`, disable nginx at boot: `sudo systemctl disable nginx`.

## Rollback

nginx's configuration was never changed, so going back is quick:

```sh
sudo systemctl stop scalws-controller      # or: sudo systemctl stop scalwsd
sudo systemctl start nginx
```

Before starting ScalWS again, set its listeners back to the test ports. To remove the
migrated sites:

```sh
sudo scalwsctl site remove example.com     # kept as .example.com.yaml.removed-<time>
```

A site file that `migrate` replaced is next to the new one as `.<file>.replaced-<time>`;
rename it back to restore it, then check and reload.

## Common problems

* **`... has no include: for site files`**: add `include: sites.d/*.yaml` to `scalws.yaml`
  and create the directory (see [Sites](https://scalws.com/docs/sites/)).
* **`already served by ScalWS: ... run interactively, or pass --overwrite or --skip-existing`**:
  `migrate` ran without a terminal; pass one of the two options.
* **`the migrated sites do not pass the check; nothing was kept`**: the message names the
  file and field, for example a missing document root or certificate. Fix the cause
  (or the nginx configuration it came from) and run `migrate` again.
* **A server block is missing**: blocks without a usable `server_name` (`_`, regular
  expressions only) are skipped with a note; a name already used by an earlier block is
  kept once and reported.
* **`Address already in use` on port 80 or 443**: nginx (or another server) still holds
  the port: `sudo ss -ltnp 'sport = :443'`.
* **502 on PHP sites**: `scalws` cannot connect to the PHP-FPM socket; see
  [troubleshooting](https://scalws.com/docs/guide/troubleshooting/#php-fpm).
* **`state` `error` in `certs`**: DNS or port 80 does not reach ScalWS yet; see
  [Let's Encrypt](https://scalws.com/docs/howto/lets-encrypt/#common-problems).

See also: [Sites: moving from nginx or Apache](https://scalws.com/docs/sites/#moving-from-nginx-or-apache-scalwsctl-migrate),
[CLI reference](https://scalws.com/docs/reference/programs/scalwsctl/#migrate-nginx-migrate-apache).
