summaryrefslogtreecommitdiff
path: root/deploy
diff options
context:
space:
mode:
authorskylar <me@casper>2026-11-01 05:43:24 -0800
committerskylar <me@casper>2026-11-01 05:43:24 -0800
commite69ad01e700e80fa959dfee08cd556ca0d777ffd (patch)
tree645fc9db2142e87a5ef302733a77ba2fec848d88 /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.example6
-rwxr-xr-xdeploy/deploy.sh48
-rw-r--r--deploy/env.example27
-rw-r--r--deploy/nginx-yesod.conf41
-rw-r--r--deploy/turnserver.conf64
-rw-r--r--deploy/yesod.service37
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