Provision the droplet via the DigitalOcean API and run deliverable #4's setup
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:
- 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: regionsyd1, 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. - 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-lorrepo to/opt/lor/game-server). - Run deliverable #4 §8’s full verification checklist, ending in a successful WebSocket connection through Caddy to a world instance.
- 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:
- SSH keypair (
lor_runner_key/lor_runner_key.pub) — present, correct (ed25519,lor-server-runnercomment). Private key permissions were644; corrected to600in place. - DO API token (
do_api_token) — not present, and not found anywhere else in the workspace or environment.
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
- Droplet:
lor-game-server-01, ID589446588, IP209.38.29.22, regionsyd1, sizes-1vcpu-2gb(2 GB/1 vCPU/50 GB, ~US$12/mo), imageubuntu-24-04-x64, taglor-game-server. - Access:
ssh lor@209.38.29.22with the runner key;rootlogin and all password auth are disabled (PermitRootLogin no,PasswordAuthentication no, confirmed by a refused root-login attempt).lorhas passwordless sudo (/etc/sudoers.d/90-lor-nopasswd) — needed since this session drives the box entirely over non-interactive SSH with no way to type a sudo password; flagging as an operational choice, not literally spelled out in deliverable #4. - Firewall:
ufwactive, only OpenSSH/80/443 allowed (confirmed viaufw status).fail2banandunattended-upgradesboth active and enabled. - Godot: 4.7.1 headless (
godot4 --headless --version→4.7.1.stable.official.a13da4feb), x86_64 binary at/usr/local/bin/godot4. - Postgres: role + database
lor_game, password generated withsecrets.token_urlsafe(24)and written only to/etc/lor/game.env(600, ownedlor:lor) — never appeared in any command line, log, or repo file. Connection re-verified (\conninfofrom thelor_gamerole over TCP to127.0.0.1). - Caddy: 2.11.4,
/etc/caddy/Caddyfileusing §5.2b (bare IP) — no domain exists for this project yet, so this is plainhttp://209.38.29.22, not TLS.curl -I http://209.38.29.22→200. - systemd:
lor-director.serviceandlor-world@1.servicebothenabledandactive (running). - Repo:
land-of-lorcloned to/opt/lor/game-server,origincorrectly pointing athttps://github.com/krisrandall/land-of-lor.git.
Deviations from deliverable #4 as written (all judgment calls made in-session, none needed to block on Kris)
- SSH key delivery. The API token is deliberately scoped
ssh_key:readonly (per Q001), so the runner key couldn’t be registered as a DO SSH-key object and attached via the droplet API’sssh_keysfield (which only accepts pre-registered IDs/fingerprints). Used cloud-inituser_datato inject the key directly instead — a standard DO-documented pattern for this exact situation. - First droplet attempt (ID
589445718) was destroyed and recreated. DigitalOcean’s own droplet-init flow expires the root password unless a droplet APIssh_keysentry 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 addingchage -d $(date +%Y-%m-%d) root/chage -M -1 rootto cloud-initruncmdand 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. - 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. land-of-lorhas 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-serverexactly as specified, then added a minimal, explicitly-labeled stub (project.godot,main.gd,main.tscn— aWebSocketMultiplayerPeerlistener, 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 viagit pull, that pull will need manual reconciliation rather than a clean fast-forward.- Repo clone used a git bundle, not droplet-side GitHub auth.
land-of-loris 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 repointedoriginat the real URL. Follow-up: the §7 “ship a change” workflow (git pull --ff-onlyon the box) won’t work until Kris sets up real git credentials there — a deploy key scoped read-only toland-of-loris the natural choice, not yet done. - Droplet not moved into a
lor-game-serverDO project. The token hasproject:updatebut notproject:read/list, so there was no way to discover the project’s ID via API — confirmed even/v2/projects/default403s. The droplet sits in DO’s Default project. Recorded as a gap inserver-instance/Q001’s answered note too.
§8 verification checklist — all passed
ssh lor@209.38.29.22succeeds;ssh root@209.38.29.22refused. ✅ufw status: only OpenSSH/80/443ALLOW. ✅godot4 --headless --version→4.7.1.stable.official.a13da4feb. ✅postgresqlactive. ✅psql -h 127.0.0.1 -U lor_game -d lor_gameconnects,\conninfoconfirms. ✅caddyactive;curl -I http://209.38.29.22→200. ✅lor-directorandlor-world@1bothactive (running). ✅- End-to-end WebSocket check: opened
ws://209.38.29.22/world/1/from outside the droplet using Python’swebsocketslibrary — connection reachedOPENstate through Caddy’s reverse proxy to the Godot world instance. ✅
Follow-ups for Kris (not blockers, just real gaps)
- Move the droplet into a
lor-game-serverDO project by hand (console), or grant the tokenproject:readfor a future session to do it via API. - Set up real git access on the droplet for
/opt/lor/game-server(deploy key recommended) before relying on thegit pull --ff-onlydeploy workflow. - No domain is pointed at
209.38.29.22yet — when one exists, switch the Caddyfile to §5.2a for automatic TLS. - Confirm the ~US$12/mo billing alert mentioned in Q001 is actually set (this session couldn’t verify it — no
billingscope, by design). - The stub Godot project at
/opt/lor/game-serverneeds to be swapped for real server code, and the director’srun.shplaceholder needs the real runtime onceserver-architecture’s open question lands.
View source in repository · also available as raw markdown.