Ubuntu Droplet Setup
Ubuntu Droplet Setup
Migration in progress.
api.legendary-arena.comand its PostgreSQL are moving off Render onto a self-hosted DigitalOcean Ubuntu droplet fronted by Cloudflare, perdocs/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
- Provision — Provision Droplet
, owns
infra/cloud-init.yaml. First-boot user-data: the non-rootoperatoruser + authorized key, base apt packages, timezone, and the/etc/laanchor for the deploy.env. One-shot — fill the key placeholder before boot. - SSH hardening — SSH 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. - Firewall — UFW Firewall
, owns
infra/scripts/06-ufw.ps1. Default-deny inbound; allow SSH + 80/443 (443 later narrowed to Cloudflare IPs). - Fail2ban + auto-patching — Fail2ban 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
- Node runtime — Node Runtime
, owns
infra/scripts/10-node.ps1. fnm at/opt/fnmpinning Node 24.18.0 and corepack/pnpm 10.32.1 to match.node-version. - PostgreSQL — PostgreSQL Setup
, owns
infra/scripts/30-postgres.ps1. Self-hosted PostgreSQL 18 on localhost, scram-sha-256, 8 GB-box tuning; creates thelarole and database. - Nginx reverse proxy — Nginx Reverse Proxy
, owns
infra/nginx/api.conf+infra/scripts/20-nginx.ps1. Proxiesapi.to127.0.0.1:3000with the WebSocket-upgrade headers Socket.IO needs; the plain-HTTP stage before TLS. - Cloudflare TLS — Cloudflare 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)
- Deploy the server — Deploy 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
- Migration Checklist — the status view; this guide’s steps are its Phase 1–3 checklist rows.
- Render-to-DigitalOcean Migration — the concept hub holding the locked architecture (droplet size, region, co-located DB, TLS posture) this guide executes.
- Secrets and Env
— the
/etc/la/.env(mode 600) contract that Provision Droplet anchors and Deploy Server consumes. - Systemd Service
— the
la-serverunit installed at step 9 that keeps the app running across reboots. docs/PLAN.md— the phase sequence and exit criteria this ordering follows.
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-hardenedsnapshot 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
docs/PLAN.md— §6 phase sequence, §6.1 exit criteria, locked droplet decisions.infra/cloud-init.yaml— first-boot bootstrap that starts the sequence.- Migration Checklist — live progress against this guide.
- Render-to-DigitalOcean Migration — the concept hub and locked architecture.
- Step pages: Provision Droplet , SSH Hardening , UFW Firewall , Fail2ban and Unattended Upgrades , Node Runtime , PostgreSQL Setup , Nginx Reverse Proxy , Cloudflare TLS , Deploy Server .