454b3842bb
Chat session updates with their profile key checked against the services' keys, signed chat and the message chain, last-seen acknowledgements, the signature cache, signed say/me/msg, enforce-secure-profile, and vanilla's log lines. Offline servers and players without a chat session chat unsigned, as on vanilla. tools/test_secure_chat_parity.py compares both servers with test keys and a mock services server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
331 lines
16 KiB
Markdown
331 lines
16 KiB
Markdown
# Host a server
|
||
|
||
`horizon-server` hosts a Minecraft Java Edition 26.3 server with Horizon's own game as the engine: one native
|
||
program, no Java, one settings file, a command line, and a web panel. Vanilla 26.3 clients and Horizon clients
|
||
can join it. This page covers what works on `main` today and says plainly what doesn't yet. The design and the
|
||
plan are in [docs/server.md](https://github.com/ludovic111/horizon/blob/main/docs/server.md).
|
||
|
||
## Before you start
|
||
|
||
- **Online mode checks accounts** (release v2026.10.17.07 and newer). With `online_mode = true`, the
|
||
default, players who sign in with the official Minecraft launcher join with their own accounts, as on a
|
||
vanilla server.
|
||
- **Names you add are accounts** (release v2026.10.17.14 and newer). `init --player`, `horizon-server
|
||
whitelist add` and `op`, and the console's `whitelist add`, `op` and `ban` look a name up the way vanilla
|
||
does: players already seen (kept in `usercache.json`), then Mojang's profile service. In online mode a name
|
||
with no account is refused. So you can whitelist friends before they first join.
|
||
- **Horizon's own client can't sign in to a Microsoft account yet.** That needs an Azure app registration and
|
||
Mojang's approval, which the project doesn't have
|
||
([docs/microsoft-account.md](https://github.com/ludovic111/horizon/blob/main/docs/microsoft-account.md)).
|
||
Until then Horizon players can join offline servers only (`online_mode = false`). An offline server never
|
||
checks accounts: anyone who reaches it can join under any name. Keep it on your LAN or a tailnet
|
||
(Tailscale, ZeroTier), or share it only with friends you trust.
|
||
- **Secure chat** works as on vanilla's server: in online mode, vanilla clients sign their chat, and with
|
||
`enforce-secure-profile=true` (the default) unsigned chat is refused. On an offline server, or for a player
|
||
without a chat session (Horizon's own client today), chat stays unsigned and shows as not secure.
|
||
- **Nether and End:** each player travels on their own through portals, and operators with `execute in
|
||
<dimension> run tp`. The players in the level they leave stop seeing them, and the ones in the level they
|
||
arrive in start to, as on vanilla (checked against vanilla with two players, by command and through an End
|
||
portal: `tools/test_server_dimension_players_parity.py`). The console can't send another player to a
|
||
dimension yet (`execute in ... run tp NAME`).
|
||
- **Movement checks and chunk pacing** (release v2026.10.17.16 and newer). The server pulls back players who
|
||
move too fast ("moved too quickly"), as vanilla does, and sends chunks as fast as each client acknowledges
|
||
them.
|
||
- **No Docker image is published.** You build it yourself from the repository (below). Publishing an image
|
||
needs the project owner.
|
||
- Nothing of Mojang's is downloaded or shipped. The server reads the game files you prepare from your own copy
|
||
of Minecraft 26.3.
|
||
|
||
## 1. Install
|
||
|
||
Linux (x86-64) and macOS (Apple Silicon):
|
||
|
||
```sh
|
||
curl -fsSL https://raw.githubusercontent.com/ludovic111/horizon/main/packaging/server/install.sh | sh
|
||
```
|
||
|
||
Windows (x64), in PowerShell:
|
||
|
||
```powershell
|
||
irm https://raw.githubusercontent.com/ludovic111/horizon/main/packaging/server/install.ps1 | iex
|
||
```
|
||
|
||
The script downloads the latest Horizon release for your computer from GitHub, checks it against the SHA-256
|
||
GitHub publishes, and never asks for sudo or administrator rights. It installs:
|
||
|
||
- the release in `~/.local/share/horizon/releases/<version>` (Windows: `%LOCALAPPDATA%\Horizon`), with
|
||
`current` pointing at the one in use;
|
||
- `horizon-server` and `horizon-prepare` in `~/.local/bin` (Windows: added to your PATH);
|
||
- a `game-files` folder for the files you prepare, kept across updates.
|
||
|
||
Options: `--version vX.Y.Z`, `--dir FOLDER`, `--bin FOLDER` (Windows: `-Version`, `-Dir`). Other systems,
|
||
such as Linux on ARM, build from source: `cargo build --release -p horizon-client --bin horizon-client -p
|
||
horizon-server` and keep both programs in one folder.
|
||
|
||
The Horizon launcher also installs releases. Every release folder holds `horizon-server`, so you can run it
|
||
from there too.
|
||
|
||
## 2. Prepare the game files
|
||
|
||
Horizon reads Minecraft's data from your own copy of the game. You need Minecraft Java Edition 26.3, played
|
||
once from the official launcher on this computer, Java 25 (a JDK), and the 26.3 `server.jar`: it's the
|
||
`downloads.server.url` in your launcher's `versions/26.3/26.3.json`. Then:
|
||
|
||
```sh
|
||
horizon-prepare --prepare --server-jar /path/to/server.jar
|
||
```
|
||
|
||
Add `--minecraft-dir PATH` if your launcher keeps the game elsewhere. The prepared files go to
|
||
`game-files`, which updates keep.
|
||
|
||
**On a headless machine** (a VPS, a home server), prepare on your own computer instead, with the launcher or
|
||
`horizon-prepare`, and copy the prepared `26.3` folder to the server: into
|
||
`~/.local/share/horizon/game-files/horizon-client/26.3`, or into `resources/` in the server folder, or anywhere
|
||
`[engine] resources` in `server.toml` names. A folder copied this way needs release v2026.10.17.07 or
|
||
newer: older ones don't keep `registries.json` and `commands.json` with the prepared files, and the
|
||
server then stops with "registries.json not found next to the resources". Prepare again with a newer release.
|
||
|
||
## 3. First run
|
||
|
||
```sh
|
||
mkdir my-server && cd my-server
|
||
horizon-server init
|
||
```
|
||
|
||
`init` asks five things and writes `server.toml`:
|
||
|
||
1. whether you agree to the [Minecraft EULA](https://aka.ms/MinecraftEULA): only a yes writes `eula=true`;
|
||
2. the world's name and an optional seed (an existing world or vanilla server folder is kept and its
|
||
`server.properties` imported);
|
||
3. the port, 25565 by default;
|
||
4. whether to use a whitelist (yes by default, as in vanilla) and who to add;
|
||
5. whether to make the first player an operator.
|
||
|
||
It then tells you where it found the game and its files. Every question has a flag, so it also runs from a
|
||
script:
|
||
|
||
```sh
|
||
horizon-server init --eula --world survival --seed 12345 --port 25565 --whitelist --player Alex --op Alex --yes
|
||
```
|
||
|
||
`--no-whitelist` turns the whitelist off, and `--offline` turns online mode off (see "Before you start").
|
||
The players you add are looked up as Minecraft accounts in online mode, and get offline UUIDs on an offline
|
||
server, as vanilla does.
|
||
|
||
Start the server in the foreground, with the console on your terminal (Ctrl-C stops it cleanly):
|
||
|
||
```sh
|
||
horizon-server run
|
||
```
|
||
|
||
or in the background:
|
||
|
||
```sh
|
||
horizon-server start
|
||
horizon-server status
|
||
```
|
||
|
||
```text
|
||
Running, up 29 s
|
||
TPS 20.0, MSPT 1.0 ms average, 3.1 ms P95
|
||
0 of 20 players, 377 chunks loaded
|
||
RAM 1.42 GB, CPU 2 % of a core
|
||
```
|
||
|
||
Players join at your computer's address and the port (`192.168.1.20:25565`, or a tailnet name). Every
|
||
command takes `--dir FOLDER` to work on a server folder from elsewhere.
|
||
|
||
## 4. Settings: `server.toml`
|
||
|
||
You edit one file. The server writes vanilla's `server.properties` from it before every start, so the game
|
||
reads exactly what a vanilla server reads, and a vanilla server folder works as it is.
|
||
|
||
```toml
|
||
[server]
|
||
motd = "A Minecraft Server"
|
||
port = 25565
|
||
bind = "" # listen on every address, or only on this one (server-ip)
|
||
max_players = 20
|
||
online_mode = true # check accounts with Mojang (see "Before you start")
|
||
whitelist = true
|
||
enforce_whitelist = false
|
||
view_distance = 10
|
||
simulation_distance = 10
|
||
|
||
[world]
|
||
name = "world"
|
||
seed = "" # empty: a random seed on the first start
|
||
game_mode = "survival"
|
||
difficulty = "easy"
|
||
hardcore = false
|
||
|
||
# Any other server.properties key, spelled as vanilla spells it:
|
||
[properties]
|
||
# spawn-protection = 16
|
||
|
||
[engine]
|
||
path = "" # the game program; empty: the one beside horizon-server
|
||
resources = "" # the prepared game files; empty: resources/ here, else the usual places
|
||
restart_on_crash = true
|
||
|
||
[backups]
|
||
every = "6h" # 30m, 6h, 1d...; "0" turns automatic backups off
|
||
keep = 12 # automatic backups kept; backups made by hand are never deleted
|
||
|
||
[panel]
|
||
bind = "127.0.0.1:8130" # this computer only, or a tailnet address (100.x); "" turns it off
|
||
allow_public = false
|
||
```
|
||
|
||
The defaults are vanilla 26.3's. Unknown keys and values vanilla would refuse are errors, so a typo stops the
|
||
start instead of being ignored. When the game changes a setting itself (`/whitelist on`), the change is copied
|
||
back into `server.toml` when it stops, and your comments are kept. Changes to the file apply at the next
|
||
restart: there is no live reload yet.
|
||
|
||
Already have a vanilla server? Run `horizon-server init` in its folder, or `horizon-server import
|
||
server.properties` to write `server.toml` from it. The world, `ops.json`, `whitelist.json` and the ban lists
|
||
keep vanilla's names and formats, so going back to vanilla works too.
|
||
|
||
## 5. Commands
|
||
|
||
```text
|
||
horizon-server init first run
|
||
horizon-server run in the foreground, with the console; Ctrl-C stops it
|
||
horizon-server start | stop | restart in the background (its output goes to logs/supervisor.log)
|
||
horizon-server status [--json] up or not, TPS, MSPT, players, chunks, RAM, CPU
|
||
horizon-server cmd COMMAND... one console command, and what it answers
|
||
horizon-server logs [-f] [-n N] the log; -f follows it
|
||
horizon-server op|deop NAME
|
||
horizon-server whitelist on|off|list
|
||
horizon-server whitelist add|remove NAME
|
||
horizon-server backup [NAME] back up now
|
||
horizon-server backups list the backups
|
||
horizon-server restore BACKUP [--yes]
|
||
horizon-server service install|uninstall|print [--system]
|
||
horizon-server panel a one-time login link for the web panel
|
||
horizon-server import FILE server.toml from a vanilla server.properties
|
||
```
|
||
|
||
While the server runs, `op`, `whitelist` and `cmd` go to the game's console, so they act exactly as vanilla's
|
||
console commands do. While it's stopped, they edit vanilla's files directly. Any console command works through
|
||
`cmd`: `horizon-server cmd ban Griefer`, `horizon-server cmd say Restarting in 5 minutes`.
|
||
|
||
The server restarts the game by itself if it crashes: after 5 s, then waiting longer each time, up to 5
|
||
minutes. It keeps the crash report and gives up after 5 crashes in 10 minutes, and `status` says why.
|
||
|
||
Coming: `update` and `rollback`, `console` (attach to a server running in the background), `ban` and
|
||
`pardon` as their own commands, and `mcp` (an MCP server so an AI assistant can run the server).
|
||
|
||
## 6. Web panel
|
||
|
||
The panel runs inside `horizon-server` while the server runs. It shows live TPS, MSPT, RAM and CPU graphs for
|
||
the last 10 minutes, the players with their ping (and a kick button), the console with a command line, and the
|
||
backups (back up now, restore with a confirmation), with Restart and Stop.
|
||
|
||
```sh
|
||
horizon-server panel
|
||
```
|
||
|
||
prints a login link that works once, for 10 minutes. The panel listens on `127.0.0.1:8130`, this computer
|
||
only. To reach it from your other devices:
|
||
|
||
- put your tailnet address in `[panel] bind` (for example `bind = "100.101.102.103:8130"`), or
|
||
- forward the port over SSH: `ssh -L 8130:127.0.0.1:8130 your-server`, then open the link on your computer.
|
||
|
||
Any other address, such as `0.0.0.0` or a public IP, is refused unless you set `allow_public = true`, and
|
||
even then the panel warns at every start. It has no passwords to leak: whoever can read the server folder can
|
||
make a login link, and no one else can. Never put it on the open internet.
|
||
|
||
## 7. Backups
|
||
|
||
```sh
|
||
horizon-server backup before-the-update
|
||
horizon-server backups
|
||
horizon-server restore before-the-update
|
||
```
|
||
|
||
A backup zips the world, vanilla's files and `server.toml` into `backups/`. A running server saves first and
|
||
stays up: players don't notice. Automatic backups run on the `[backups] every` schedule while the server runs,
|
||
and the oldest automatic ones beyond `keep` are deleted. Backups you make by hand are never deleted.
|
||
|
||
`restore` takes a backup's file name, a unique start of it, or the name you gave it. It moves the current
|
||
world aside to `backups/replaced-<time>/` (never deleted), puts the backup in place, and restarts the server if
|
||
it was running. The panel does the same with one click.
|
||
|
||
## 8. Start it with the computer
|
||
|
||
```sh
|
||
horizon-server service install
|
||
```
|
||
|
||
- **Linux:** a systemd user unit (`--system` for a system unit, with sudo). To keep a user unit running while
|
||
you're logged out: `loginctl enable-linger $USER`.
|
||
- **macOS:** a launchd agent in `~/Library/LaunchAgents`.
|
||
- **Windows:** a task that starts at logon (a real Windows service comes later).
|
||
|
||
The service runs `horizon-server run` through the installed `current` release, so an update applies at the
|
||
next restart. `service print` shows what it would install, `service uninstall` removes it and leaves the server
|
||
folder alone.
|
||
|
||
## 9. Docker (build it yourself)
|
||
|
||
No image is published: build one from the repository. The image holds none of Minecraft's files; you mount the
|
||
folder you prepared (step 2) read-only.
|
||
|
||
```sh
|
||
git clone https://github.com/ludovic111/horizon && cd horizon
|
||
docker build -f packaging/server/Dockerfile -t horizon-server .
|
||
|
||
docker run -d --name horizon -p 25565:25565 --stop-timeout 180 \
|
||
-v horizon-data:/data \
|
||
-v /path/to/26.3:/opt/horizon/.local/horizon-client/26.3:ro \
|
||
-e HORIZON_EULA=true -e HORIZON_WHITELIST=true -e HORIZON_PLAYERS=Alex,Steve -e HORIZON_OP=Alex \
|
||
horizon-server
|
||
```
|
||
|
||
- `HORIZON_EULA=true` accepts the [Minecraft EULA](https://aka.ms/MinecraftEULA) for this server. Without it
|
||
the container stops and says why.
|
||
- The first start writes `/data/server.toml` from `HORIZON_EULA`, `HORIZON_WORLD`, `HORIZON_SEED`,
|
||
`HORIZON_PORT`, `HORIZON_WHITELIST`, `HORIZON_PLAYERS`, `HORIZON_OP` and `HORIZON_ONLINE`. Later starts use
|
||
the file.
|
||
- The mounted folder must come from release v2026.10.17.07 or newer (see step 2).
|
||
- Manage it with `docker exec horizon horizon-server status` (and `cmd`, `backup`, `logs`, ...).
|
||
`docker stop` saves and stops cleanly; `--stop-timeout 180` gives it the time.
|
||
- The panel stays on 127.0.0.1 inside the container.
|
||
- The image still carries ALSA and udev libraries because the game links them, even though a server never plays
|
||
a sound. A smaller engine without them is planned.
|
||
|
||
## 10. How fast is it
|
||
|
||
The latest bot benchmark against vanilla 26.3 is in
|
||
[benchmarks/server-2026-10-17](https://github.com/ludovic111/horizon/blob/main/benchmarks/server-2026-10-17/README.md).
|
||
It ran on `main` at `7bbbf6e9` (in release v2026.10.17.17), on a 4-thread laptop CPU (i7-7500U), with the bots
|
||
on another machine. Earlier measurements are in [docs/server.md](https://github.com/ludovic111/horizon/blob/main/docs/server.md).
|
||
In short:
|
||
|
||
- **Memory:** 0.8–0.9 GB with up to 100 players, and 1.4 GB at most with 100 players flying out fast. Vanilla
|
||
uses 3.4–4.0 GB with its 4 GB heap.
|
||
- **Start:** a new world is ready in 9–10 s, against 15–17 s for vanilla.
|
||
- **Same work** (players exploring at the same pace, so both servers send the same chunks): Horizon's average
|
||
tick is 44–81 % shorter than vanilla's with 1 to 50 players, and its 95th percentile is lower too.
|
||
- **Players walking around spawn:** Horizon's average tick is shorter at every count. With 25 and 50 players,
|
||
its slowest ticks (95th percentile) are a little longer than vanilla's (31 against 28 ms, 60 against 56 ms).
|
||
- **100 players** walking: Horizon keeps 15.8 TPS (63 ms ticks). Vanilla's watchdog stopped it on this machine.
|
||
- **Players flying out as fast as they can:** with 10 and 25 players vanilla's average tick is a little shorter
|
||
than Horizon's, but Horizon sends 1.6–1.8 times as many new chunks. Vanilla's watchdog stopped it at 50 and 100.
|
||
|
||
On this 4-thread machine Horizon held 20 TPS with 50 players in every scenario, and 15.8–19 TPS with 100. For more
|
||
than 50 active players, give it more cores.
|
||
|
||
## When something goes wrong
|
||
|
||
- **"no prepared game files"**: `init` and `run` list the folders they looked in. Prepare (step 2), or set
|
||
`[engine] resources`.
|
||
- **"registries.json not found next to the resources"**: the prepared folder is from an older release. Prepare
|
||
again with a newer one.
|
||
- **Nobody can join**: the whitelist is on by default (`horizon-server whitelist add NAME`), and the port must be
|
||
open in your firewall. On a tailnet, use the server's tailnet address.
|
||
- **It keeps restarting**: `horizon-server status` shows the last crash and `horizon-server logs -n 200` the log
|
||
before it. Crash reports are in `crash-reports/`.
|
||
- Report problems at <https://github.com/ludovic111/horizon/issues>, with the end of the log.
|