# Node.js applications

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

Run a Node.js application behind ScalWS: started and supervised by ScalWS under
`scalwsd.service`, or run by systemd and proxied under scalws-controller.

## Which way?

| Server unit | How the application runs | Site file key |
|---|---|---|
| `scalwsd.service` | ScalWS starts the processes, waits until they are ready, restarts them when they exit and balances requests across them. | [`node`](https://scalws.com/docs/reference/site-file/#node) |
| `scalws-controller` (default) | You run the application yourself (for example as a systemd unit). ScalWS proxies to it. | [`proxy`](https://scalws.com/docs/reference/site-file/#proxy) |

scalws-controller refuses `node` runtimes, also `node` with `upstream:`, because every
instance would start its own copy of the application
([ADR-0036](https://scalws.com/docs/adr/0036-multi-instance-state/)).

## Prerequisites

* ScalWS installed, with `include: sites.d/*.yaml` in `/etc/scalws/scalws.yaml`.
* The application deployed outside `/home`, for example `/srv/app`, with its dependencies
  installed (`npm ci`) and built if needed.
* The application listens on the port or socket given in the `PORT` environment variable.
  This line works in every case below:

```js
server.listen(process.env.PORT, process.env.HOST);
```

## Under scalwsd.service: ScalWS runs Node.js

The quickest way:

```sh
sudo scalwsctl site add app.example.com --root /srv/app --node "node server.js"
```

The command is split on whitespace and run without a shell. The site file it writes, with
more settings added:

```yaml
# /etc/scalws/sites.d/app.example.com.yaml
domain: app.example.com
root: /srv/app
node:
  command: [node, server.js]
  env: {NODE_ENV: production}
  workers: {min: 2, max: 6, autoscale: auto}
  health_path: /healthz
https_redirect: true
```

* [`node.command`](https://scalws.com/docs/reference/site-file/#node.command): the argument vector. The
  working directory is `root`.
* [`node.env`](https://scalws.com/docs/reference/site-file/#node.env): the environment is cleared and
  rebuilt; only `PATH` (system directories), `LANG`, `HOME`, the listen variables and
  these entries are set.
* [`node.workers`](https://scalws.com/docs/reference/site-file/#node.workers): without it, one process.
  With `autoscale: auto` the optimizer moves the count between `min` and `max`.
* [`node.health_path`](https://scalws.com/docs/reference/site-file/#node.health_path): a worker gets
  requests only once this path answers with a non-5xx status.
* [`node.listen`](https://scalws.com/docs/reference/site-file/#node.listen): `unix` (default, `PORT` is a
  socket path) or `tcp` (a free loopback port; `PORT` is a number and `HOST` is set), for
  frameworks that accept only a port number.

Static assets can bypass Node.js with a route. Route paths are not stripped, so the
route's `root` is the directory that contains `assets/`:

```yaml
domain: app.example.com
root: /srv/app
node: {command: [node, server.js], env: {NODE_ENV: production}}
routes:
  - path: /assets
    runtime: {type: static, root: /srv/app/public, precompressed: true}
```

For Next.js, [`profile: nextjs`](https://scalws.com/docs/reference/site-file/#profile) supplies the runtime
(`npm run start`, `listen: tcp`, `NODE_ENV=production`, 60 s start-up timeout); run
`npm run build` first:

```yaml
domain: next.example.com
root: /srv/next
profile: nextjs
```

The processes run inside the unit's sandbox, where `/srv` is read-only. A directory the
application writes to needs a drop-in with `ReadWritePaths=` (see
[quick start](https://scalws.com/docs/guide/quick-start/#under-scalwsdservice-a-managed-php-fpm-pool)).

## Under scalws-controller: proxy to your own service

Run the application as a systemd unit that listens on loopback only:

```ini
# /etc/systemd/system/app-example-com.service
[Unit]
Description=app.example.com
After=network.target

[Service]
User=app
WorkingDirectory=/srv/app
Environment=NODE_ENV=production PORT=3000 HOST=127.0.0.1
ExecStart=/usr/bin/node server.js
Restart=always

[Install]
WantedBy=multi-user.target
```

```sh
sudo systemctl daemon-reload
sudo systemctl enable --now app-example-com
sudo scalwsctl site add app.example.com --proxy http://127.0.0.1:3000
```

The site file:

```yaml
# /etc/scalws/sites.d/app.example.com.yaml
domain: app.example.com
proxy: http://127.0.0.1:3000
```

The proxy sends the client's `Host` header and tunnels WebSockets by default; see
[reverse proxy](https://scalws.com/docs/howto/reverse-proxy/) for timeouts, routes and `preserve_host`.

## Check it works

```sh
curl -sI -H 'Host: app.example.com' http://127.0.0.1/
sudo scalwsctl site list
scalwsctl apps                       # under scalwsd.service: workers ready/target
```

Under scalwsd.service the application's stdout and stderr go to the journal with target
`scalws::app`:

```sh
journalctl -u scalwsd -o cat | jq -c 'select(.target == "scalws::app")'
```

## Common problems

* **`scalws-controller cannot run a managed Node.js process`** from `site add --node`, or
  **`managed node runtimes are not supported with several instances`** on reload: use the
  proxy setup above, or switch to `scalwsd.service`
  (`sudo systemctl disable --now scalws-controller && sudo systemctl enable --now scalwsd`).
* **503 with `Retry-After: 1`, `scalwsctl apps` shows no ready worker**: the application
  does not listen on `PORT`, crashes at start, or `health_path` answers 5xx. A worker that
  is not ready within `startup_timeout` (30 s) is restarted.
* **`node` not found**: the command is looked up in the system directories
  (`/usr/local/bin`, `/usr/bin`, ...). For a Node.js installed elsewhere, write the
  absolute path: `command: [/opt/node/bin/node, server.js]`.
* **Arguments with spaces**: use the list form of `command`; the string form is split on
  whitespace.
* **502 under the controller**: the service is not running or listens elsewhere; test it
  with `curl http://127.0.0.1:3000/`.

See also: [applications](https://scalws.com/docs/guide/applications/#node),
[how managed runtimes are run](https://scalws.com/docs/guide/applications/#how-managed-runtimes-are-run).
