How-to

Reverse proxy

Put ScalWS in front of an HTTP application, send an /api path to a second upstream, and control the Host header and WebSockets.

Prerequisites

  • ScalWS installed, with include: sites.d/*.yaml in /etc/scalws/scalws.yaml.
  • The upstream applications running and reachable from the server, for example a web front end on 127.0.0.1:3000 and an API on 127.0.0.1:4000 or a Unix socket.

Upstreams are spoken to over HTTP/1.1, as http://host[:port], https://host[:port] or unix:/absolute/path. Works under both scalws-controller and scalwsd.service.

The site file

sudo scalwsctl site add example.com --proxy http://127.0.0.1:3000 --www alias

Then add the API route:

# /etc/scalws/sites.d/example.com.yaml
domain: example.com
www: alias
proxy: http://127.0.0.1:3000
routes:
  - path: /api
    runtime:
      type: proxy
      upstream: "http://127.0.0.1:4000"
      response_timeout: 120s
  - path: /healthz
    runtime: {type: respond, body: "ok\n"}
https_redirect: true
  • proxy: the default upstream, as a URL or as a map of settings.
  • routes[].path: /api matches /api and everything below it on whole segments (/api/users, not /apikeys). The longest matching prefix wins.
  • The path is not stripped: the API receives /api/users, not /users. An upstream URL with a path (http://127.0.0.1:4000/v1) is refused. If the API expects paths without the prefix, set the route’s rewrite (rewrite: {prefix: /} sends /api/users as /users), or configure the prefix in the application.
  • response_timeout: time for the upstream to send response headers (default 60s); connect_timeout defaults to 5s.

The Host header: preserve_host

preserve_host is true by default: the upstream sees the client’s Host (example.com). Set it to false when the upstream expects its own name, for example a virtual host on another server or a hosted service:

domain: example.com
proxy:
  upstream: "https://app.example.org"
  preserve_host: false

With false the request carries Host: app.example.org. For https:// upstreams the host is also the TLS server name; tls_ca_file adds a CA bundle for a private CA.

Headers the upstream gets:

  • X-Forwarded-For (the client address) and X-Forwarded-Proto (http or https), set by ScalWS. Values the client sent in Forwarded, X-Forwarded-* and X-Real-IP are discarded.
  • X-Request-Id, the server’s request id.
  • Hop-by-hop headers are removed in both directions.

WebSockets

websocket is true by default: HTTP/1.1 Upgrade: websocket requests are tunnelled to the upstream on a dedicated connection. Nothing to configure for a typical application. Turn it off on a route that must never upgrade:

domain: example.com
proxy: http://127.0.0.1:3000
routes:
  - path: /api
    runtime: {type: proxy, upstream: "unix:/run/api/api.sock", websocket: false}

Server-wide limits apply to all tunnels: server.limits.max_upgraded_connections (default 10000, beyond it 503) and server.limits.upgraded_idle_timeout (default 300s without traffic in either direction). WebSockets over HTTP/3 are not supported.

Apply and check

sudo scalwsctl check
sudo scalws-controller reload        # or: sudo scalwsctl reload under scalwsd.service

curl -sI -H 'Host: example.com' http://127.0.0.1/            # front end
curl -sI -H 'Host: example.com' http://127.0.0.1/api/ping    # API
curl -s  -H 'Host: example.com' http://127.0.0.1/healthz     # ok

A WebSocket handshake through the proxy answers 101 Switching Protocols when the upstream accepts it at that path (/socket here):

curl -si -H 'Host: example.com' -H 'Connection: Upgrade' -H 'Upgrade: websocket' \
     -H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' \
     http://127.0.0.1/socket | head -1

Common problems

  • 502 Bad Gateway: the upstream refused the connection or sent invalid HTTP. The log shows upstream request failed with reason connect or protocol. Test the upstream with curl from the server.
  • 504 Gateway Timeout: no response headers within response_timeout. Raise it on the route that needs it.
  • The application builds wrong URLs or redirects to the upstream address: it gets the upstream’s name in Host; keep preserve_host: true, or make the application use X-Forwarded-Proto for the scheme.
  • The API gets 404 for every path: it expects paths without /api; the prefix is not stripped unless the route sets rewrite: {prefix: /}.
  • runtime.upstream: ... in scalwsctl check: the upstream has a path, user information or port 0.
  • WebSockets fail: websocket: false on that route, or the max_upgraded_connections limit is reached.
  • A Unix socket upstream gives Permission denied: user scalws must be able to connect to the socket.

See also: applications: proxy, routes, troubleshooting.