Add deployment guide and pm2 process file

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-13 09:40:04 +02:00
co-authored by Claude Opus 5.5
parent 0de6617c82
commit 037b98f5e1
3 changed files with 213 additions and 0 deletions
+193
View File
@@ -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.
+3
View File
@@ -24,3 +24,6 @@ Co-op mystery games for couples. Two players, different clues, every lock needs
## Adding a new game later ## Adding a new game later
Copy a YAML in `content/games/`, change the `id` to match the filename, write your chapters, 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. 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.
+17
View File
@@ -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,
},
},
],
};