diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..dfdc0c3 --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,193 @@ +# Deploying Two Keys + +How to run the site under pm2 on an app server and publish it at +`https://two-keys.crosscode.co.za` through frp. + +``` +browser ──https──▶ central server (frps, TLS) ──frp tunnel──▶ app server (frpc) ──▶ 127.0.0.1:3010 (pm2: two-keys) +``` + +## One port, one domain + +Two Keys is a **single Node process**. The same port serves the web pages, the `/api` routes and +the websocket (`/socket.io`), and the browser connects back to whatever address it loaded the page +from. So only `two-keys.crosscode.co.za` is needed. + +`two-keys-server.crosscode.co.za` is **not required**. There is no separate backend port to expose, +and the client has no setting for a different server address, so pointing a second domain at it +would change nothing. + +The examples below use these values. Change them to suit: + +| Value | Used here | +|-------|-----------| +| Install folder | `/var/www/two-keys` | +| Local port | `3010` (set in `ecosystem.config.cjs`) | +| pm2 app name | `two-keys` | +| Public domain | `two-keys.crosscode.co.za` | + +## 1. Prerequisites (app server) + +- Node.js 20 or newer (`node -v`) +- git +- pm2: `sudo npm install -g pm2` +- frpc, already connected to your central frps + +## 2. Create the folder and give your user ownership + +Run these as your normal login user, not as root. `$USER` is the user you are logged in as. + +```bash +sudo mkdir -p /var/www/two-keys +sudo chown -R "$USER":"$USER" /var/www/two-keys +``` + +Everything after this runs **without** `sudo`. If you ever run `git`, `npm` or `pm2` in this folder +with `sudo`, files end up owned by root and later builds fail with `EACCES`. Fix it by running the +`chown` line again. + +The app writes its room snapshots to `server/data/rooms.json`, so the pm2 user must own the folder. + +## 3. Get the code and build it + +```bash +git clone https://c-git.crosscode.co.za/joshua/two-keys.git /var/www/two-keys +cd /var/www/two-keys +git checkout v1.0.0 # or stay on main for the latest + +npm ci # do NOT add --omit=dev: the build needs the dev dependencies +npm run validate-content # must print "All 8 games are valid." +npm run build +``` + +## 4. Start it with pm2 + +The port is set in `ecosystem.config.cjs` (`PORT: 3010`). Edit that file first if 3010 is taken. + +```bash +cd /var/www/two-keys +pm2 start ecosystem.config.cjs +pm2 save +``` + +Make pm2 come back after a reboot (once per server): + +```bash +pm2 startup +# pm2 prints one "sudo env PATH=... pm2 startup ..." line. Copy and run that line, then: +pm2 save +``` + +Check it locally: + +```bash +pm2 status two-keys +curl http://127.0.0.1:3010/api/health +pm2 logs two-keys --lines 20 # expect: "Two Keys server on http://localhost:3010 (8 games, ...)" +``` + +## 5. Publish it through frp + +### DNS + +Point `two-keys.crosscode.co.za` at the **central server** (the one running frps), the same way +your other frp-hosted sites are set up. + +### frpc on the app server + +Add one HTTP proxy to the frpc config, next to your existing ones. + +`frpc.toml` (frp 0.52 and newer): + +```toml +[[proxies]] +name = "two-keys" +type = "http" +localIP = "127.0.0.1" +localPort = 3010 +customDomains = ["two-keys.crosscode.co.za"] +``` + +`frpc.ini` (older frp): + +```ini +[two-keys] +type = http +local_ip = 127.0.0.1 +local_port = 3010 +custom_domains = two-keys.crosscode.co.za +``` + +Then restart frpc, for example: + +```bash +sudo systemctl restart frpc +``` + +### Central server + +Nothing Two Keys-specific is needed in frps itself: it routes by host name on its `vhostHTTPPort`, +and websockets pass through an frp `http` proxy without extra settings. + +What does need attention is whatever terminates HTTPS in front of frps: + +- **Certificate:** add `two-keys.crosscode.co.za` to it, the same way as your other sites. +- **Websocket upgrade:** if nginx (or similar) sits in front of frps, its server block for this + domain must forward upgrades, or the game falls back to slow polling: + + ```nginx + server { + server_name two-keys.crosscode.co.za; + # listen 443 ssl; plus your certificate lines + + location / { + proxy_pass http://127.0.0.1:8080; # your frps vhostHTTPPort + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 3600s; # keep long games connected + } + } + ``` + + The `Host` header must be passed through unchanged: frps uses it to pick the tunnel. + +## 6. Check it from outside + +```bash +curl https://two-keys.crosscode.co.za/api/health +``` + +Then open `https://two-keys.crosscode.co.za` on two phones, start a game on one, and join with the +room code on the other. If both lobbies update when a role is picked, the websocket is working +end to end. + +## Updating to a new version + +```bash +cd /var/www/two-keys +git pull # or: git fetch --tags && git checkout v1.1.0 +npm ci +npm run validate-content +npm run build +pm2 restart two-keys +``` + +Games in progress survive the restart: rooms are saved to `server/data/rooms.json` on shutdown and +restored on start. Players reconnect by themselves within a few seconds. + +## Troubleshooting + +| Symptom | Likely cause | +|---------|--------------| +| `EACCES` during `npm ci`, build, or at start-up | Folder not owned by your user. Re-run the `chown` in step 2. | +| `EADDRINUSE` in `pm2 logs` | Port 3010 is taken. Change `PORT` in `ecosystem.config.cjs` and `localPort` in frpc, then `pm2 restart two-keys --update-env` and restart frpc. | +| Local `curl` works, domain gives frp's "page not found" | `customDomains` doesn't match the host name exactly, frpc wasn't restarted, or the front proxy rewrites `Host`. | +| Site loads but "Can't reach the server" toasts, or games feel laggy | Websocket upgrade headers missing on the central server's HTTPS proxy. | +| Site loads but no games listed | `npm run build` or `validate-content` failed. Check `pm2 logs two-keys`. | + +Do not run more than one instance (no pm2 cluster mode, no second copy behind a load balancer): +room state is held in memory by the one process. diff --git a/README.md b/README.md index 39d1865..7c3a812 100644 --- a/README.md +++ b/README.md @@ -24,3 +24,6 @@ Co-op mystery games for couples. Two players, different clues, every lock needs ## Adding a new game later Copy a YAML in `content/games/`, change the `id` to match the filename, write your chapters, and run `npm run validate-content`. No code changes needed for new story games. + +## Deploying +See [DEPLOYMENT.md](DEPLOYMENT.md) for running it under pm2 and publishing it through frp. diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs new file mode 100644 index 0000000..de961a8 --- /dev/null +++ b/ecosystem.config.cjs @@ -0,0 +1,17 @@ +// pm2 process file. See DEPLOYMENT.md. +module.exports = { + apps: [ + { + name: 'two-keys', + script: 'server/dist/index.js', + cwd: __dirname, + // One instance only: rooms live in this process's memory. + instances: 1, + exec_mode: 'fork', + env: { + NODE_ENV: 'production', + PORT: 3010, + }, + }, + ], +};