Legendary Arena Lab

Ubuntu Droplet Setup

wiki

Ubuntu Droplet Setup

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 is the umbrella guide for standing up the single DigitalOcean Ubuntu droplet that replaces Render’s web service + managed PostgreSQL: it lays out the ordered bring-up — provision, harden, install runtime and data, front with Nginx/TLS, deploy — and links each step to the wiki page and infra/ artifact that owns it. It duplicates none of those pages’ mechanics; it is the linear path through them. For where the program stands against this sequence, see the Migration Checklist .

Mechanics

The droplet is one box doing everything: Ubuntu 24.04 LTS, 4 vCPU / 8 GB to start (region sfo3), running apps/server under systemd behind Nginx with PostgreSQL 18 co-located on localhost (docs/PLAN.md ; Render-to-DigitalOcean Migration ). The rebuild invariant is “run the numbered infra/scripts/*.ps1 in order” (docs/PLAN.md ) — this guide walks that order.

The bring-up runs in the numbered script sequence, grouped by PLAN §6 phase:

Phase 1 — provision + harden

  1. ProvisionProvision Droplet , owns infra/cloud-init.yaml . First-boot user-data: the non-root operator user + authorized key, base apt packages, timezone, and the /etc/la anchor for the deploy .env. One-shot — fill the key placeholder before boot.
  2. SSH hardeningSSH Hardening , owns infra/scripts/05-user-and-ssh.ps1 . Key-only login; disables root login and password auth via a managed sshd drop-in, with a two-session anti-lockout sequence.
  3. FirewallUFW Firewall , owns infra/scripts/06-ufw.ps1 . Default-deny inbound; allow SSH + 80/443 (443 later narrowed to Cloudflare IPs).
  4. Fail2ban + auto-patchingFail2ban and Unattended Upgrades , owns infra/scripts/07-fail2ban-upgrades.ps1 . SSH brute-force bans + automatic security patching — the patching you now own instead of Render.

Phase 2 — runtime, web, data

  1. Node runtimeNode Runtime , owns infra/scripts/10-node.ps1 . fnm at /opt/fnm pinning Node 24.18.0 and corepack/pnpm 10.32.1 to match .node-version.
  2. PostgreSQLPostgreSQL Setup , owns infra/scripts/30-postgres.ps1 . Self-hosted PostgreSQL 18 on localhost, scram-sha-256, 8 GB-box tuning; creates the la role and database.
  3. Nginx reverse proxyNginx Reverse Proxy , owns infra/nginx/api.conf + infra/scripts/20-nginx.ps1 . Proxies api. to 127.0.0.1:3000 with the WebSocket-upgrade headers Socket.IO needs; the plain-HTTP stage before TLS.
  4. Cloudflare TLSCloudflare TLS , owns infra/scripts/21-cloudflare-tls.ps1 . Origin Certificate + Full (strict), 80→443 redirect, and UFW 443 restricted to live Cloudflare IP ranges.

Phase 3 — first deploy (dress rehearsal)

  1. Deploy the serverDeploy Server , owns infra/scripts/40-app-deploy.ps1 + infra/systemd/la-server.service + infra/.env.example . Clone/build via fnm, install + enable the systemd unit, restart, and health-check — run against a COPY of prod DB, never the live one.

Backups, monitoring, the deploy pipeline (Phase 4), cutover (Phase 5), and Render decommission (Phase 7) continue in the Migration Checklist ; the Phase 4+ scripts are planned and not yet in infra/ .

Interactions

Edge Cases

  • Order is load-bearing for a few steps, not all. Provision must precede everything; SSH hardening must land before you rely on key-only access; Nginx (plain HTTP) comes before Cloudflare TLS so the origin is reachable for cert validation. Node/Postgres can go in either order relative to each other but both precede the app deploy.
  • The anti-lockout window. SSH hardening and the UFW enable both risk locking you out; keep a second authenticated session open until you’ve verified access on a fresh shell (SSH Hardening § Edge Cases ).
  • cloud-init is one-shot. It fires once on first boot and does not re-run; an unreplaced key placeholder means no login, and drift is fixed with the numbered scripts, not by re-running cloud-init (Provision Droplet § Edge Cases ).
  • Snapshot after hardening. PLAN §5 calls for a post-bootstrap-hardened snapshot once Phase 1 + base services are in — take it before layering the app so a rebuild starts from a hardened baseline.
  • This is provisioning, not cutover. A fully built droplet is still not a Render replacement until the Phase 4 restore drill passes and DNS is flipped (Migration Checklist ).

Verify

After the sequence, the box should answer locally and pass its own health check:

# runtime + DB present
node --version            # expected: v24.18.0
systemctl status postgresql   # expected: active (exited)/running

# app service up and healthy
systemctl status la-server    # expected: active (running)
curl -s localhost:3000/health # expected: healthy response from apps/server

# firewall + cloud-init clean
sudo ufw status               # expected: Status: active, 22/80/443 allowed
cloud-init status --long      # expected: status: done

Per-step verification lives on each linked page’s own Verify section.

References