Skip to content

Backups and disaster recovery

PrexorCloud ships one first-party backup component: the backup-orchestrator Module. It snapshots the config and small text files of running Instances by reading them through the Daemon and packing them into a controller-local tar.gz. It does not back up MongoDB, Valkey, the Controller filesystem, or Minecraft world data — those tiers are the operator’s responsibility, and this page tells you exactly which tool covers which tier.

Read the scope section first. It is the part most likely to surprise you: a snapshot is config files, nothing more.

What you’ll learn

  • What backup-orchestrator captures and the hard limits that define that scope
  • How to trigger a snapshot over REST and how to run periodic snapshots
  • Where archives land and how to ship them off-host
  • How to restore a config snapshot (there is no restore command — you untar it)
  • The DR posture for every tier the module does not cover

The module at a glance

PropertyValueSource
Module idbackup-orchestratorsrc/main/module/module.yaml
Version1.0.0module.yaml
HostController onlyhosts: [controller]
StorageMongo, snapshots collection (≤ 50 000 docs)module.yaml, SnapshotRepository
Required capabilityprexor.instance.files (>=1.0.0 <2.0.0)module.yaml
Archive root/var/lib/prexorcloud/snapshots (override: PREXORCLOUD_BACKUP_DIR)BackupOrchestratorModule
REST mount/api/v1/modules/backup-orchestrator/snapshotsmodule-route dispatcher + BackupRoutes

The module is a Capability API consumer: it never opens its own Daemon gRPC channel. It reads Instance files through the built-in InstanceFileAccess capability the Controller registers as prexor.instance.files.

Install it like any platform Module:

Terminal window
prexorctl module install backup-orchestrator
prexorctl module list
ID VERSION HOST STATE
backup-orchestrator 1.0.0 controller ACTIVE

What the module captures

A snapshot is a tar.gz of the config files in one Instance’s working directory. That is the whole feature.

By default the snapshot picks up these filename patterns (basename only):

PatternCatches
*.propertiesserver.properties, paper-global.properties
*.jsonops.json, whitelist.json, banned-players.json
*.yml, *.yamlplugin and proxy config
*.txteula.txt and similar
*.cfg, *.tomlmod config (Forge/Fabric/NeoForge)

The pattern matcher supports * (any run of characters) and literal characters only — no ?, no character classes, no path separators in the pattern. Matching is against the basename, so *.json matches config/ops.json. Directories are always skipped.

Override the patterns per request through the REST patterns field. An empty or omitted list uses the defaults above.

What is not captured

Not capturedWhyWhat covers it
World data (region .mca, NBT, chunks)Binary; the Daemon RPC encodes content as UTF-8 and round-trips binary lossilyA prexor.instance.snapshot capability with a Daemon-side tar handler — not yet shipped
MongoDB (Groups, Templates, deployments, audit, Module data)Out of the module’s scopemongodump / your Mongo backup
Valkey coordination stateOut of scope; rebuildableOptional BGSAVE; usually skipped
Controller filesystem (controller.yml, the CA in data/certs/)Out of scopeFilesystem backup of the install root
Daemon host config and mTLS materialOut of scopePer-host backup

The most important line: the CA private key under the Controller’s data/certs/ is the only irreplaceable material in the system, and backup-orchestrator does not touch it. Back it up separately. If it is lost, every Daemon must rejoin from scratch.

The 64 KiB read cap

The scope limit above is a direct consequence of how the Daemon serves file reads. Understanding the cap explains every “truncated” you will see.

The read path has three caps stacked on top of each other:

LayerCapConstant
Daemon ReadInstanceFile default64 KiB when the request omits max_bytesInstanceFileReader.DEFAULT_MAX_BYTES = 64 * 1024
Daemon absolute ceiling1 MiB — the Daemon never returns more, whatever the caller asksInstanceFileReader.MAX_BYTES_CEILING = 1 * 1024 * 1024
backup-orchestrator per-file request256 KiBSnapshotService.READ_MAX_BYTES = 256 * 1024

The module asks for 256 KiB per file, above the 64 KiB Daemon default but well under the 1 MiB ceiling. So a config file up to 256 KiB is captured whole. A file larger than 256 KiB is captured up to the cap — the first 256 KiB — and its path is recorded in the snapshot’s truncatedFiles list so you can spot the partial. The archive still lands; truncation never fails a snapshot.

Reads are head-first (first N bytes). The Daemon also supports tail reads (tail=true, last N bytes), but backup-orchestrator always reads heads.

The directory walk that feeds the read loop has its own Daemon-side caps: 5 000 entries and 24 directory levels, with directories over 500 children summarized rather than enumerated. The module discards summary markers and only reads concrete file paths. A walk that hits the entry or depth cap comes back with truncated=true.

Every walk and read blocks up to 20 seconds and never throws. Unreachable Daemons, timeouts, and Daemon-reported errors surface as an error tag, not an exception:

Error tagMeaning
DAEMON_UNREACHABLENo live Daemon channel for the node
TIMEOUTDaemon did not reply within 20 s
INSTANCE_NOT_FOUNDNo such Instance working directory on that Daemon
FILE_NOT_FOUNDPath does not exist under the Instance dir
NOT_REGULAR_FILESymlink or non-regular file (symlinks are never followed)
PATH_OUTSIDE_INSTANCEPath-traversal attempt; rejected
FILE_UNREADABLEI/O error reading the file

A walk error short-circuits the snapshot: no archive, and an error record is still persisted so the failure is visible. A per-file read error is logged and skipped — one bad file does not abort the snapshot.

Trigger a snapshot over REST

The REST surface is the primary interface. All four routes mount under /api/v1/modules/backup-orchestrator/.

MethodPathPurpose
GET/snapshotsList recent snapshots (?instance=, ?limit=)
POST/snapshotsTrigger a snapshot
GET/snapshots/{id}Fetch one snapshot record
DELETE/snapshots/{id}Delete the archive and its record

Take a snapshot

nodeId and instanceId are required. group may be blank for an ungrouped Instance. patterns is optional.

Terminal window
curl -sS -X POST \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
https://controller:8443/api/v1/modules/backup-orchestrator/snapshots \
-d '{
"nodeId": "node-1",
"group": "lobby",
"instanceId": "lobby-1"
}'

A successful snapshot returns 201 with the metadata record:

{
"id": "0b1f9c2e-...",
"instanceId": "lobby-1",
"group": "lobby",
"nodeId": "node-1",
"createdAt": "2026-06-07T09:14:03Z",
"archiveSizeBytes": 4821,
"archivePath": "/var/lib/prexorcloud/snapshots/lobby-1/1717751643000-0b1f9c2e.tar.gz",
"fileCount": 5,
"truncatedFiles": [],
"patterns": ["*.properties", "*.json", "*.yml", "*.yaml", "*.txt", "*.cfg", "*.toml"],
"error": ""
}

Status codes:

CodeCondition
201Snapshot written (error is empty)
502Snapshot ran but the underlying walk failed — record is returned with a populated error
400instanceId or nodeId missing/blank

Override the patterns to capture only what you need:

Terminal window
curl -sS -X POST \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
https://controller:8443/api/v1/modules/backup-orchestrator/snapshots \
-d '{"nodeId":"node-1","group":"lobby","instanceId":"lobby-1","patterns":["*.yml"]}'

List and inspect

Terminal window
# Recent snapshots across all instances (default limit 50, max 500).
curl -sS -H "Authorization: Bearer $TOKEN" \
'https://controller:8443/api/v1/modules/backup-orchestrator/snapshots?limit=20'
# Just one instance.
curl -sS -H "Authorization: Bearer $TOKEN" \
'https://controller:8443/api/v1/modules/backup-orchestrator/snapshots?instance=lobby-1'
# One record by id.
curl -sS -H "Authorization: Bearer $TOKEN" \
https://controller:8443/api/v1/modules/backup-orchestrator/snapshots/<id>

The list is sorted newest-first by createdAt. A limit of 0 or non-numeric falls back to 50; anything above 500 is clamped to 500.

Delete

Terminal window
curl -sS -X DELETE -H "Authorization: Bearer $TOKEN" \
https://controller:8443/api/v1/modules/backup-orchestrator/snapshots/<id>

Returns 204 on success, 404 if the id is unknown. The archive file is removed best-effort; the Mongo record is removed when it exists.

Periodic snapshots

The module does not run a schedule by default. REST triggers are always available; the periodic path is opt-in through environment variables read once at module start.

VariableEffectDefault
PREXORCLOUD_BACKUP_INTERVAL_MINUTESSnapshot period in minutes. 0 or absent disables the schedule.disabled
PREXORCLOUD_BACKUP_INITIAL_DELAY_MINUTESDelay before the first run.1
PREXORCLOUD_BACKUP_TARGETSComma-separated nodeId/group/instanceId triples.none
PREXORCLOUD_BACKUP_DIRArchive root override./var/lib/prexorcloud/snapshots

The schedule is active only when both a positive interval and at least one well-formed target are present. Otherwise the Module stays REST-only and logs that periodic snapshots are disabled.

Targets are configured explicitly because the Module cannot enumerate live Instances from its context — list the long-lived ones (a persistent lobby, a hub) by hand:

Terminal window
# In the Controller's environment.
export PREXORCLOUD_BACKUP_INTERVAL_MINUTES=60
export PREXORCLOUD_BACKUP_INITIAL_DELAY_MINUTES=5
export PREXORCLOUD_BACKUP_TARGETS="node-1/lobby/lobby-1,node-1/hub/hub-1"

Target parsing is total — a malformed token (not a three-part node/group/instance triple, or a blank node or instance) is skipped, never fatal. The group segment may be blank for an ungrouped Instance, written as node-1//inst-1. A single unreachable target in a scheduled run is logged and skipped; it does not abort the run or kill the task.

Restart the Controller after changing these — they are read once in onStart.

Where archives land

$PREXORCLOUD_BACKUP_DIR/
└── <instanceId>/
└── <epochMillis>-<first-8-of-snapshotId>.tar.gz

The instanceId is sanitized (anything outside [A-Za-z0-9_.-] becomes _) before it is used as a directory name. The archive is a standard gzip-compressed POSIX tar; each entry’s path mirrors its relative path inside the Instance directory.

The archive is controller-local. The module does not ship anything off-host — that is your job.

Inspect an archive

Terminal window
tar tzf /var/lib/prexorcloud/snapshots/lobby-1/1717751643000-0b1f9c2e.tar.gz
server.properties
ops.json
config/paper-global.yml

Ship off-host

A snapshot on the Controller disk is one failure away from useless. Ship it.

Terminal window
# Encrypt and push to object storage.
SRC=/var/lib/prexorcloud/snapshots
age -r age1examplexxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
-o /tmp/snapshots.tar.age \
<(tar cf - -C "$SRC" .)
aws s3 cp /tmp/snapshots.tar.age s3://your-backups/prexorcloud/snapshots-$(date -u +%Y%m%dT%H%M%SZ).age

Use whatever encrypted off-host store fits your workflow — S3, restic, borg, rclone, Backblaze B2. The point is off-host and encrypted.

Restore a config snapshot

There is no restore command and no restore API. A snapshot is a tar.gz of config files; restoring it means putting those files back into the Instance’s Template or working directory and redeploying.

Typical flow after a bad config push:

Terminal window
# 1. Pull the archive (locally or from off-host store).
mkdir -p /tmp/restore && tar xzf <archive>.tar.gz -C /tmp/restore
# 2. Diff against current config to see what changed.
diff -ru /tmp/restore /path/to/template/files
# 3. Copy the good files back into the Template, then push and redeploy.
prexorctl template apply <template>
prexorctl group redeploy <group>

Restore is a manual, deliberate act. Because a snapshot is config only, restoring it never touches world data, player data, or platform state — it cannot make those worse, and it cannot recover them either.

Truncated files matter at restore time. If a file appears in truncatedFiles, the archived copy is the first 256 KiB only — do not restore it blindly over a complete file. Re-fetch the full file from the Instance or another source.

DR posture for the tiers this module does not cover

backup-orchestrator is one slice of a recovery plan. The rest is conventional infrastructure work. Treat this table as the checklist for the tiers the Module leaves to you.

TierSourceHow to back upWhat “recovered” means
Durable platform stateMongoDBmongodump on your cadence; ship off-hostController boots; every Group, Template, deployment, audit row, and Module record returns; Daemons reconnect with existing certs
CoordinationValkeyOptional BGSAVE; usually skippedEmpty Valkey is acceptable — the Controller rebuilds leases on first reconciliation
Controller filesystemcontroller.yml, data/certs/ (the CA)Filesystem backup of the install rootConfig and CA recoverable; the CA private key is the only irreplaceable material
Daemon hostsdaemon.yml + per-Daemon mTLSPer-host backupDaemon restored; reconnects and reconciles Instances from the Controller
Instance configInstance working-dir config filesbackup-orchestrator (this Module)Config files recoverable per the scope and cap above
Instance world dataRegion/NBT filesOut of scope today — use server-side world saving / your own snapshot jobNot covered by PrexorCloud tooling yet

Sizing your RPO is direct: RPO equals your slowest relevant cadence. If you mongodump hourly and snapshot Instance config daily, your platform-state RPO is one hour and your config RPO is one day. PrexorCloud does not run those crons for you.

Common failures

SymptomCauseFix
POST /snapshots returns 502 with error: DAEMON_UNREACHABLENo live Daemon channel for nodeIdConfirm the Daemon is connected (prexorctl node list); retry
error: INSTANCE_NOT_FOUNDWrong group/instanceId, or the Instance is not running on that nodeCheck the Instance is alive on the named node
Snapshot has fileCount: 0No files matched the patternsWiden patterns, or confirm the Instance dir actually holds config files
File appears in truncatedFilesFile exceeds the 256 KiB per-file capExpected for large files; do not restore the partial over a complete file
Snapshot silently misses a deep fileWalk hit the 5 000-entry / 24-level cap (truncated=true on the walk)Reduce the patterns; deep nested config beyond the cap is not enumerated
Periodic snapshots never runInterval is 0/absent or no valid targetSet both PREXORCLOUD_BACKUP_INTERVAL_MINUTES (> 0) and a valid PREXORCLOUD_BACKUP_TARGETS; restart the Controller
Module fails to load: missing capabilityprexor.instance.files not registered (built-in; should always be present)Check Controller startup logs; the capability registers before stored Modules load

Next up