← Server instance · all tasks

Provision the droplet via the DigitalOcean API and run deliverable #4's setup

done priority 1 estimate M

server-instance/001 · created 2026-08-05 · updated 2026-08-02

What to do

server-architecture has landed its runtime decisions and deliverables/004-server-setup-guide.md already exists as an exact, step-by-step runbook. This task is executing that runbook via the DigitalOcean API instead of by hand:

  1. Using the credentials Kris supplies (see server-instance/Q001 — do not proceed without them, and do not use anyone’s personal DO account/API token or personal SSH key for this), call the DigitalOcean API to provision a droplet matching deliverable #4 §1: region syd1, Ubuntu 24.04 LTS (ubuntu-24-04-x64), the confirmed size (2 GB/1 vCPU regular unless Q001’s answer says otherwise), with the dedicated runner SSH keypair attached at creation.
  2. Run deliverable #4’s setup against the new droplet end-to-end: §2 (hardening), §3 (Godot headless — verify the x86_64 binary, per the guide’s own recommendation), §4 (Postgres + role/database), §5 (Caddy — pick 5.2a or 5.2b depending on whether a domain is available yet), §6 (systemd units for director + a world instance), §7 (clone the land-of-lor repo to /opt/lor/game-server).
  3. Run deliverable #4 §8’s full verification checklist, ending in a successful WebSocket connection through Caddy to a world instance.
  4. Record what was actually provisioned — droplet ID, IP, region/size actually used, any point where this deviated from deliverable #4 as written — as a dated note appended to this task file. “It worked” alone is not sufficient; the next session needs to be able to find the box.

Definition of done

A running droplet, reachable exactly as deliverable #4’s verification checklist describes, provisioned via the DO API rather than by hand, with its real identifying details (droplet ID/IP, actual final config) recorded back in this task file.

Where the work lands

The droplet itself and the land-of-lor repo clone on it (/opt/lor/game-server); the record of what was provisioned lands back in this workspace repo, in this task file.

Docs to read first

deliverables/004-server-setup-guide.md in full — it is the spec this task executes, not background reading. server-instance/questions/001-droplet-provisioning-credentials.md for exactly what must exist before this task can start at all.

Blocked by

server-instance/Q001 — real DO API credentials, a dedicated runner SSH keypair, and confirmation of region/size are all prerequisites an agent cannot supply for itself. This task stays needs-input until Kris answers that question.

2026-08-02 — attempted pickup, still blocked

A session was pointed at this task on the premise that Q001 was fully answered. Checked /workspace/lor/.secrets/ before creating anything billable, per this task’s own instructions not to proceed without real credentials:

No droplet, DO project, tag, or API call was made — nothing billable exists as a result of this session. Full detail in server-instance/Q001’s “Progress check, 2026-08-02” note; state stays needs-input pending the token.

2026-08-02 — provisioned and verified end-to-end

Kris placed the DO API token; re-verified it (format, permissions, and a live region:read call) before creating anything. Full run against deliverables/004-server-setup-guide.md, executed via the DO API and SSH (no console clicks). Done.

What’s actually running

Deviations from deliverable #4 as written (all judgment calls made in-session, none needed to block on Kris)

  1. SSH key delivery. The API token is deliberately scoped ssh_key:read only (per Q001), so the runner key couldn’t be registered as a DO SSH-key object and attached via the droplet API’s ssh_keys field (which only accepts pre-registered IDs/fingerprints). Used cloud-init user_data to inject the key directly instead — a standard DO-documented pattern for this exact situation.
  2. First droplet attempt (ID 589445718) was destroyed and recreated. DigitalOcean’s own droplet-init flow expires the root password unless a droplet API ssh_keys entry was used at creation; since that wasn’t available (see #1), the first droplet locked out with “password expired, no TTY to change it” even though pubkey auth succeeded. Fixed by adding chage -d $(date +%Y-%m-%d) root / chage -M -1 root to cloud-init runcmd and recreating. The first droplet existed for under 2 minutes before deletion — DO bills per-second, so cost impact is negligible, but if Kris sees two droplet-hour line items on the invoice, that’s why.
  3. No domain yet, so §5.2b (bare IP, plain http://) was used rather than a domain-backed TLS config. Chosen over the self-signed-TLS alternative the guide also offers, since this is explicitly a dev-stage box for a small number of trusted testers per the epic’s own non-goals.
  4. land-of-lor has no server-side Godot project yet (still docs/ideation stage — confirmed by checking the repo directly, contrary to this task’s framing that assumed a runnable server existed). Cloned the real repo to /opt/lor/game-server exactly as specified, then added a minimal, explicitly-labeled stub (project.godot, main.gd, main.tscn — a WebSocketMultiplayerPeer listener, nothing more) directly on the droplet, not committed to git (excluded via .git/info/exclude, not the repo’s tracked .gitignore). This is what made §6-§8 genuinely verifiable rather than just “the systemd unit exists.” Follow-up: delete these three files once real world-server code lands; if real files of the same name arrive via git pull, that pull will need manual reconciliation rather than a clean fast-forward.
  5. Repo clone used a git bundle, not droplet-side GitHub auth. land-of-lor is private and no GitHub credential was ever placed on the droplet (out of scope for this task, and not something to hand to a box without being asked). Bundled this container’s already-authenticated clone and transferred it via SFTP, then repointed origin at the real URL. Follow-up: the §7 “ship a change” workflow (git pull --ff-only on the box) won’t work until Kris sets up real git credentials there — a deploy key scoped read-only to land-of-lor is the natural choice, not yet done.
  6. Droplet not moved into a lor-game-server DO project. The token has project:update but not project:read/list, so there was no way to discover the project’s ID via API — confirmed even /v2/projects/default 403s. The droplet sits in DO’s Default project. Recorded as a gap in server-instance/Q001’s answered note too.

§8 verification checklist — all passed

  1. ssh lor@209.38.29.22 succeeds; ssh root@209.38.29.22 refused. ✅
  2. ufw status: only OpenSSH/80/443 ALLOW. ✅
  3. godot4 --headless --version4.7.1.stable.official.a13da4feb. ✅
  4. postgresql active. ✅
  5. psql -h 127.0.0.1 -U lor_game -d lor_game connects, \conninfo confirms. ✅
  6. caddy active; curl -I http://209.38.29.22200. ✅
  7. lor-director and lor-world@1 both active (running). ✅
  8. End-to-end WebSocket check: opened ws://209.38.29.22/world/1/ from outside the droplet using Python’s websockets library — connection reached OPEN state through Caddy’s reverse proxy to the Godot world instance. ✅

Follow-ups for Kris (not blockers, just real gaps)

View source in repository · also available as raw markdown.