Minecraft World Backups and Isolated Restore
A verifiable backup contract connecting RCON saves, live file copies, tar archives, Cloud Storage retention, and isolated restore.
For operational backups, a systemd timer ran a Bash script. When no players were connected, the
script called RCON save-all, copied world*, configuration, and plugins into a working directory,
compressed the copy with tar and pigz, and uploaded it to Cloud Storage.
The remote archive was not guaranteed to represent a consistent recovery point. Minecraft writes
continued during cp -r, and some copy failures were logged without stopping the job. The archive
was tar.gz, while the startup script’s restore branch called unzip. These are gaps visible in
the source, not evidence that a real restore incident occurred.
The Backup Pipeline and Its Failure Points
flowchart LR accTitle: Production world-backup pipeline and failure points accDescr: A systemd timer checks connected players, runs save-all, copies the live world, creates a tar.gz archive, and uploads it to Cloud Storage. Consistency and error propagation can fail between those stages. T[systemd timer] --> P[Check connected players] P --> S[RCON save-all] S --> C[cp -r working copy] C --> A[tar + pigz] A --> U[gsutil upload] U --> N[Discord notification] S -. Autosave continues .-> C C -. Some errors ignored .-> A A -. tar gzip archive .-> R[Restore branch] R -. expects ZIP .-> X[Format mismatch]
The timer used UTC to run in the early morning in Korea. Persistent=true caused a missed run to
execute on the next boot.
[Unit]
Description=Daily Minecraft backup
[Timer]
OnCalendar=*-*-* 19:00:00 UTC
Persistent=true
[Install]
WantedBy=timers.target
If the VM was off at the scheduled time, Persistent=true runs the missed job when the VM next
starts. That can make backup race with Minecraft restoration and initialization immediately after
boot. After=minecraft.service alone only orders unit startup; it does not prove readiness. The
timer needs a delay or readiness gate, and the script must fail safely when RCON is unavailable.
Preventing duplicate execution was implemented with flock.
LOCKFILE=/run/lock/minecraft-backup.lock
exec 200>"$LOCKFILE"
flock -n 200 || {
echo "backup already running" >&2
exit 1
}
For one VM, this is enough to prevent overlap between timer and manual runs. Do not delete the lock file to release it; let the kernel release the lock when the file descriptor closes.
Why save-all Does Not Freeze a Point in Time
Minecraft’s latest state is split between JVM memory and multiple files on disk. Region, entity,
player-data, and level files are not replaced atomically. save-all flushes memory to disk, but it
does not prevent a later autosave from changing those files.
The actual core code was in the following order:
execute_rcon_command "save-all"
cp -r "$MINECRAFT_DIR/world"* "$TMP_BACKUP_DIR/" \
2>/dev/null || log_error "world copy failed"
cp "$MINECRAFT_DIR/"{ops.json,whitelist.json,server.properties} \
"$TMP_BACKUP_DIR/" 2>/dev/null || log_error "config copy failed"
tar -I 'pigz -9' -cf "$BACKUP_PATH" -C "$TMP_BACKUP_DIR" .
Neither cp failure exits the script, so an archive missing a required world can still be compressed
and uploaded. If an autosave occurs while a large world is being copied, early region files and
later player-data files may represent different points in time. cp -r produces a copy, not a
snapshot.
Zero connected players does not mean zero writes. Ticks, plugins, autosave, and scheduled tasks can continue changing files. Checking player count is a useful load-management policy, but consistency requires a separate write boundary.
Ending the Write Pause Even When Copying Fails
For a small server, the working copy can follow this order: save-off, save-all flush, copy, then
save-on.
sequenceDiagram
accTitle: application-consistent working copy creation sequence
accDescr: The job disables autosave, flushes state to disk, copies the required files, and re-enables autosave on both success and failure.
participant Job as backup job
participant MC as Minecraft
participant Disk as live world
participant Work as working copy
Job->>MC: save-off
Job->>MC: save-all flush
MC-->>Job: Save completion response
Job->>Disk: Verify list of required files
Disk->>Work: copy
alt copy success
Job->>MC: save-on
Job->>Work: Create manifest and checksum
else command or copy failure
Job->>MC: save-on
Job-->>Job: Exit failed and block upload
end
Put save-on in the cleanup handler, not only at the end of the success path. A flag can avoid
issuing it when writes were never disabled.
set -Eeuo pipefail
save_disabled=0
work_dir=$(mktemp -d /backup/minecraft.XXXXXX)
cleanup() {
status=$?
if (( save_disabled == 1 )); then
rcon "save-on" || echo "CRITICAL: save-on failed" >&2
fi
rm -rf -- "$work_dir"
exit "$status"
}
trap cleanup EXIT INT TERM
rcon "save-off"
save_disabled=1
rcon "save-all flush"
cp -a -- /opt/minecraft/world /opt/minecraft/world_nether \
/opt/minecraft/world_the_end "$work_dir/"
rcon "save-on"
save_disabled=0
set -e cannot catch every failure, but it does prevent the old pattern of turning a required copy
failure into success with || log. Keep explicit lists of required and optional files, and do not
create an archive when a required file is missing. A save-on failure in cleanup threatens the live
game state and deserves a higher-severity alert than an ordinary backup failure.
Autosave remains disabled for the duration of the copy, so the gameplay risk grows with the world. Persistent Disk or filesystem snapshots shorten that pause, but Minecraft should still be flushed first to approach an application-consistent point. A raw disk snapshot may otherwise resemble the state after a crash.
A Verifiable Archive from the Working Copy
Once the working copy exists, Minecraft writes can resume before compression. That keeps a long compression step out of the write pause. Check size and file inventory before and after creating the archive.
archive="minecraft-$(date -u +%Y%m%dT%H%M%SZ).tar.gz"
tar --create --gzip --file "$archive" \
--directory "$work_dir" \
world world_nether world_the_end server.properties plugins
sha256sum "$archive" > "$archive.sha256"
tar --list --gzip --file "$archive" >/dev/null
The production code used tar -I 'pigz -9'. File extension, archive format, and restoration tool
must agree. Extract tar.gz with tar -xzf or an equivalent tar API; reserve unzip for ZIP
archives.
The manifest beside the archive carries the minimum information needed to make a restore decision.
{
"schema_version": 1,
"archive_format": "tar.gz",
"created_at": "2026-07-23T00:00:00Z",
"server_version": "example-version",
"worlds": ["world", "world_nether", "world_the_end"],
"sha256": "example-sha256",
"uncompressed_bytes": 123456789
}
Real project, bucket, username, and RCON credentials do not belong in the manifest. If a plugin uses an external database, its backup and consistency point must be part of the same manifest. Restoring only file-based state may open the world while silently losing operational plugin state.
Upload completion and storage policy
A Cloud Storage upload is complete only when the archive and its checksum or manifest both succeed. Upload under a temporary name and promote it to the final prefix after verification, or upload the manifest last so only bundles with a manifest count as complete. Discord should summarize the final object generation and verification result without exposing signed URLs or secrets.
In production, Cloud Scheduler published to Pub/Sub and a separate function deleted old objects. If retention depends only on age, a Cloud Storage lifecycle rule expresses the same policy with fewer resources and permissions.
resource "google_storage_bucket" "backup" {
name = "example-minecraft-backups"
location = var.region
uniform_bucket_level_access = true
versioning {
enabled = true
}
lifecycle_rule {
condition {
age = 30
matches_prefix = ["verified/"]
}
action {
type = "Delete"
}
}
}
A simple age rule cannot express a policy such as “retain recent restore-tested monthly backups for longer.” A cleanup function and state store are justified only for object-specific decisions such as verification status, legal holds, or monthly and annual retention. In either design, the production VM service account should only create and read objects under its prefix; bucket administration and bulk deletion remain separate.
Restore as a Separate State Machine
Downloading a backup must not overwrite the live world. Verify its integrity and boot it from a quarantine path first.
stateDiagram-v2 accTitle: Minecraft Backup Quarantine Restore State Machine accDescr: A selected backup becomes a production replacement candidate only after its manifest, checksum, archive, file structure, and quarantined server boot have been verified. [*] --> SELECTED: select backup SELECTED --> MANIFEST_OK: check schema and format MANIFEST_OK --> CHECKSUM_OK: SHA-256 match CHECKSUM_OK --> EXTRACTED: extract into quarantine EXTRACTED --> STRUCTURE_OK: check required worlds and configuration STRUCTURE_OK --> BOOTED: start server on a separate port BOOTED --> VERIFIED: inspect protocol, world, and plugins VERIFIED --> CANDIDATE: production replacement candidate SELECTED --> FAILED: verification failed MANIFEST_OK --> FAILED CHECKSUM_OK --> FAILED EXTRACTED --> FAILED STRUCTURE_OK --> FAILED BOOTED --> FAILED
Run the restore on a different path and port from production, preferably on a temporary VM. Keep it off the public network and use the same Java and server versions. Check startup logs, Java protocol status, world seed and sample coordinates, player data, whitelist, and critical plugin state.
If production must be replaced, do not delete the current directory. Stop Minecraft cleanly, move the live directory to a rollback name, and atomically rename the verified restore into place on the same filesystem. Check ownership and SELinux or AppArmor policy before starting. If validation fails, quarantine the restored copy and rename the original directory back.
RPO and RTO calculated from actual restores
RPO is not the backup interval. It is the distance between the failure and the newest point that can actually be restored. Daily backups still yield a longer RPO if the three most recent archives fail integrity checks. RTO includes selection, download, verification, extraction, server startup, world validation, and cutover—not just file transfer.
flowchart LR accTitle: Backup retention and restore verification accDescr: A new archive moves to the verified prefix after checksum inspection. Only an archive that also passes a scheduled quarantine restore is considered recoverable. NEW[New archive] --> HASH[Checksum and manifest check] HASH -- Failure --> BAD[Quarantine and alert] HASH -- Success --> VERIFIED[Verified prefix] VERIFIED --> RESTORE[Scheduled quarantine restore] RESTORE -- Failure --> BAD RESTORE -- Success --> PROVEN[Proven recoverable] PROVEN --> LIFE[Retention policy]
Restore records should capture object generation, creation and verification times, server version, restore duration, test results, and operator. The useful operational question is not “Does a file exist?” but “Which archive was successfully opened by the real server software, and when?”
References
Share
No comments yet. Be the first to leave one.
Pending review