How-to

Migrating from nginx

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.

Prerequisites

  • ScalWS installed on the nginx host, with include: sites.d/*.yaml in /etc/scalws/scalws.yaml (see 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).
  • 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:

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:[email protected]"]
    terms_of_service_agreed: true
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.

2. Dry run

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:

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:

# 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

sudo scalwsctl migrate nginx

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

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.

OptionEffect
--dry-runPrint the plan and the files; change nothing.
--overwriteReplace conflicting site files without asking.
--skip-existingKeep conflicting sites without asking.
-y, --yesDo not ask before writing.
--no-reloadWrite 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);
  • add_header, expires: in .htaccess (Header set, ExpiresByType) or in the application;
  • auth_basic, allow/deny: a route with basic_auth or allow/deny, protect the area in the application, or close it with security.deny_paths;
  • limit_req: security.rate_limits;
  • proxy_pass with a URI part: a route sends the original path to the upstream unless it sets rewrite.prefix.

Then check and reload:

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

5. Test next to nginx

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:

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:[email protected]"]
    terms_of_service_agreed: true

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

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:

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:

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:

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).
  • 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.
  • state error in certs: DNS or port 80 does not reach ScalWS yet; see Let’s Encrypt.

See also: Sites: moving from nginx or Apache, CLI reference.