A Docker Compose sandbox for exercising the Cartograph Minecraft plugin against real server software, with simulated player traffic via Mineflayer bots.
The rig supports six platforms (Paper, Spigot, Folia, NeoForge, Velocity,
BungeeCord) across two Minecraft version lines (1.21.x on Java 21,
26.1.x on Java 25). Plugin telemetry can be inspected locally via a tiny
Node sink, or pointed at any HTTP endpoint via CARTOGRAPH_API_URL.
cp .env.example .env
./gradlew -p ../cartograph-plugin installToTestrig # populates plugins/
bin/tail-telemetry & # local telemetry sink
bin/up paper 1.21 # start a topology
bin/bots idle 5 # spawn 5 idle botsWithin a minute you should see boot, heartbeat, and player join/leave events in the sink.
| Command | Bring-up |
|---|---|
bin/up paper 1.21 |
Paper 1.21.4 standalone backend on :25565 |
bin/up paper 26.1 |
Paper 26.1 standalone backend on :25565 |
bin/up spigot 1.21 |
Spigot 1.21.4 standalone backend on :25565 |
bin/up spigot 26.1 |
Spigot 26.1 standalone backend on :25565 |
bin/up folia 1.21 |
Folia 1.21.4 (experimental channel) on :25565 |
bin/up neoforge 1.21 |
NeoForge 1.21.4 standalone server on :25565 |
bin/up velocity 1.21 |
Velocity proxy on :25577 + 2 Paper backends (network-internal) |
bin/up velocity 26.1 |
Velocity (Java 25) on :25577 + 2 Paper backends |
bin/up bungee 1.21 |
BungeeCord proxy on :25578 + 2 Spigot backends |
bin/up bungee 26.1 |
BungeeCord (Java 25) on :25578 + 2 Spigot backends |
bin/up folia 26.1 and bin/up neoforge 26.1 are intentionally rejected.
Folia has not released a 26.x experimental build, and NeoForge 26.x is beta-only
upstream — the plugin's policy is to support stable releases only. These topologies
will be added when the upstream stable releases ship.
bin/down brings down whichever topology is up. bin/reset additionally
prunes volumes for a clean slate.
bin/bots command |
What it does |
|---|---|
bin/bots idle 5 |
5 bots, idle for 300s — heartbeat & presence |
bin/bots active 8 600 |
8 bots, random walks + chat for 600s |
bin/bots churn 20 300 |
20 bots, rapid join/leave loop for 300s |
bin/bots soak 3 1800 |
3 bots, slow wander for 30 minutes |
bin/bots auto-detects the active topology and connects to the proxy
(when present) or directly to the backend.
There are two ways to receive telemetry:
1. Local Node sink (default). bin/tail-telemetry [port] runs a tiny
HTTP server that pretty-prints every payload to stdout and replies
204 No Content. Set CARTOGRAPH_API_URL=http://host.docker.internal:8000/api/telemetry
in .env (already the default).
2. Real platform. Point CARTOGRAPH_API_URL at a running Cartograph
platform instance, a staging URL, or a tunnel.
The reference scenarios from the test rig brief, with concrete commands:
| Scenario | Topology | Bots | What it verifies |
|---|---|---|---|
| Boot event on startup | bin/up paper 1.21 |
none | Boot event arrives with token + version metadata |
| Heartbeat cadence | bin/up paper 1.21 |
none | Heartbeats every 60s |
| Player join/leave events | bin/up paper 1.21 |
bin/bots idle 5 |
Join on connect, leave on disconnect |
| Buffered event flush | bin/up paper 1.21 |
bin/bots churn 20 300 |
Time-threshold flush under load |
| Session duration | bin/up paper 1.21 |
bin/bots soak 3 1800 |
Accurate session durations |
| Graceful shutdown | bin/up paper 1.21 |
bin/bots idle 2 600 |
Shutdown event fires on bin/down |
| Spigot parity | bin/up spigot 1.21 |
bin/bots active 5 300 |
Same telemetry as Paper |
| Folia thread safety | bin/up folia 1.21 |
bin/bots active 10 300 |
No region-thread violations |
| NeoForge mod loading | bin/up neoforge 1.21 |
bin/bots idle 3 300 |
Mod loads, full telemetry fires |
| Velocity proxy + backends | bin/up velocity 1.21 |
bin/bots active 8 300 |
Three distinct tokens, correct routing |
| BungeeCord proxy + backends | bin/up bungee 1.21 |
bin/bots active 8 300 |
Same as Velocity for Bungee |
| 26.1 — supported platforms | bin/up <paper|spigot|velocity|bungee> 26.1 |
bin/bots idle 3 300 |
Plugin works on Java 25 / MC 26.1 |
| Auth failure handling | bin/up paper 1.21 (with bogus token) |
none | Plugin handles 401 without crashing |
| Multi-topology side-by-side | bin/up velocity 1.21 && bin/up bungee 1.21 |
bin/bots idle 2 300 per topology |
Both proxy nets coexist on different ports |
| Var | Used by | Notes |
|---|---|---|
CARTOGRAPH_API_URL |
every plugin-running container | Telemetry destination |
TOKEN_BACKEND |
standalone backend | API key for single-server topologies |
TOKEN_VELOCITY_PROXY |
velocity-proxy |
Distinct per-node token |
TOKEN_VELOCITY_BACKEND_{1,2} |
Velocity backends | One per backend |
TOKEN_BUNGEE_PROXY |
bungee-proxy |
|
TOKEN_BUNGEE_BACKEND_{1,2} |
BungeeCord backends | |
SERVER_TYPE / SERVER_VERSION / SERVER_IMAGE_TAG / PLUGINS_VERSION_DIR / PLUGINS_TYPE_DIR |
every container | Set by bin/up arguments |
SERVER_MEMORY / PROXY_MEMORY |
resource limits | |
BOT_* |
bot orchestrator | Defaults overridden by bin/bots arguments |
First Spigot boot is very slow. The itzg/minecraft-server image runs
BuildTools to compile Spigot from source on first boot. Subsequent boots
are fast because the result is cached in the data volume. bin/reset will
clear that cache.
Bots exit immediately with unsupported_protocol. Mineflayer protocol
support for 26.1 may not have shipped yet. Run the topology without bots
and observe boot/heartbeat events directly. See bots/README.md.
Plugin doesn't connect to the sink. Check from inside the container:
docker exec $(docker compose -p cartograph-testrig ps -q backend) \
sh -c 'apk add --no-cache curl >/dev/null 2>&1 || true; \
curl -v "$CARTOGRAPH_API_URL"'Linux hosts may need extra_hosts: ["host.docker.internal:host-gateway"]
added to each compose service if they don't already have it via Docker
Desktop.
${CARTOGRAPH_*} literals appear in the container's plugin config.
That means REPLACE_ENV_VARIABLES=TRUE isn't being honored, or
REPLACE_ENV_VARIABLE_PREFIX is misspelled. Confirm both env vars are
set on the relevant service and that the file is under
/data/plugins/ (Bukkit), /data/config/ (NeoForge), or /server/ (proxies).
Where do the JARs come from? Either run
./gradlew -p ../cartograph-plugin installToTestrig to populate every
plugins/<platform>/<version>/ directory, or drop a JAR by hand. See
plugins/README.md.
cartograph-testrig/
├── docker-compose.yml # base: shared network only
├── compose/ # one file per topology, plus overrides
├── plugins/<platform>/<line>/ # JAR drop directories (gitignored)
├── config/ # plugin config templates with ${VAR}
├── proxy-config/ # Velocity/BungeeCord server-side configs
├── bots/ # Mineflayer orchestrator (TS + Docker)
├── tail-telemetry/ # Node debug sink + Jest test
├── bin/ # POSIX shell helpers
└── docs/superpowers/{specs,plans}/ # design & plan docs
Plugin JARs are version-specific (cartograph-paper-1.21-*.jar differs
from cartograph-paper-26.1-*.jar). A flat plugins/bukkit/ would force
swapping JARs every time you switch MC line. The per-version subdirectory
layout means both lines can stay populated, and bin/up simply mounts
the matching one.
The Paper, Spigot, and Folia JARs all declare themselves as name: Cartograph
in plugin.yml, so mounting all three into /data/plugins at once causes
an "Ambiguous plugin name" error on server boot. To avoid this, the Bukkit
tree splits one level deeper by server type:
plugins/bukkit/<line>/{paper,spigot,folia}/. bin/up exports
PLUGINS_TYPE_DIR (paper | spigot | folia) which the compose files
use to mount only the matching subdirectory. NeoForge, Velocity, and
BungeeCord don't have this issue (each platform produces a single,
uniquely-named JAR).
config/bukkit/ is the standalone-server config. config/bukkit-backend/
is identical except for flags.proxy-backend: true, which tells the
Cartograph plugin to skip player join/leave tracking and report the
node's type as BACKEND. Without this flag, backends behind a Velocity
or BungeeCord proxy emit duplicate join events alongside the proxy.
bin/up exports a small set of vars, plus the TOKEN_* values from
.env. Compose interpolates them into service env. The itzg/* images,
seeing REPLACE_ENV_VARIABLES=TRUE and REPLACE_ENV_VARIABLE_PREFIX=CARTOGRAPH_,
substitute ${CARTOGRAPH_*} placeholders inside the synced config files
on startup. End result: each container sees its own API key inside
config.yml.