---
number: 5
title: lor-game-server-01 is live: server notes and how to deploy to it
date: 2026-08-02
summary: Deliverable
epic: server-instance
status: delivered
due-with: null
artifacts: ["epics/server-instance/tasks/001-provision-droplet-via-do-api.md","epics/server-instance/questions/001-droplet-provisioning-credentials.md","deliverables/004-server-setup-guide.md"]
cta: [object Object]
---

Deliverable #4 was the runbook: exact commands to go from "no droplet exists" to "a WebSocket connection reaches a running Godot process." `server-instance/001` executed it end-to-end, via the DigitalOcean API rather than by hand. This is that: what's actually running, right now, and how to work with it.

## The box

- **Droplet:** `lor-game-server-01`, ID `589446588`, region `syd1` (Sydney), size `s-1vcpu-2gb` (2 GB RAM / 1 vCPU / 50 GB SSD, ~US$12/month), image `ubuntu-24-04-x64`, tag `lor-game-server`.
- **Address:** `209.38.29.22` — currently served plain HTTP, no domain yet: **http://209.38.29.22/**.
- **Access:** `ssh lor@209.38.29.22` with the dedicated runner SSH key. Root login and password auth are both disabled — key-only, non-root, matching deliverable #4 §2.
- Provisioned via the DigitalOcean API against deliverable #4 as the spec, not through the console — see `server-instance/001`'s dated notes for the exact deviations (cloud-init key injection instead of the droplet API's `ssh_keys` field, a bundle-transferred git clone instead of droplet-side GitHub auth, and why).

## What's running, and how it's arranged

- **Hardening** (deliverable #4 §2): `ufw` active, allowing only OpenSSH/80/443; `fail2ban` and `unattended-upgrades` both enabled.
- **Postgres** (§4): a dedicated `lor_game` role and database — not the superuser role — reachable only from `127.0.0.1`. Its password lives in an environment file on the box, never in git or a command line.
- **Godot** (§3): 4.7.1 headless, x86_64, at `/usr/local/bin/godot4`.
- **systemd** (§6): `lor-director.service` and the templated `lor-world@.service` (currently one instance, `lor-world@1`), both `enabled` and `active`. The director's own runtime (Node vs. Go) is still an open question in `server-architecture` — its unit currently points at a placeholder `ExecStart`.
- **Caddy** (§5): reverse-proxying `/director/*` and `/world/N/*` to the director and world processes on localhost. Running **§5.2b, bare IP over plain HTTP** — no domain is pointed at the box yet, so there's no TLS. Fine for a small number of trusted testers at this stage; not something to point real players at.
- **The game code itself, and a temporary stand-in for it:** `/opt/lor/game-server` is a real clone of `land-of-lor`, `origin` pointing at `https://github.com/krisrandall/land-of-lor.git`. But `land-of-lor` is still docs/ideation stage — there's no server-side Godot project in it yet — so a minimal stub (`project.godot`, `main.gd`, `main.tscn`: just enough for a `WebSocketMultiplayerPeer` listener) was added directly on the droplet, **deliberately not committed to git** (excluded via `.git/info/exclude`). This is what made the systemd units and the end-to-end WebSocket check genuinely verifiable rather than just "the service exists." **It needs to be deleted once real server code lands** — and if real files of the same names arrive via `git pull` first, that pull will need manual reconciliation rather than a clean fast-forward.

## Deploying code to it

The workflow is deliverable #4 §7 — a manual pull-and-restart, deliberately not a CI pipeline at this project's current stage:

```bash
cd /opt/lor/game-server
git pull --ff-only
sudo systemctl restart lor-director.service
sudo systemctl restart 'lor-world@*.service'
```

If `git pull --ff-only` refuses, stop and reconcile by hand — never force, reset, or rebase automatically, same rule this workspace's own `deploy-watch.sh` follows.

**One thing stands between this and actually working today:** the clone on the box was seeded by transferring an already-authenticated bundle over SFTP, not by giving the droplet its own GitHub credentials — deliberately, since handing a box real GitHub access wasn't in scope for the provisioning task itself. Until a deploy key is added, `git pull` on the box has nothing to authenticate with. **Follow-up:** set up a deploy key scoped read-only to `land-of-lor` on the droplet before relying on this workflow for a real change.

## Day-to-day ops (deliverable #4 §9)

- **Is it up?** `systemctl status lor-director lor-world@1 postgresql caddy`, and `curl -I http://209.38.29.22` should return `200`.
- **Logs:** `journalctl -u lor-director -f`, `journalctl -u lor-world@1 -f`, `journalctl -u caddy -f`, `journalctl -u postgresql -f` — everything logs to journald, no separate log files to hunt for.
- **Before anything risky** (an OS upgrade, a schema migration, a systemd unit rewrite): take a DigitalOcean snapshot. Power off first (`sudo poweroff`, then **Droplet → Snapshots → Take Snapshot** in the DO console) — a live snapshot of a running Postgres data directory isn't guaranteed consistent.
- **Rough cost:** ~US$12/month for the droplet itself; snapshots bill separately, roughly $0.06/GB/month — a few dollars a month at most for occasional snapshots of a box this size.

## Open follow-ups

None of these block anything else in this epic or in `server-architecture` — recorded here so they're not lost, not because anything is stuck:

- **DO project assignment.** The token used to provision lacked `project:read`, so the droplet couldn't be looked up or moved into a `lor-game-server` DO project via the API — it currently sits in DO's Default project. A few seconds in the console, or grant the token `project:read` for a future session to do it via API.
- **Deploy key for `/opt/lor/game-server`**, as above — the thing that actually makes the §7 deploy workflow usable.
- **Domain and TLS.** Once a domain is pointed at `209.38.29.22`, switch the Caddyfile from §5.2b to §5.2a for automatic Let's Encrypt TLS.
- **Billing alert.** Recommended as part of provisioning (`server-instance/Q001` step 5) but not independently verifiable by the provisioning session — no `billing` scope, by design. Worth Kris confirming by hand in the DO console.
- **The stub Godot project** described above needs replacing with real server code once `land-of-lor` has any, and the director's runtime placeholder needs the real implementation once `server-architecture`'s one open question lands.