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/*.yamlin/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/homeand/srvread-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). Userscalwsmust 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: truesudo 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-runWithout 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.yamlWhat is converted:
ServerNameandServerAlias→domainandaliases;www.<domain>as an alias →www: alias; a separatewww.host that only redirects to the domain →www: redirect.- A port-80 host that only redirects to HTTPS (
Redirect / https://..., orRewriteCond %{HTTPS} off/%{SERVER_NAME}withRewriteRule ^ https://%{SERVER_NAME}...as certbot writes it) and the HTTPS host of the same names → one site withhttps_redirect: true. DocumentRoot,DirectoryIndex;AllowOverrideother thanNone→htaccess: true.- PHP inside the
<VirtualHost>:SetHandler "proxy:unix:/run/php/php8.3-fpm.sock|fcgi://localhost"orProxyPassMatch ^/(.*\.php)$ fcgi://127.0.0.1:9000/...→php:with that pool. ProxyPass /→proxy;ProxyPassfor another path → a route.ProxyPreserveHost Ongivespreserve_host: true, otherwisefalse, as Apache sent it.Redirectof/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: alias3. Migrate
sudo scalwsctl migrate apache
sudo scalwsctl migrate apache /etc/httpd/conf/httpd.conf --skip-existingConflicts. 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.
| 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, conflicts must be decided with --overwrite or --skip-existing;
otherwise nothing is written.
4. Work through the TODO lines
RewriteRulein the virtual host: move the rules into an.htaccessfile in the document root and sethtaccess: true. Per-directory rules match the path without the leading/, so^/old$in the virtual host becomes^old$in.htaccess.Header,Expires*:.htaccesssupportsHeader set|append|unsetandExpiresByType.Auth*andRequire valid-user: a route withbasic_auth(an htpasswd file), or protect the area in the application. An.htaccesswith HTTP authentication answers403for its whole directory in ScalWS.Alias: a route with astaticruntime; route paths are not stripped, so itsrootis the directory that contains the alias name, unless the route setsrewrite.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 reload5. 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/ | headA 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: trueStop 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.serviceWatch 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 apache2Before 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’sa2enconf php8.3-fpm) or ran in mod_php. Only handlers inside the virtual host are read. Addphp: unix:/run/php/php8.3-fpm.sockto 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 runmigrateagain.- A
ProxyPasstarget with a path (http://127.0.0.1:4000/app/):migrateproxies 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:migrateran without a terminal.- A virtual host is missing: hosts without a usable
ServerNameare 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
.htaccessuses HTTP authentication or another access rule ScalWS cannot evaluate; see .htaccess support. Address already in useon 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.