First start
This is the one first-start sequence for Moongate. It applies whether you installed the release with the Linux installer, run the container image, or build from source. Moongate is under active development: the transport, packet pipeline, scripting and persistence infrastructure are available, but account login and a playable world are not implemented yet. See Implementation status.
A server start needs four things in place: a server root, a configuration that points at your client files, two PostgreSQL databases, and the core SQL migrations applied to them. The steps below produce them in that order.
Before you start
Section titled “Before you start”- Your own Ultima Online client data. Moongate does not distribute it. The initial packet protocol targets ClassicUO 7.x.
- A reachable PostgreSQL server on which you can create databases. The examples in this repository use PostgreSQL 16.
- A free TCP port; the game listener defaults to 2593.
- For a source build: Git and the .NET 10 SDK selected by
global.json. Node.js is only needed to work on the documentation website.
Where the commands live
Section titled “Where the commands live”The sequence uses three executables. Each installation method ships them:
| Command | Installed release | Source checkout |
|---|---|---|
| Prepare a root | mgboot (releases after 0.6.0) |
dotnet run --project src/Moongate.Server -- --initialize-root |
| Server | moongate |
dotnet run --project src/Moongate.Server -c Release -- |
| Migration runner | /opt/moongate/migration-runner/Moongate.MigrationRunner |
dotnet run --project src/Moongate.MigrationRunner -- ... --migrations-directory ./migrations |
The steps below use the installed names. Substitute the source-checkout form, keeping
everything after --. For the container image the same steps run through
--entrypoint; see Run with Docker.
For a source checkout, build once first:
git clone https://github.com/moongate-community/moongate.gitcd moongategit switch developdotnet build Moongate.slnx -c Releasedevelop includes unreleased work. To reproduce a release, check out its tag and
use the documentation published for that version.
First start
Section titled “First start”-
Prepare the root. Give the server a directory of its own:
Terminal window sudo mkdir -p /srv/moongate && sudo chown "$USER" /srv/moongatemgboot /srv/moongateThis writes
config/moongate.tomlwith the defaults, createslogs/,plugins/andscripts/, and copies the release’s core SQL intomigrations/. It needs no database and no client files. Prepare a root with mgboot describes what happens on a root that already exists.mgbootships in releases after 0.6.0. On 0.6.0, start the server once instead: it writes the configuration and exits. Nothing else is needed, because on 0.6.0 both the server and the migration runner read the core SQL from/opt/moongate/migrations, beside the executable:Terminal window moongate --root-directory /srv/moongate -
Edit the configuration. Open
/srv/moongate/config/moongate.tomland set the client path and both database connections. Keep the other generated sections:[ultima]ultima_path = "/absolute/path/to/your/ultima-client"[persistence.accounts]connection_string = "postgres://moongate:moongate@localhost:5432/auth"[persistence.realm]connection_string = "postgres://moongate:moongate@localhost:5432/world"Use an absolute client path; relative paths resolve from the process working directory, not from the root. The two connection strings shown are the generated defaults; change host, credentials and database names to match step 3. Outside local development, write
"$MOONGATE_ACCOUNTS_DATABASE"and"$MOONGATE_REALM_DATABASE"instead and supply the URIs from your secret provider. The configuration reference lists every setting. -
Create the databases. Moongate never creates databases or roles. With the default connection strings, run as a PostgreSQL superuser:
CREATE ROLE moongate LOGIN PASSWORD 'moongate';CREATE DATABASE auth OWNER moongate;CREATE DATABASE world OWNER moongate;Both databases are checked at every start, in every server mode. For a deployment, give the server a DML-only role and keep schema changes on a separate role; see Separate DDL and runtime roles.
-
Apply the core migrations. Startup validates the versioned SQL history and refuses to start while files are pending, so apply them first:
Terminal window /opt/moongate/migration-runner/Moongate.MigrationRunner apply --root-directory /srv/moongate --target auth/opt/moongate/migration-runner/Moongate.MigrationRunner apply --root-directory /srv/moongate --target world--target authuses[persistence.accounts],--target worlduses[persistence.realm]. The runner reads the root’s configuration and itsmigrations/directory;statusin place ofapplylists pending files without applying them. The world catalog has no core tables yet, so itsapplyreports nothing to do. -
Start the server.
Terminal window moongate --root-directory /srv/moongateAlways pass
--root-directory. Without it the server uses the directory the binary sits in. A successful start logsPostgres connection successfulonce per database, then the loaded services and the bound endpoints. A missingscripts/init.luais a warning and starts an empty scripting environment; a bootstrap script that exists but fails prevents startup. Add scripts with Writing Lua scripts. The interactive console commands are listed in Server commands. -
Stop it. Press Ctrl+C and let shutdown finish. After a successful startup the host runs a final world save before closing PostgreSQL persistence. A failed startup or a faulted game loop cannot promise that save. Do not terminate the process while it is waiting for one.
To run a second instance, give it its own root, its own listener port and its own realm database. Never point two servers at one root or at one realm database.
Files and process ownership
Section titled “Files and process ownership”All server-managed paths below are relative to --root-directory:
| Path | Purpose |
|---|---|
config/moongate.toml |
Server configuration; created once, never rewritten |
migrations/auth/, migrations/world/ |
Core SQL copied by mgboot (releases after 0.6.0); plugins ship their own under plugins/ |
logs/moongate-*.clef |
Structured JSON log events, one per line |
plugins/ |
One assembly bundle per plugin directory |
scripts/ |
Lua source and generated editor definitions |
moongate.pid |
Current process identifier |
moongate.pid.lock |
Lock file used to exclude another instance |
The PID guard is acquired before configuration is loaded. A live PID or an
already-held lock rejects another start. A stale or malformed PID is replaced.
The guard checks process liveness, not executable identity, so a reused PID can
also reject startup. Investigate that process before changing the PID file.
Normal cleanup removes the PID file if it still belongs to this process; the
.lock file may remain after its handle is released. Its presence alone does
not mean the server is running.
Console logs show time, level, source and message. File logs roll daily and at 10 MiB, keeping up to 30 files. For metrics see Diagnostics; for schema operations and world saves see PostgreSQL persistence.
Common startup problems
Section titled “Common startup problems”| Symptom | Check |
|---|---|
Exits right after writing config/moongate.toml |
Expected on a fresh root: ultima_path is still ChangeMe. Continue with step 2 |
| Client path error | Set ultima.ultima_path to readable, real client data |
| TOML parse or validation error | Fix the named field; existing files are not silently replaced |
Postgres connection failure |
The database does not exist, the host is wrong, or the role cannot log in. Inside a container, localhost is the container itself |
| Persistence variable missing | Export the PostgreSQL URI referenced by the target’s connection_string |
| Pending or changed migrations | Run the migration runner status and apply for the named target (step 4). Never edit an applied file |
| PostgreSQL schema changes required | An entity needs DDL that no migration provides. Generate and review a versioned SQL file with --persistence-schema generate, then apply it with the runner while the server is stopped |
| Port binding failure | Check network.listen_address, port availability and interface addresses |
| Another instance detected | Check the PID and running process; use a separate root for another server |
| Script startup error | Fix scripts/init.lua; inspect the script filename and line in the log |
