diff options
| author | skylar <me@casper> | 2026-11-01 05:43:24 -0800 |
|---|---|---|
| committer | skylar <me@casper> | 2026-11-01 05:43:24 -0800 |
| commit | e69ad01e700e80fa959dfee08cd556ca0d777ffd (patch) | |
| tree | 645fc9db2142e87a5ef302733a77ba2fec848d88 /deploy | |
Yesod: self-hosted WebXR rooms
Start a room in a world and hang out in VR from the headset's browser:
A-Frame + Networked-A-Frame over EasyRTC, with a coturn relay that can hide
everyone's IP address. Ships with the Campfire, Temple, Cabin, Balcony and
Test room worlds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'deploy')
| -rw-r--r-- | deploy/deploy.env.example | 6 | ||||
| -rwxr-xr-x | deploy/deploy.sh | 48 | ||||
| -rw-r--r-- | deploy/env.example | 27 | ||||
| -rw-r--r-- | deploy/nginx-yesod.conf | 41 | ||||
| -rw-r--r-- | deploy/turnserver.conf | 64 | ||||
| -rw-r--r-- | deploy/yesod.service | 37 |
6 files changed, 223 insertions, 0 deletions
diff --git a/deploy/deploy.env.example b/deploy/deploy.env.example new file mode 100644 index 0000000..a71cccd --- /dev/null +++ b/deploy/deploy.env.example @@ -0,0 +1,6 @@ +# Copy to deploy/deploy.env (not tracked) and fill in. Read by deploy/deploy.sh. +DEPLOY_TARGET=root@example.org +#APP_DIR=/opt/yesod +#SERVICE=yesod +#PORT=8095 +#NPM=npm diff --git a/deploy/deploy.sh b/deploy/deploy.sh new file mode 100755 index 0000000..03ece4a --- /dev/null +++ b/deploy/deploy.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# Push the app to a server, install its dependencies there, swap it in, and +# restart. First-time setup is described in README.md; this is for updates. +# +# deploy/deploy.sh [ssh-target] +# +# Settings come from the environment or from deploy/deploy.env (not tracked; +# copy deploy/deploy.env.example): +# DEPLOY_TARGET ssh target, e.g. root@example.org (or the first argument) +# APP_DIR where the app lives (default /opt/yesod) +# SERVICE its systemd unit (default yesod) +# PORT its port, for the health check (default 8095) +# NPM the npm command on the server (default npm) +set -euo pipefail + +cd "$(dirname "$0")/.." +if [ -f deploy/deploy.env ]; then + # shellcheck source=/dev/null + . deploy/deploy.env +fi + +TARGET="${1:-${DEPLOY_TARGET:-}}" +APP_DIR="${APP_DIR:-/opt/yesod}" +SERVICE="${SERVICE:-yesod}" +PORT="${PORT:-8095}" +NPM="${NPM:-npm}" + +if [ -z "$TARGET" ]; then + echo "Set DEPLOY_TARGET in deploy/deploy.env, or pass an ssh target: deploy/deploy.sh root@example.org" >&2 + exit 1 +fi + +tar --exclude=node_modules --exclude=.git --exclude=deno.lock \ + --exclude='.passphrase*' --exclude='*.local.md' --exclude=deploy --exclude=tools -czf - . \ + | ssh "$TARGET" "set -e + rm -rf $APP_DIR.new + mkdir -p $APP_DIR.new + tar -C $APP_DIR.new --no-same-owner -xzf - + chmod -R u=rwX,go=rX $APP_DIR.new + cd $APP_DIR.new + $NPM ci --omit=dev --ignore-scripts --no-audit --no-fund --loglevel=error + rm -rf $APP_DIR.old + if [ -d $APP_DIR ]; then mv $APP_DIR $APP_DIR.old; fi + mv $APP_DIR.new $APP_DIR + systemctl restart $SERVICE + sleep 1 + systemctl is-active $SERVICE + curl -fsS http://127.0.0.1:$PORT/healthz && echo" diff --git a/deploy/env.example b/deploy/env.example new file mode 100644 index 0000000..eec947f --- /dev/null +++ b/deploy/env.example @@ -0,0 +1,27 @@ +# The app's settings: copy to /etc/yesod/env (mode 640, root:yesod). Every +# line is optional except the two TURN lines, which turn on the relay that +# "Hide my IP address" needs. + +# Where the app listens. nginx proxies to it. +#HOST=127.0.0.1 +#PORT=8095 + +# The relay (coturn). TURN_SECRET must match static-auth-secret in +# /etc/turnserver.conf; make one with: openssl rand -hex 32 +TURN_SECRET= +TURN_URLS=turn:example.org:3478?transport=udp,turn:example.org:3478?transport=tcp +STUN_URLS=stun:example.org:3478 + +# What the lobby calls the site. +#SITE_NAME=Yesod + +# Where the worlds' sky is drawn from, in degrees (north and east positive). +# The default is Greenwich. +#SKY_LATITUDE=51.4769 +#SKY_LONGITUDE=-0.0005 + +# Limits (see README.md). Keep coturn's quotas in step if you raise them. +#MAX_PEOPLE=8 +#MAX_ROOMS=10 +#MAX_CONNECTIONS=60 +#MAX_CONNECTIONS_PER_IP=6 diff --git a/deploy/nginx-yesod.conf b/deploy/nginx-yesod.conf new file mode 100644 index 0000000..dc9404b --- /dev/null +++ b/deploy/nginx-yesod.conf @@ -0,0 +1,41 @@ +# Yesod at https://example.org/vr/ (any path works: the app only uses +# relative URLs). Include this from your site's TLS server block; it proxies to +# the Node app on PORT (8095 by default). +# +# Nothing about visitors is logged here: no access log (who loaded the page, +# when, from where), and only critical errors, which don't name visitors. +location = /vr { + access_log off; + error_log /dev/null crit; + return 301 /vr/; +} + +location /vr/ { + access_log off; + error_log /dev/null crit; + + proxy_pass http://127.0.0.1:8095/; + proxy_http_version 1.1; + + # socket.io upgrades to a WebSocket for signaling. + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_read_timeout 1h; + proxy_send_timeout 1h; + client_max_body_size 16k; + + # Compress scripts, JSON and 3D models (.glb, and .vrm which is served as + # octet-stream): the libraries shrink ~75%, avatars and hands 50-85%. + # Images are already compressed and left alone. + gzip on; + gzip_proxied any; + gzip_vary on; + gzip_comp_level 5; + gzip_min_length 1024; + gzip_types application/javascript text/css application/json model/gltf-binary application/octet-stream; +} diff --git a/deploy/turnserver.conf b/deploy/turnserver.conf new file mode 100644 index 0000000..efddc5e --- /dev/null +++ b/deploy/turnserver.conf @@ -0,0 +1,64 @@ +# coturn for Yesod: STUN plus a TURN relay for voice when two people can't +# reach each other directly, and for everyone who hides their IP address. +# Credentials are short-lived HMACs minted by the app (yesod.service) from the +# shared secret below. +# +# On install, set realm to your domain and replace STATIC_AUTH_SECRET with +# TURN_SECRET from /etc/yesod/env (see README.md). + +listening-port=3478 +realm=example.org +use-auth-secret +static-auth-secret=STATIC_AUTH_SECRET +fingerprint + +# Relay ports (open these UDP ports in the firewall). With "Hide my IP +# address" on, everyone relays: each person holds one allocation, and one +# port, per other person in the room. The app's cap of 60 people in rooms of +# up to 8 needs at most 60 x 7 = 420. If you change MAX_PEOPLE or +# MAX_CONNECTIONS, keep these in step. +min-port=49160 +max-port=49999 + +# Each person gets their own TURN username from the app; 7 others plus a +# little slack for reconnects. The total matches the app's limits above. +user-quota=10 +total-quota=450 +stale-nonce=600 + +# No TLS listener (nginx owns 443), no admin CLI, no TCP relays. +no-tls +no-dtls +no-cli +no-tcp-relay +no-multicast-peers + +# Never relay into private, loopback or reserved address space. +denied-peer-ip=0.0.0.0-0.255.255.255 +denied-peer-ip=10.0.0.0-10.255.255.255 +denied-peer-ip=100.64.0.0-100.127.255.255 +denied-peer-ip=127.0.0.0-127.255.255.255 +denied-peer-ip=169.254.0.0-169.254.255.255 +denied-peer-ip=172.16.0.0-172.31.255.255 +denied-peer-ip=192.0.0.0-192.0.0.255 +denied-peer-ip=192.0.2.0-192.0.2.255 +denied-peer-ip=192.88.99.0-192.88.99.255 +denied-peer-ip=192.168.0.0-192.168.255.255 +denied-peer-ip=198.18.0.0-198.19.255.255 +denied-peer-ip=198.51.100.0-198.51.100.255 +denied-peer-ip=203.0.113.0-203.0.113.255 +denied-peer-ip=224.0.0.0-255.255.255.255 +denied-peer-ip=::1 +denied-peer-ip=64:ff9b::-64:ff9b::ffff:ffff +denied-peer-ip=::ffff:0.0.0.0-::ffff:255.255.255.255 +denied-peer-ip=100::-100::ffff:ffff:ffff:ffff +denied-peer-ip=2001::-2001:1ff:ffff:ffff:ffff:ffff:ffff:ffff +denied-peer-ip=2002::-2002:ffff:ffff:ffff:ffff:ffff:ffff:ffff +denied-peer-ip=fc00::-fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff +denied-peer-ip=fe80::-febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff + +# No logs: coturn otherwise records every session with both ends' addresses. +# (To debug a relay problem, swap these for "syslog" and "verbose" for a while.) +no-stdout-log +log-file=/dev/null +simple-log diff --git a/deploy/yesod.service b/deploy/yesod.service new file mode 100644 index 0000000..6065a48 --- /dev/null +++ b/deploy/yesod.service @@ -0,0 +1,37 @@ +[Unit] +Description=Yesod WebXR rooms (Networked-A-Frame + EasyRTC signaling) +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=yesod +Group=yesod +WorkingDirectory=/opt/yesod +EnvironmentFile=/etc/yesod/env +ExecStart=/usr/bin/node server.js +Restart=on-failure +RestartSec=3 + +# Hardening: the app only needs to read its own files and listen on localhost. +NoNewPrivileges=yes +ProtectSystem=strict +ProtectHome=yes +PrivateTmp=yes +PrivateDevices=yes +ProtectKernelTunables=yes +ProtectKernelModules=yes +ProtectKernelLogs=yes +ProtectControlGroups=yes +ProtectClock=yes +ProtectHostname=yes +RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX +RestrictNamespaces=yes +RestrictRealtime=yes +LockPersonality=yes +CapabilityBoundingSet= +SystemCallArchitectures=native +MemoryMax=300M + +[Install] +WantedBy=multi-user.target |