---
id: server-instance/001
title: Provision the droplet via the DigitalOcean API and run deliverable #4's setup
epic: server-instance
state: done
priority: 1
blocked_by: []
estimate: M
created: 2026-08-05
updated: 2026-08-02
claimed_by: null
claimed_at: null
delivers: []
review_artifact: null
---

## 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:

- SSH keypair (`lor_runner_key` / `lor_runner_key.pub`) — present, correct (ed25519, `lor-server-runner` comment). Private key permissions were `644`; corrected to `600` in 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`, ID **`589446588`**, IP **`209.38.29.22`**, region `syd1`, size `s-1vcpu-2gb` (2 GB/1 vCPU/50 GB, ~US$12/mo), image `ubuntu-24-04-x64`, tag `lor-game-server`.
- **Access:** `ssh lor@209.38.29.22` with the runner key; `root` login and all password auth are disabled (`PermitRootLogin no`, `PasswordAuthentication no`, confirmed by a refused root-login attempt). `lor` has 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:** `ufw` active, only OpenSSH/80/443 allowed (confirmed via `ufw status`). `fail2ban` and `unattended-upgrades` both 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 with `secrets.token_urlsafe(24)` and written only to `/etc/lor/game.env` (`600`, owned `lor:lor`) — never appeared in any command line, log, or repo file. Connection re-verified (`\conninfo` from the `lor_game` role over TCP to `127.0.0.1`).
- **Caddy:** 2.11.4, `/etc/caddy/Caddyfile` using **§5.2b (bare IP)** — no domain exists for this project yet, so this is plain `http://209.38.29.22`, not TLS. `curl -I http://209.38.29.22` → `200`.
- **systemd:** `lor-director.service` and `lor-world@1.service` both `enabled` and `active (running)`.
- **Repo:** `land-of-lor` cloned to `/opt/lor/game-server`, `origin` correctly pointing at `https://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)

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 --version` → `4.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.22` → `200`. ✅
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)

- Move the droplet into a `lor-game-server` DO project by hand (console), or grant the token `project:read` for 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 the `git pull --ff-only` deploy workflow.
- No domain is pointed at `209.38.29.22` yet — 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 `billing` scope, by design).
- The stub Godot project at `/opt/lor/game-server` needs to be swapped for real server code, and the director's `run.sh` placeholder needs the real runtime once `server-architecture`'s open question lands.