Files
two-keys/DEPLOYMENT.md
joshuaandClaude Opus 5.5 d1a9c7d0f7 V1.0.1
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>
2026-10-13 17:15:47 +02:00

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.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:

    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

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.