# Migrating from Apache

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

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](https://scalws.com/docs/howto/htaccess/)).

## Prerequisites

* ScalWS installed on the Apache host, with `include: sites.d/*.yaml` in
  `/etc/scalws/scalws.yaml` (see [Sites](https://scalws.com/docs/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](https://scalws.com/docs/guide/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:

```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 and are retried later; see step 6
and [Let's Encrypt](https://scalws.com/docs/howto/lets-encrypt/).

## 2. Dry run

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

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

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

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

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

* `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`](https://scalws.com/docs/reference/site-file/#routes-.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`](https://scalws.com/docs/reference/site-file/#routes-.rewrite).
* `WSGIDaemonProcess`, `PassengerEnabled`: run the application with gunicorn or Node.js
  and proxy to it (see [Python](https://scalws.com/docs/howto/python/), [Node.js](https://scalws.com/docs/howto/nodejs/)).

Then check and reload:

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

## 5. Test next to Apache

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

```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 Apache and let ScalWS bind the ports (a short interruption):

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

```sh
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](https://scalws.com/docs/howto/htaccess/).
* **`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](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),
[Migrating from nginx](https://scalws.com/docs/howto/from-nginx/).
