Legendary Arena Lab

Nginx Reverse Proxy

wiki

Nginx Reverse Proxy

Migration in progress. api.legendary-arena.com and its PostgreSQL are moving off Render onto a self-hosted DigitalOcean Ubuntu droplet fronted by Cloudflare, per docs/PLAN.md . Until decommission, Render stays warm as the rollback target.

Summary

This page stands up Nginx as the plain-HTTP front door to the game server, proxying api.legendary-arena.com to the app on the loopback interface. It is deliberately the port-80 stage; TLS and port 443 are layered on afterward by the Cloudflare TLS step, which rewrites the same site file.

Mechanics

The owning artifacts are infra/nginx/api.conf (the template) and infra/scripts/20-nginx.ps1 (the installer, root-required, Set-StrictMode/stop). The config proxies everything to the app and carries the WebSocket upgrade headers:

server {
    listen 80;
    server_name api.legendary-arena.com;
    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        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;
    }
    location = /health {
        proxy_pass http://127.0.0.1:3000/health;
    }
}

The installer runs in order:

  1. Install. Installs the nginx package.

  2. Render / template config. Renders api.conf into /etc/nginx/sites-available/la-api.conf, substituting server_name from DOMAIN (default api.legendary-arena.com) and the upstream port from UPSTREAM_PORT (default 3000):

    DOMAIN        = api.legendary-arena.com
    UPSTREAM_PORT = 3000        # proxy_pass http://127.0.0.1:3000
    
  3. Enable site. Symlinks the file into sites-enabled and removes the packaged default site.

  4. Validate. Runs nginx -t.

  5. Reload. Enables and reloads nginx.

The proxy_http_version 1.1 line plus Upgrade/Connection "upgrade" are what carry the Socket.IO WebSocket handshake. Per docs/PLAN.md , the app is boardgame.io over Socket.IO listening on 127.0.0.1:3000, and WebSockets must pass the proxy — without these headers, gameplay dies silently even though plain HTTP requests look healthy.

Interactions

  • Cloudflare TLS — the 21 step rewrites la-api.conf to add the 443 server block and Origin Certificate; this page must land first.
  • Deploy Server — provides the 127.0.0.1:3000 upstream this proxy forwards to; without it the proxy returns 502.
  • UFW Firewall — must permit inbound 80 (and later 443) or the proxy is unreachable from the edge.

Edge Cases

  • Missing upgrade headers → silent WebSocket failure. Drop proxy_http_version 1.1 or the Upgrade/Connection headers and Socket.IO gameplay fails quietly while ordinary HTTP still returns 200.
  • Order before TLS. This must precede the Cloudflare TLS step, which overwrites la-api.conf with the 443 server block. Running them out of order loses the TLS config.
  • Upstream must be listening. If the app is not up on 127.0.0.1:3000, every proxied request 502s — deploy the server before validating.
  • Default site shadows server_name. Removing the packaged default site is required; left in place it can capture requests ahead of la-api.conf.

Execute

# Default install (api.legendary-arena.com, upstream 3000)
sudo pwsh -File infra/scripts/20-nginx.ps1

# With explicit overrides (documented defaults shown)
sudo DOMAIN=api.legendary-arena.com UPSTREAM_PORT=3000 pwsh -File infra/scripts/20-nginx.ps1

Verify

sudo nginx -t
# Expected: syntax is ok / test is successful

sudo systemctl status nginx --no-pager
# Expected: active (running)

curl -I http://127.0.0.1/health
# Expected: HTTP/1.1 200 OK

References