Wsrelay on PMSS
On a Pulsed Media seedbox, wsrelay runs in rootless Docker inside your account and answers at wss://SERVER.pulsedmedia.com/user-USERNAME/wsrelay/, behind your panel login. Tested on a Pulsed Media seedbox on 24 October 2026 with nicholashill/wsrelay 2.2: through the web address, a WebSocket handshake without the panel login got 401, with the panel login 101 (connected), and with a wrong origin 403. wsrelay is a WebSocket relay: by its documentation, web pages connected to it send messages to each other and save small pieces of data on it, so a page you host can share live state without you writing a server program. Sending messages was not part of the test.
wsrelay has no login of its own, and other accounts on the same server can connect to its port directly. Use it only for messages and data that would not matter if another customer on that server read or changed them.
Privacy
Level: MEDIUM. Using wsrelay reduces your privacy because other customers on the server can join it:
- wsrelay has no login. Its only checks are the
Originheader and that the client asks for a WebSocket subprotocol; by default any subprotocol name is accepted. The port on127.0.0.1is reachable by every account on the same server, and a program there can send anyOriginit likes, so another customer can join the relay and read or send messages. - Realms (the relay's rooms) are plain numbers, not secrets. By the wsrelay protocol, any client can join a realm, message everyone in another realm, read saved data from any realm (
<4,scorein the protocol's own example) and overwrite or delete the data saved in a realm it joins (>scorewith no value deletes). - The panel login in front of the web address keeps out visitors from the internet. It does not cover the port.
- No connection to an outside service was found in the project's documentation; the code was not audited.
The project's optional JWT login (see JWT.md) is "currently only enforced for deleting persisted realms", so it does not close the relay to other clients.
Requirements
| Item | Measured or required |
|---|---|
| Memory | Not measured separately in the test. |
| Disk | The image, about 49 MB compressed on Docker Hub. The install keeps no data volume. |
| Own login | None. See Privacy. |
| Web address | wss://SERVER.pulsedmedia.com/user-USERNAME/wsrelay/ through a proxy fragment with map-urlpath.
|
| Other apps | A web page that speaks the wsrelay protocol, for example through the project's client library. |
Install
Run these over SSH as your own user. Replace SERVER with your server's name as it appears in your panel address, USERNAME with your user name, and PORT with a free port between 2000 and 38000; check with ss -tln that nothing listens on it. Run docker ps first: an empty list means Docker is running.
1. Make the folder and the settings file. ACCEPTED_ORIGINS is the address of the web page that will use the relay: https:// and the host name only, with no port, path or trailing slash, because wsrelay compares only those two parts. Separate several addresses with commas and no spaces. The test used the server's own address. Every account's pages on the server are served under that same address, so the origin check cannot tell your page from another customer's page there. Left out, the setting accepts every origin (upstream README).
mkdir -p ~/docker/wsrelay chmod 700 ~/docker/wsrelay cat > ~/docker/wsrelay/app.env <<'EOF' ACCEPTED_ORIGINS=https://SERVER.pulsedmedia.com EOF chmod 600 ~/docker/wsrelay/app.env
2. Pull the tested image.
docker pull nicholashill/wsrelay@sha256:dbbe78ff89e20f9bcd41fe020c74cd9a981ab5c9d86b8c88a33236ae7cf12873
3. Start it on a loopback port. In the other app guides on this wiki, the app's own login is set before its port is published. wsrelay has none, so other accounts can reach the port from this first start.
docker run -d --name wsrelay --restart unless-stopped --env-file ~/docker/wsrelay/app.env \ -p 127.0.0.1:PORT:22002 \ nicholashill/wsrelay@sha256:dbbe78ff89e20f9bcd41fe020c74cd9a981ab5c9d86b8c88a33236ae7cf12873
Keep 127.0.0.1: in the -p option. docker logs wsrelay shows "Version 2.2", the accepted origin and "Listening on port 22002."
4. Add the proxy fragment as ~/.lighttpd/custom.d/wsrelay.conf, with your user name and port. It keeps the panel login: do not add auth.require = (), because wsrelay has no login to fall back on. The setenv line stops your web server from passing your panel login, which is your account password, on to wsrelay after checking it; this line was tested with ordinary web requests, not with a wsrelay connection.
$HTTP["url"] =~ "^/user-USERNAME/wsrelay/" {
setenv.set-request-header = ( "Authorization" => "" )
proxy.server = ( "" => ( ( "host" => "127.0.0.1", "port" => PORT ) ) )
proxy.header = ( "upgrade" => "enable",
"map-urlpath" => ( "/user-USERNAME/wsrelay/" => "/", "/user-USERNAME/wsrelay" => "" ) )
}
Check it with /usr/sbin/lighttpd -t -f ~/.lighttpd.conf. The web server picks the file up within a minute or two (how that works).
Configure
ACCEPTED_ORIGINSmust match the page that opens the connection. In the test a handshake with another origin, or with none, got 403. For another origin the log says "Rejected connection from non-permitted origin" and names it.- Clients must ask for a WebSocket subprotocol. The test client asked for
wsrelay; a handshake without any subprotocol got 403, even with the right origin. Any name is accepted by default.ACCEPTED_PROTOCOLSin the upstream README narrows the list (not tested), so the subprotocol is not a password. - Clients connect to
wss://SERVER.pulsedmedia.com/user-USERNAME/wsrelay/with your panel login; in the testcurlsent it, and a page in a browser was not tried. The panel login opens your whole panel, so the relay is for your own pages and devices. Pulsed Media's terms say a personal account is used by its primary owner only and that access to its services may not be resold or shared; giving a service to someone free of charge is allowed, and you carry the responsibility for what they do. - Without a data volume, realm data that clients save stays inside the container and is gone when the container is removed. The upstream README shows mounting
/app/datato keep it; that was not tested.
Integrate
The upstream documentation describes no integration with Sonarr, Radarr, Prowlarr or Jellyfin. Its clients are web pages. Pages in your /user-USERNAME/ folder are behind your panel login. Pages in the public folder (/public-USERNAME/) are open to the internet, so their visitors have no panel login and the relay answers them with 401. See web hosting.
Test
The handshake through the web address, from the server or your own computer; curl asks for the panel password:
curl -s -i -N --max-time 4 -u USERNAME https://SERVER.pulsedmedia.com/user-USERNAME/wsrelay/ \ -H 'Connection: Upgrade' -H 'Upgrade: websocket' -H 'Sec-WebSocket-Version: 13' \ -H "Sec-WebSocket-Key: $(head -c 16 /dev/urandom | base64)" \ -H 'Sec-WebSocket-Protocol: wsrelay' -H 'Origin: https://SERVER.pulsedmedia.com' | head -1
The same check straight to the port on the server, which is what another account can do: run it without -u USERNAME and with http://127.0.0.1:PORT/ as the address.
| Check | Result in the test |
|---|---|
docker ps |
wsrelay running
|
ss -tln | grep ':PORT ' |
Must show 127.0.0.1:PORT, never 0.0.0.0:PORT or *:PORT, which would put it on the internet
|
Web address, panel login, right origin, wsrelay subprotocol |
HTTP/1.1 101 Switching Protocols
|
| Web address, no panel login | 401 |
| Web address, panel login, wrong origin | 403 |
Straight to 127.0.0.1:PORT, right origin and subprotocol |
101 |
Straight to 127.0.0.1:PORT, wrong or missing origin, or no subprotocol |
403 |
Plain GET / straight to the port |
404; wsrelay only answers WebSocket handshakes |
Uninstall
Everything the install created belongs to your account, so plain commands remove it:
docker rm -f wsrelay rm ~/.lighttpd/custom.d/wsrelay.conf touch ~/.lighttpd/custom rm -r ~/docker/wsrelay docker image rm nicholashill/wsrelay@sha256:dbbe78ff89e20f9bcd41fe020c74cd9a981ab5c9d86b8c88a33236ae7cf12873
The touch line makes the web server restart without the removed fragment: deleting a file alone does not trigger the restart (details). Removing the container also deletes any realm data clients saved, because the install keeps no data volume.
Troubleshooting
- The handshake gets 403. The page's origin is not in
ACCEPTED_ORIGINS(check for a port or trailing slash in the setting), or the client did not ask for any WebSocket subprotocol.docker logs wsrelaynames the rejected origin. - The handshake gets 401. The panel login is missing. That is the fragment working as intended.
- A plain request gets 404. Normal: wsrelay answers only WebSocket handshakes. In the test a plain request straight to the port got 404; through the web address it was not tried.
- 502 Bad Gateway. The container is not running or listens on another port; check
docker psand the port in the fragment. - The relay still answers after you removed the fragment. Run
touch ~/.lighttpd/custom; the restart follows within a minute or two.
A relay that other people use, such as a public chat or a game for your friends, does not fit a shared seedbox. Behind the panel login every player would need your panel password, which opens your whole panel. Without the panel login (auth.require = () in the fragment), the relay would be reachable from the internet at the same web address with only the origin check left, which any program can pass. For a public relay, run wsrelay on a VPS of your own; that setup was not tested.
For your own pages and devices no separate server is needed: rootless Docker starts automatically for every account on a Pulsed Media seedbox, next to your web folder.
See also
- Lighttpd Custom Configuration — proxy fragments,
map-urlpathand the panel login - Rootless DOCKER — Docker starts automatically for every account on a PMSS seedbox
- Self-Hosted Apps on PMSS — other applications for rootless Docker, with their login facts
- Web Hosting on PMSS — serving the web pages that would use the relay
- Seedbox — Pulsed Media seedbox plans, where rootless Docker is included