Setting up otomo for your game¶
Otomo is game-agnostic: you run the framework and supply the game itself. This guide takes you from a fresh Linux host to a running stack with a public HTTPS edge, and says which parts are yours to own.
For the general architecture and conventions, read Onboarding first.
Prerequisites¶
- A Linux host with Docker and the Docker Compose plugin,
opensslandgit. Nothing else is required to bring the stack up: no Go, no Node, no Postgres client. - A DNS name pointing at the host, for TLS.
- Ports 80 and 443 reachable from the internet. If you cannot open them, you need a domain whose DNS provider has an API and a DNS-01 certificate instead (see "Enabling the public edge and TLS").
- Enough capacity. The stack idles in the low hundreds of megabytes, but
go buildandgo test -raceare CPU-heavy. Do not run a release during a busy match.
First deployment¶
generate-secrets.sh writes deploy/.env, renders deploy/valkey/valkey.conf and creates
the deploy/secrets/ directories. It generates passwords as hex so they need no escaping in a
DSN, in SQL or in a sed substitution.
up.sh is docker compose up plus the two things YAML cannot express: the passwords, and
signing keys that a service binary must generate against a migrated database. In order it
builds the images, waits for Postgres, Valkey and the registry to become healthy, runs every
*-migrate one-shot, runs auth genkey, then admin_auth genkey -totp, admin_auth genkey
and admin_auth bootstrap-root, starts the remaining services, and polls readiness on the
internal :9090.
The chown steps¶
generate-secrets.sh creates deploy/secrets/admin_auth/ as 0700, owned by the admin-auth
container's uid (65532), because that container writes its two key files there. Run as root,
the script chowns the directory itself; run as another user, it prints the exact line to run:
The same non-root constraint gives deploy/secrets/ mode 1777 (the sticky bit means only
the owner can replace the file, and the file itself is 0600). As a result, reading either
the auth signing key or the root password from the host needs sudo:
If root_password is missing, up.sh prints its path and stops before starting admin-auth,
rather than leaving a container crash-looping. Re-run generate-secrets.sh.
Provisioning an existing host¶
deploy/postgres/init/01-provision.sh runs only on the first start with an empty data
directory. It is not re-run when you pull a new image or run up.sh, so a new database or
role does not reach an already-provisioned host on its own. After changing that script, run:
provision-upgrade.sh runs the same file inside the postgres container. It is idempotent,
but it resets each service role's password to the value in deploy/.env, so pair a
password change with a restart of the services that use it. On a host provisioned before the
staff identity service existed, run generate-secrets.sh first so ADMIN_AUTH_RW_PASSWORD is
added to .env; provision-upgrade.sh refuses to run while a required password is empty.
Enabling the public edge and TLS¶
The edge is nginx in services/edge. It terminates TLS and routes the player API, /docs/
and /. Certificates come from the host's certbot, never from the edge container itself.
The certificate quota is shared across every team using the parent domain, so rehearse before you issue:
# 1. Firewall intent (Docker publishes past ufw; keep in step with compose.yaml).
sudo ufw allow 80/tcp comment 'otomo edge: ACME + redirect'
sudo ufw allow 443/tcp comment 'otomo edge: TLS'
# 2. certbot, the ACME webroot and the deploy hook.
sudo deploy/edge/certbot.sh install
# 3. In deploy/.env: OTOMO_PUBLIC_HOST=<host> and COMPOSE_PROFILES=edge, then:
deploy/scripts/up.sh
# 4. Rehearse. This checks the challenge path from the internet and spends no quota.
sudo OTOMO_PUBLIC_HOST=<host> deploy/edge/certbot.sh dry-run
# 5. Only after step 4 succeeds, and only ever once per host name:
sudo OTOMO_PUBLIC_HOST=<host> deploy/edge/certbot.sh issue --confirm-single-issuance
# 6. In deploy/.env set OTOMO_PUBLIC_BASE_URL=https://<host> and re-run:
deploy/scripts/up.sh
certbot.sh issue refuses to run when a certificate already exists, when there is no
successful dry-run from the last two hours, or when a certificate for the name was issued in
the last seven days. After issuance, certbot.timer renews automatically and the deploy hook
reloads the edge; nobody runs issue again.
If ports 80 and 443 cannot be opened, use a domain whose DNS provider has an API, an A
record pointing at the host, and a DNS-01 certificate, and serve HTTPS on an allowed port.
A certificate is for a name, not a port.
Full runbook: deploy/edge/README.md and the
Allocator and edge plan.
What is game-specific and what is framework¶
| Concern | Owner | Notes |
|---|---|---|
| Config namespaces and content | Your game | You define the namespaces and their JSON schemas and author the JSON. Config validates, versions, channels and publishes it; Patch serves manifests and blobs to clients. See Config and Patch (minimal). |
| Session rules | Your game | Game rules (party size, name rules, lobby option values) are data in a server-audience Config namespace such as session.rules, enforced by Session's Go code. No server-side scripting. See the player plane plan. |
| Game servers | Your game | You build the headless server image. Each server registers with the Allocator, heartbeats every 5 seconds and reports when a match ends. See the Allocator and edge plan and the launch hand-off. |
| Game client | Your game | The client uses the Godot SDK and talks only to the public gateway. See the Godot SDK guide. |
| Game site content | Your game | The site lives in your own repository, never in otomo's. See Wikis and sites and the wiki service. |
| Auth, Gateway, Patch, Session, Allocator, Dashboard, admin UI, edge, wiki | Framework | Run it, configure it, do not fork the game into it. |
The dependency is one-way: _site.yml and the HTTP contracts are the whole interface.
Connecting a game client¶
The client knows one base URL: the public gateway. The flow is:
- Patch first, over public routes, to get content and client config.
- Log in with Auth's device login, receiving a short-lived access token and a rotating refresh token.
- Use Session for profile, presence, friends, party/lobby and events.
- Launch: the leader's lobby launch makes Session call the Allocator, which reserves a game server and issues a single-use join ticket. Session delivers the address, port and each member's own ticket as an event. The client presents the ticket on connect through the Gameplay Proxy.
The Godot SDK guide is the client side of every contract; the launch hand-off is the server side of the launch path.
Operations¶
Health¶
Every service answers /healthz, /readyz and /metrics on its internal :9090 listener.
The distroless images have no shell, so probe them from outside with a throwaway container
rather than with a Compose healthcheck. A gateway /readyz returns 503 until both token
domains' JWKS are fetched.
Admin plane¶
gateway_dev binds only the host's loopback. Open an SSH local forward, then use the UI:
The break-glass root account is root@otomo.internal; read its password with
sudo cat deploy/secrets/admin_auth/root_password and create one personal admin account per
person.
Dashboard¶
Metrics and logs are served by the Dashboard service (Dashboard). Prometheus, Loki and Alloy run in the same Compose project and none publishes a port.
Releases, rollback and secrets¶
Deploy one service by image tag and roll back by re-pointing the tag, as described in
Onboarding and deploy/README.md.
deploy/scripts/release.sh gateway # build, push, deploy (auth, gateway, gateway_dev)
# rollback: edit OTOMO_GATEWAY_TAG in deploy/.env to the previous SHA, then
cd deploy && docker compose --env-file .env -f compose.yaml up -d gateway
release.sh covers auth, gateway and gateway_dev today. For other services, follow the
same steps by hand: export OTOMO_<SERVICE>_TAG=<sha>, then docker compose build,
push, record the tag in deploy/.env, and up -d <service>.
Secrets live only in deploy/.env and deploy/secrets/, are generated on the host, and are
never committed.