How-to

Node.js applications

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 unitHow the application runsSite file key
scalwsd.serviceScalWS starts the processes, waits until they are ready, restarts them when they exit and balances requests across them.node
scalws-controller (default)You run the application yourself (for example as a systemd unit). ScalWS proxies to it.proxy

scalws-controller refuses node runtimes, also node with upstream:, because every instance would start its own copy of the application (ADR-0036).

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:
server.listen(process.env.PORT, process.env.HOST);

Under scalwsd.service: ScalWS runs Node.js

The quickest way:

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:

# /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: the argument vector. The working directory is root.
  • node.env: the environment is cleared and rebuilt; only PATH (system directories), LANG, HOME, the listen variables and these entries are set.
  • node.workers: without it, one process. With autoscale: auto the optimizer moves the count between min and max.
  • node.health_path: a worker gets requests only once this path answers with a non-5xx status.
  • 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/:

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 supplies the runtime (npm run start, listen: tcp, NODE_ENV=production, 60 s start-up timeout); run npm run build first:

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).

Under scalws-controller: proxy to your own service

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

# /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
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:

# /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 for timeouts, routes and preserve_host.

Check it works

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:

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, how managed runtimes are run.