Connect over polling first so the game still works when a proxy drops the websocket upgrade. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6.2 KiB
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.
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
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.
cd /var/www/two-keys
pm2 start ecosystem.config.cjs
pm2 save
Make pm2 come back after a reboot (once per server):
pm2 startup
# pm2 prints one "sudo env PATH=... pm2 startup ..." line. Copy and run that line, then:
pm2 save
Check it locally:
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):
[[proxies]]
name = "two-keys"
type = "http"
localIP = "127.0.0.1"
localPort = 3010
customDomains = ["two-keys.crosscode.co.za"]
frpc.ini (older frp):
[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:
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.zato 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:
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
Hostheader must be passed through unchanged: frps uses it to pick the tunnel.
6. Check it from outside
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
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 games feel laggy, or /socket.io requests with transport=websocket fail in the browser's network tab |
Websocket upgrade headers missing on the central server's HTTPS proxy. The game still works over polling. |
| 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.