# nginx directives in ScalWS

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

This page maps the nginx directives people use most to their ScalWS equivalent and says which ones `scalwsctl migrate nginx` converts for you.

For the migration itself, see [Migrating from nginx](https://scalws.com/docs/howto/from-nginx/).

## How to read the table

The column "Converted by `scalwsctl migrate`" uses four values:

* **yes**: the migration turns the directive into a site file key.
* **ignored (not needed)**: the migration skips it without a note, because ScalWS does
  this by itself or has no setting for it.
* **reported as TODO**: the directive is not converted; the migration writes a `# TODO:`
  line with the source file and line into the site file.
* **no**: the migration does not read the directive at all, and there is no note.

What the migration reads:

* It follows `include` everywhere and reads every `server { }` block and every
  `upstream { }` block. Directives in the `http { }`, `events { }` and main contexts are
  not read, so a directive that is only set there (for example `client_max_body_size` in
  `http { }`) gets no note. The rows below describe a directive inside a `server` or
  `location` block unless they say otherwise.
* Inside a `location`, directives whose name starts with `proxy_` or `fastcgi_` are
  skipped without a note, apart from `proxy_pass`, `proxy_set_header Host` and
  `fastcgi_pass`, which are converted.
* Every `server` block becomes one virtual host; hosts with the same main name are merged
  into one site (see `listen` and `return`).

Links in the "ScalWS equivalent" column go to the [configuration reference](https://scalws.com/docs/reference/).
Site keys are shown as they appear in a site file; the same keys exist under
`tenants[].applications[]` in the full configuration form.

## Server, names and files

| Directive | ScalWS equivalent | Converted by `scalwsctl migrate` | Notes |
|---|---|---|---|
| `listen` | [`server.listeners`](https://scalws.com/docs/reference/server/#server.listeners) | yes | Listeners are server-wide, not per site, so ports are not copied. A `listen` with `ssl`, `quic`, `443` or `:443` marks the virtual host as HTTPS, which sets [`tls`](https://scalws.com/docs/reference/site-file/#tls). An HTTP host that only redirects to HTTPS and the HTTPS host with the same name become one site. |
| `server_name` | [`domain`](https://scalws.com/docs/reference/site-file/#domain), [`aliases`](https://scalws.com/docs/reference/site-file/#aliases), [`www`](https://scalws.com/docs/reference/site-file/#www) | yes | The first name becomes `domain`, the others `aliases`. `www.<domain>` becomes `www: alias`; a separate server for `www.<domain>` that only redirects to the bare domain becomes `www: redirect`. Names that are not domains (`_`, regular expressions) are dropped with a note; a server with no usable name (a default server) is skipped. |
| `root` | [`root`](https://scalws.com/docs/reference/site-file/#root) | yes | Only `root` at server level. `root` inside a `location` is skipped without a note. |
| `index` | [`static.index`](https://scalws.com/docs/reference/site-file/#static.index), [`php.index`](https://scalws.com/docs/reference/site-file/#php.index) | yes | Written to `php.index` for a PHP site when a name ends in `.php`, otherwise to `static.index` when it differs from the default `index.html`. |
| `include` | [`include`](https://scalws.com/docs/reference/top-level/#include) | yes | Followed during the migration (paths relative to the directory of `nginx.conf`, `*` and `?` wildcards), up to 2000 files and 16 levels. ScalWS's own `include` is a list of files merged into the main configuration. |
| `sendfile` | [`server.sendfile`](https://scalws.com/docs/reference/server/#server.sendfile) | ignored (not needed) | On by default for large static files on plain-TCP HTTP/1 connections (Linux). |
| `worker_processes` | [`server.event_loops`](https://scalws.com/docs/reference/server/#server.event_loops) | no | Main context. Default: one event loop per CPU. |
| `worker_connections` | [`server.limits.max_connections`](https://scalws.com/docs/reference/server-limits/#server.limits.max_connections) | no | `events` context. ScalWS limits open client connections across all listeners (default 10000). |

## Locations and routing

| Directive | ScalWS equivalent | Converted by `scalwsctl migrate` | Notes |
|---|---|---|---|
| `location /prefix/` | [`routes`](https://scalws.com/docs/reference/site-file/#routes) | yes | A prefix location with `proxy_pass`, `fastcgi_pass` or a redirect `return` becomes a route with that runtime (trailing `/` removed). A `proxy_pass` with a URI part, `/` included, under a prefix other than `/` is reported as TODO: nginx replaces the prefix, while a ScalWS route sends the full request path unless it sets [`rewrite.prefix`](https://scalws.com/docs/reference/site-file/#routes-.rewrite). `location /` sets the site's default runtime. A location without such a directive (only `root`, `try_files`, `expires` and the like) gives no route. Routes match whole path segments, longest first. Nested locations are reported as TODO. |
| `location = /path` | [`routes`](https://scalws.com/docs/reference/site-file/#routes) | yes | Converted like a prefix location: ScalWS has no exact-match routes, so the route also matches paths below it. |
| `location ~ regex` / `~*` | [`routes[].path_type: regex`](/docs/reference/site-file/#routes-.path_type) | reported as TODO | A route with `path_type: regex` matches a regular expression anchored at the start of the path (`(?i)` for `~*`); the migration does not write such routes. A regex location with `fastcgi_pass` and `php` in its pattern (the usual `\.php$` block) makes the site a PHP site. `location ~ /\.ht` is skipped (ScalWS never serves dotfiles) and so is any `acme-challenge` location. Other regex locations with a handler are reported; for rules, use `.htaccess` `RewriteRule` with [`htaccess`](https://scalws.com/docs/reference/site-file/#htaccess). |
| `try_files` | [`php.front_controller`](https://scalws.com/docs/reference/site-file/#php.front_controller) | yes | Only when the last argument is a `.php` file (`/index.php?$args` becomes `front_controller: /index.php`). Other `try_files` in a location are skipped without a note; at server level they are reported. For a single-page app fallback use a `RewriteRule` in `.htaccess`. |
| `return 301\|302\|303\|307\|308 URL` | [`https_redirect`](https://scalws.com/docs/reference/site-file/#https_redirect), [`redirect`](https://scalws.com/docs/reference/site-file/#redirect) | yes | `return 301 https://$host$request_uri` (or the same host by name) on a port-80 server becomes `https_redirect: true` on the HTTPS site. A redirect to another origin becomes a `redirect` runtime; `$request_uri` in the target sets [`redirect.keep_path`](https://scalws.com/docs/reference/site-file/#redirect.keep_path). |
| `return` with other codes | [`runtime` of `type: respond`](https://scalws.com/docs/reference/site-file/#runtime.respond.status) | no | `return 404` and `return 444` (certbot's port-80 servers) and other non-redirect codes are dropped without a note. A fixed response is a `respond` runtime with `status`, `body` and `content_type`. |
| `rewrite` | [`redirect`](https://scalws.com/docs/reference/site-file/#redirect), `.htaccess` `RewriteRule` | yes | At server level, only `rewrite … permanent` or `redirect` is converted, and then into a redirect of the whole site to the target: the pattern is not kept. Check such sites. Other server-level rewrites (`last`, `break`, no flag) and every rewrite inside a location are reported as TODO. Internal rewrites work in `.htaccess` with `htaccess: true`. |
| `if` | `.htaccess` `RewriteCond` | reported as TODO | Only certbot's `if ($host = …) { return 301 https://$host$request_uri; }` is converted, into `https_redirect`. Everything else is reported. |
| `error_page` | `.htaccess` `ErrorDocument` | reported as TODO | With `htaccess: true`, `ErrorDocument 404 /404.html` serves a local document with the original status, a URL redirects, and text is sent as is. |

## Upstreams, proxying and PHP

| Directive | ScalWS equivalent | Converted by `scalwsctl migrate` | Notes |
|---|---|---|---|
| `proxy_pass` | [`proxy.upstream`](https://scalws.com/docs/reference/site-file/#proxy.upstream) | yes | `http://`, `https://` and `unix:` upstreams. A URI part in the target (`proxy_pass http://app/api/`) is noted: the migration does not rewrite the path; a route's [`rewrite`](https://scalws.com/docs/reference/site-file/#routes-.rewrite) (`prefix` or `path`) can. |
| `proxy_set_header Host` | [`proxy.preserve_host`](https://scalws.com/docs/reference/site-file/#proxy.preserve_host) | yes | `proxy_set_header Host $host` or `$http_host` gives `preserve_host: true`; without it the migration sets `preserve_host: false` (ScalWS's own default is `true`). Other `proxy_set_header` lines are skipped without a note. ScalWS removes client-supplied `X-Forwarded-*` and sends its own `X-Forwarded-For`. |
| `upstream` | [`proxy.upstream`](https://scalws.com/docs/reference/site-file/#proxy.upstream), [`php.fastcgi`](https://scalws.com/docs/reference/site-file/#php.fastcgi) | yes | An `upstream` name used by `proxy_pass` or `fastcgi_pass` is replaced by the **first** `server` of the group; a group with more servers is reported as TODO ("only the first is used"); weights and `keepalive` are dropped. For load balancing, define a pool in the main configuration's [`upstreams`](https://scalws.com/docs/reference/top-level/#upstreams) and use it from [`proxy.backends`](https://scalws.com/docs/reference/site-file/#proxy.backends) (see [load balancing](https://scalws.com/docs/guide/load-balancing/)). |
| `proxy_redirect`, `proxy_*_timeout`, `proxy_buffer*` | [`proxy.connect_timeout`](https://scalws.com/docs/reference/site-file/#proxy.connect_timeout), [`proxy.response_timeout`](https://scalws.com/docs/reference/site-file/#proxy.response_timeout) | ignored (not needed) | Buffering is not configurable. ScalWS does not rewrite `Location` headers from the upstream. |
| `fastcgi_pass` | [`php.fastcgi`](https://scalws.com/docs/reference/site-file/#php.fastcgi) | yes | `unix:/run/php/php8.3-fpm.sock` stays `unix:…`; `127.0.0.1:9000` becomes `tcp:127.0.0.1:9000`. Instead of an existing pool, ScalWS can also run PHP-FPM itself with [`php.fpm`](https://scalws.com/docs/reference/site-file/#php.fpm). |
| `fastcgi_param`, `fastcgi_index`, `fastcgi_split_path_info`, `fastcgi_read_timeout` | [`php.response_timeout`](https://scalws.com/docs/reference/site-file/#php.response_timeout) | ignored (not needed) | ScalWS sends the FastCGI parameters itself. |
| `proxy_cache` / `fastcgi_cache` | [`cache`](https://scalws.com/docs/reference/site-file/#cache), [`server.cache`](https://scalws.com/docs/reference/server-cache/#server.cache.memory) | reported as TODO | Reported at server level; inside a location they are skipped without a note (`proxy_`/`fastcgi_` prefix), and `proxy_cache_path` / `fastcgi_cache_path` in `http` are not read. ScalWS caches per site with `cache` (bypass paths, cookies and query parameters, `default_ttl`, `stale_while_revalidate`) in the shared memory and optional disk store of `server.cache`. |

## TLS and HTTP versions

| Directive | ScalWS equivalent | Converted by `scalwsctl migrate` | Notes |
|---|---|---|---|
| `ssl_certificate` | [`tls`](https://scalws.com/docs/reference/site-file/#tls), [`server.listeners[].tls.certificates`](/docs/reference/server-listeners/#server.listeners-.tls.certificates) | yes | Marks the host as HTTPS. With [`server.acme.enabled`](https://scalws.com/docs/reference/server-acme/#server.acme.enabled) the site gets `tls: auto` (a certificate from Let's Encrypt); otherwise `tls: manual` and a TODO to add the certificate to the HTTPS listener. The file is not copied. |
| `ssl_certificate_key` | [`server.listeners[].tls.certificates[].key`](/docs/reference/server-listeners/#server.listeners-.tls.certificates-.key) | ignored (not needed) | Goes with the certificate on the listener when you use `tls: manual`. |
| `ssl_protocols`, `ssl_ciphers`, `ssl_prefer_server_ciphers`, `ssl_session_*`, `ssl_dhparam`, `ssl_ecdh_curve`, `ssl_stapling*`, `ssl_early_data` | none | ignored (not needed) | No settings: TLS 1.2 and 1.3 with forward-secret AEAD suites, stateless TLS 1.3 session tickets, no 0-RTT, no OCSP stapling. |
| `ssl_client_certificate`, `ssl_verify_client` | none | reported as TODO | Client certificates are not requested on application listeners. |
| `http2` | none | ignored (not needed) | HTTP/2 is always on: ALPN `h2` on TLS listeners, prior-knowledge h2c on plain listeners. HTTP/3 is [`server.listeners[].http3`](/docs/reference/server-listeners/#server.listeners-.http3). |

## Headers, caching and compression

| Directive | ScalWS equivalent | Converted by `scalwsctl migrate` | Notes |
|---|---|---|---|
| `add_header` | `.htaccess` `Header`, [`routes[].response_headers`](/docs/reference/site-file/#routes-.response_headers) | reported as TODO | No site-level key for response headers; a route can set them with `response_headers`. With [`htaccess`](https://scalws.com/docs/reference/site-file/#htaccess) `true`, `Header set\|append\|add\|unset` in `.htaccess` (conditional `env=`/`expr=` forms are not supported). ScalWS adds no `Strict-Transport-Security` by itself. |
| `expires` | `.htaccess` `ExpiresByType` / `ExpiresDefault` | reported as TODO | With `htaccess: true`. |
| `gzip`, `gzip_*` | [`static.precompressed`](https://scalws.com/docs/reference/site-file/#static.precompressed) | ignored (not needed) | ScalWS does not compress responses on the fly. With `precompressed: true` the static runtime serves an existing `<file>.br` or `<file>.gz` to clients that accept it. Proxied and PHP responses pass as the backend sends them. |

## Limits and timeouts

| Directive | ScalWS equivalent | Converted by `scalwsctl migrate` | Notes |
|---|---|---|---|
| `client_max_body_size` | [`server.limits.max_request_body_bytes`](https://scalws.com/docs/reference/server-limits/#server.limits.max_request_body_bytes) | reported as TODO | Server-wide (default 10 MiB); a route can override it with [`max_body`](https://scalws.com/docs/reference/site-file/#routes-.max_body). A larger `Content-Length` gets `413`. |
| `keepalive_timeout` | [`server.limits.idle_timeout`](https://scalws.com/docs/reference/server-limits/#server.limits.idle_timeout) | ignored (not needed) | Default 60 s, for HTTP/1 and HTTP/2. |
| `client_header_timeout` | [`server.limits.header_read_timeout`](https://scalws.com/docs/reference/server-limits/#server.limits.header_read_timeout) | reported as TODO | Default 15 s. |
| `client_body_timeout` | [`server.limits.body_read_timeout`](https://scalws.com/docs/reference/server-limits/#server.limits.body_read_timeout) | reported as TODO | Maximum gap between body chunks, default 30 s. |
| `limit_req` | [`server.security.per_ip.requests_per_second`](https://scalws.com/docs/reference/server-security/#server.security.per_ip.requests_per_second), [`security.rate_limits`](https://scalws.com/docs/reference/site-file/#security.rate_limits) | reported as TODO | `per_ip` limits every client address (IPv6 per /64) server-wide, with [`burst`](https://scalws.com/docs/reference/server-security/#server.security.per_ip.burst); `security.rate_limits` sets a rate for path patterns of one site. Over the limit: `429`. `limit_req_zone` in `http` is not read. |
| `limit_conn` | [`server.security.per_ip.max_connections`](https://scalws.com/docs/reference/server-security/#server.security.per_ip.max_connections) | reported as TODO | Server-wide, per client address. |

## Access control and client addresses

| Directive | ScalWS equivalent | Converted by `scalwsctl migrate` | Notes |
|---|---|---|---|
| `allow` / `deny` | `.htaccess` `Require ip` / `Require all denied`, [`security.deny_paths`](https://scalws.com/docs/reference/site-file/#security.deny_paths) | reported as TODO | Address rules: `Require [not] ip …` or `Order`/`Allow`/`Deny` in `.htaccess` (with `htaccess: true`), answered `403`. To close a path for everyone: `deny_paths` (answered `404`, `*` wildcards). A route can also take [`allow`](https://scalws.com/docs/reference/site-file/#routes-.allow) and [`deny`](https://scalws.com/docs/reference/site-file/#routes-.deny) lists of addresses and networks (answered `403`). |
| `auth_basic`, `auth_basic_user_file` | [`routes[].basic_auth`](/docs/reference/site-file/#routes-.basic_auth) | reported as TODO | A route's `basic_auth` checks an htpasswd file (`users_file`, bcrypt, SHA-256/512 crypt or Apache MD5 entries) with a `realm`. In `.htaccess`, `Auth*` directives are not supported and close the directory with `403` rather than publish it. |
| `set_real_ip_from` | [`server.trusted_proxies`](https://scalws.com/docs/reference/server/#server.trusted_proxies) | reported as TODO | For requests from a trusted peer, the client is the right-most `X-Forwarded-For` entry that is not itself a trusted proxy. |
| `real_ip_header` | [`server.trusted_proxies`](https://scalws.com/docs/reference/server/#server.trusted_proxies), [`server.listeners[].proxy_protocol`](/docs/reference/server-listeners/#server.listeners-.proxy_protocol) | reported as TODO | Only `X-Forwarded-For` and the PROXY protocol are read. Other headers (`X-Real-IP`, a CDN's own header) and `Forwarded` are not supported. |

## Logging

| Directive | ScalWS equivalent | Converted by `scalwsctl migrate` | Notes |
|---|---|---|---|
| `access_log` | [`server.logging.access_log`](https://scalws.com/docs/reference/server-logging/#server.logging.access_log), [`server.logging.format`](https://scalws.com/docs/reference/server-logging/#server.logging.format) | ignored (not needed) | One line per request, on by default, `json` or `text`, written to the service log (`journalctl -u scalwsd`; under scalws-controller each instance's own file, see [logs](https://scalws.com/docs/guide/files-and-services/#logs)). No per-site log files or custom formats. |
| `error_log` | [`server.logging.level`](https://scalws.com/docs/reference/server-logging/#server.logging.level) | ignored (not needed) | Also to the service log. |
