How-to

Migrating from Apache

Move the virtual hosts of an Apache httpd server to ScalWS with scalwsctl migrate apache, test them next to Apache, switch ports 80 and 443 over, and roll back if needed.

migrate only reads the Apache configuration. Each <VirtualHost> becomes one ScalWS site file under /etc/scalws/sites.d/; what it cannot convert is written into the file as # TODO: lines. Sites that rely on .htaccess keep working with htaccess: true (see .htaccess support).

Prerequisites

  • ScalWS installed on the Apache host, with include: sites.d/*.yaml in /etc/scalws/scalws.yaml (see Sites). Apache keeps running.
  • ScalWS listening on ports Apache does not use. The packaged configuration listens on 80 and 443, so while Apache 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 through PHP-FPM. ScalWS has no equivalent of mod_php: a site whose PHP runs in mod_php needs a PHP-FPM pool (sudo apt install php8.3-fpm). User scalws must be able to connect to the pool’s 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 (<VirtualHost *:443>, SSLEngine on) tls: auto when server.acme is enabled; otherwise tls: manual and a TODO naming Apache’s certificate file. Enable ACME before migrating, with test listeners next to Apache:

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 and are retried later; see step 6 and Let’s Encrypt.

2. Dry run

sudo scalwsctl migrate apache --dry-run

Without a path it reads /etc/apache2/apache2.conf, or /etc/httpd/conf/httpd.conf when the first does not exist, and every file named by Include and IncludeOptional. It prints the plan and the files and writes nothing:

Apache: 3 virtual host(s) → 2 site(s)
  write     example.com → /etc/scalws/sites.d/example.com.yaml
            TODO: header always set X-Frame-Options SAMEORIGIN: not converted (/etc/apache2/sites-enabled/example.com-le-ssl.conf:14)
  write     app.example.com → /etc/scalws/sites.d/app.example.com.yaml

What is converted:

  • ServerName and ServerAlias → domain and aliases; www.<domain> as an alias → www: alias; a separate www. host that only redirects to the domain → www: redirect.
  • A port-80 host that only redirects to HTTPS (Redirect / https://..., or RewriteCond %{HTTPS} off / %{SERVER_NAME} with RewriteRule ^ https://%{SERVER_NAME}... as certbot writes it) and the HTTPS host of the same names → one site with https_redirect: true.
  • DocumentRoot, DirectoryIndex; AllowOverride other than None → htaccess: true.
  • PHP inside the <VirtualHost>: SetHandler "proxy:unix:/run/php/php8.3-fpm.sock|fcgi://localhost" or ProxyPassMatch ^/(.*\.php)$ fcgi://127.0.0.1:9000/... → php: with that pool.
  • ProxyPass / → proxy; ProxyPass for another path → a route. ProxyPreserveHost On gives preserve_host: true, otherwise false, as Apache sent it.
  • Redirect of / to another origin → redirect.

Everything else (Header, Auth*, Alias, ScriptAlias, WSGI*, Passenger*, RewriteRule, Require all denied in a section, …) becomes a TODO with its file and line.

A written file looks like this:

# Migrated from Apache: /etc/apache2/sites-enabled/example.com.conf:1, /etc/apache2/sites-enabled/example.com-le-ssl.conf:2
# TODO: header always set X-Frame-Options SAMEORIGIN: not converted (/etc/apache2/sites-enabled/example.com-le-ssl.conf:14)
# Site example.com. Fields: https://scalws.com/docs/sites/
domain: example.com
htaccess: true
https_redirect: true
php: unix:/run/php/php8.3-fpm.sock
root: /var/www/example.com
tls: auto
www: alias

3. Migrate

sudo scalwsctl migrate apache
sudo scalwsctl migrate apache /etc/httpd/conf/httpd.conf --skip-existing

Conflicts. A domain ScalWS already serves is never written twice. For each one migrate asks:

example.com is already served (example.com by example-com/example-com (/etc/scalws/sites.d/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, conflicts must be decided with --overwrite or --skip-existing; otherwise nothing is written.

4. Work through the TODO lines

  • RewriteRule in the virtual host: move the rules into an .htaccess file in the document root and set htaccess: true. Per-directory rules match the path without the leading /, so ^/old$ in the virtual host becomes ^old$ in .htaccess.
  • Header, Expires*: .htaccess supports Header set|append|unset and ExpiresByType.
  • Auth* and Require valid-user: a route with basic_auth (an htpasswd file), or protect the area in the application. An .htaccess with HTTP authentication answers 403 for its whole directory in ScalWS.
  • Alias: a route with a static runtime; route paths are not stripped, so its root is the directory that contains the alias name, unless the route sets rewrite.prefix.
  • WSGIDaemonProcess, PassengerEnabled: run the application with gunicorn or Node.js and proxy to it (see Python, Node.js).

Then check and reload:

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

5. Test next to Apache

sudo scalwsctl site list
curl -sI -H 'Host: example.com' http://127.0.0.1:8080/
curl -s  -H 'Host: app.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 Apache and let ScalWS bind the ports (a short interruption):

sudo scalwsctl check
sudo systemctl stop apache2          # httpd on Red Hat based systems
sudo scalws-controller reload        # under scalws-controller: binds the new addresses
sudo systemctl restart scalwsd       # under scalwsd.service

Watch the certificates with scalwsctl certs (under scalws-controller: scalwsctl --socket /run/scalws/web-0/admin.sock certs). 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 Apache at boot: sudo systemctl disable apache2.

Rollback

Apache’s configuration was never changed:

sudo systemctl stop scalws-controller      # or: sudo systemctl stop scalwsd
sudo systemctl start apache2

Before starting ScalWS again, set its listeners back to the test ports. Remove migrated sites with sudo scalwsctl site remove <domain> (the file is kept as a hidden .removed-<time> backup); a site file that migrate replaced is next to it as .<file>.replaced-<time> and can be renamed back.

Common problems

  • A PHP site came out without php: (static): PHP was configured outside the <VirtualHost> (for example Debian’s a2enconf php8.3-fpm) or ran in mod_php. Only handlers inside the virtual host are read. Add php: unix:/run/php/php8.3-fpm.sock to the site file before the switch: a static site does not run PHP.
  • the migrated sites do not pass the check; nothing was kept: the message names the file and field; fix the source or the written site file and run migrate again.
  • A ProxyPass target with a path (http://127.0.0.1:4000/app/): migrate proxies to the origin (http://127.0.0.1:4000) and writes a TODO line, because ScalWS keeps the request’s own path; adjust the application or the route by hand.
  • already served by ScalWS: ... run interactively, or pass --overwrite or --skip-existing: migrate ran without a terminal.
  • A virtual host is missing: hosts without a usable ServerName are skipped with a note; a name already used by an earlier host is kept once and reported.
  • 403 for a directory that worked in Apache: its .htaccess uses HTTP authentication or another access rule ScalWS cannot evaluate; see .htaccess support.
  • Address already in use on port 80 or 443: Apache still holds the port: sudo ss -ltnp 'sport = :80'.

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