A backup is a promise to your future self, and the future self is rarely in a good mood when it collects. This guide makes the promise testable.
The short answer
A Minecraft server backup is a copy of the server directory, made while the world files are in a consistent state, stored on a second machine, and restored once as a test. On a running Java server the sequence is save-off, save-all flush, copy, save-on [1][2]. Send these commands over RCON from a script. Keep the RCON password out of the script, and make sure save-on runs even when the copy fails. Our assessment: the restore test is the part that turns a file into a backup, so do it before you need it. Everything below applies to your own worlds and servers.
How it works
- What to back up. The world lives in the folder named by
level-name, which isworldby default [3]. The Minecraft Wiki recommends copying the whole server directory, because you also getserver.properties, the ban lists and the other configuration [2]. We agree, with two exceptions that can be rebuilt: logs and crash reports. The layout inside the world changed with Minecraft 26.1. Before that, vanilla kept the Overworld in the world folder and the Nether and End inDIM-1andDIM1, and Paper kept them in separateworld_netherandworld_the_endfolders [18]. Since 26.1, all default dimensions sit underdimensions/minecraft/in the world folder, and player files moved to aplayersfolder withdata,statsandadvancementsinside [5][6]. Paper's per-world settings follow the same pattern, inworld/dimensions/<namespace>/<dimension>/paper-world.yml[8]. Copying the whole directory means you do not have to care which layout you run. Plugins with an external database need their own dump: a copy of the server folder does not contain that database. - Why a plain copy can break. The server writes terrain into region files, and a copy made in the middle of a write can contain half-old, half-new data [18]. The save commands exist for this.
save-offstops the server from writing to the level files so you can copy them, queues level changes meanwhile, and is reset when the server restarts. It does not cover player, advancement and statistics files [1].save-all flushwrites all players and chunks to disk immediately and freezes the server for a short time. The plainsave-allonly marks chunks as needing a save [1].save-onturns writing back on [1]. All three need permission level 4 and work on a dedicated server [1]. The sources disagree on the order. syntaxmine.com sayssave-off, thensave-all flush[18], nicompter.de runs the flush first [19]. We usesave-offfirst: after it, nothing new reaches the files, so the flush that follows writes the final state. That is our reasoning, not a sourced statement. Because player, advancement and statistics files stay outsidesave-off[1], a file can still change while you copy it, and that matters for step 6. Run the commands once by hand and watch the console before you trust the script: we found no source that says how soon an RCON reply comes back aftersave-all flush, so the script's order (flush, then copy) is our assumption. - Online or offline. Stopping the server with
/stopsaves the world, and then you simply copy the directory [2]. That is the simplest and safest way, and for a server that is allowed to sleep at night it is enough. For a server that runs around the clock, use the online sequence. The flush freezes the server briefly [1], so schedule it for a quiet hour. - RCON without leaking the password. RCON is the remote console. You enable it with
enable-rcon=true,rcon.portandrcon.passwordinserver.properties, and the default port is 25575 [3][4]. The Wiki warns that RCON is not encrypted, that the password can be intercepted on untrusted networks, and that you should ideally connect from localhost only [3]. Never forward the port to the internet.broadcast-rcon-to-ops(defaulttrue) sends command output to online operators, so every backup would show up in their chat unless you set it tofalse[3][4]. The command-line client mcrcon takes host, port and password as options, and it also readsMCRCON_HOST,MCRCON_PORTandMCRCON_PASSfrom the environment. Command-line options override the environment, and commands with spaces need quotes [9]. Use the environment. A password given with-pbecomes part of the process's command line, and the Linux documentation says that line is readable by everybody unless the system mounts/procwithhidepid[10]. Put the password into an environment file with mode 600, which systemd reads throughEnvironmentFile=[11]. The same value sits inserver.properties, so the backup archive contains the RCON password too. Protect the archive like a key: restrictive permissions locally (umask 077in the script) and an encrypted target offsite. - Make
save-onunavoidable. If the script dies betweensave-offandsave-on, the server keeps running without writing the world. The Wiki says the setting resets on restart [1], so a crash does not make it permanent, but a running server could queue changes for hours. Bash can run a command when the shell exits: atraponEXITruns the command before the shell terminates, including after an error [13]. A signal that cannot be trapped, such asSIGKILL, still escapes it, and so does a power cut. nicompter.de uses the same idea [19]. Our script also remembers whether it turned saving off, so it does not send a needlesssave-on. - Archive, check, copy away.
tarexits with 0 on success, 2 on a fatal error, and 1 when some files changed while they were archived [14]. Exit code 1 is therefore plausible here, because of the files thatsave-offdoes not cover. We treat 1 as a warning that the restore test must confirm, and 2 as a failure. Run tar undernice -n 19so the backup yields to the game [15]. We skipionice -c 3on purpose: the idle class only gets disk time when nobody else asks for it [16], and while saving is off the server queues changes in memory, so a stretched-out backup has a price. Then list the archive withtar -tzfto see that it can be read, write a SHA-256 checksum next to it and copy both off the machine. The syntaxmine.com guide puts it plainly: a copy on the same disk as the world is a second copy of the same risk [18].rsyncpushes over a remote shell to[USER@]HOST:DEST[17]. - Schedule it. A systemd timer starts a service of the same name by default.
OnCalendar=sets the time, andPersistent=trueruns a missed job after the machine was off [12]. For a service that runs a script and exits,Type=oneshotfits [20]. Check once a week that the newest archive is recent, because a backup that fails quietly looks the same as one that works. - Restore in three sizes. A full restore: stop the server, move the broken world folder aside instead of deleting it, because it may hold newer player data, unpack the backup so the folder sits where the old one was, and start. Announce the rollback window honestly [18]. A single player: stop the server, or make sure that player is offline [2], and copy only that player's file from the backup. It is named after the player's UUID:
players/data/<uuid>.datsince 26.1,playerdata/<uuid>.datbefore [5][6][18]. Look the UUID up inusercache.json[18]. Stats and advancements are separate files next to it [5]. An area: region files are namedr.X.Z.mcaand hold 32 by 32 chunks, which is 512 by 512 blocks. A chunk or block coordinate maps to its region by dividing by 32 or by 512 and rounding down [7]. Since the entity and point-of-interest data moved out of the region files, a region exists in theregion,entitiesandpoifolders of a dimension, and you restore all three for the sameX.Z[7]. Everything inside the region rolls back, chests included [18]. In our assessment, a structure that crosses the region border can end up cut off, so look at the border afterwards. - Restore test, as a duty. A restore you have not tried is a hope. Unpack the newest archive into a separate directory, change
server-portand turn RCON off in that copy'sserver.properties(otherwise it tries to take the same ports), start it, join, and check what you would miss: your base, a chest, your inventory, the plugin data. Write down the date and how long it took. A backup from an older Minecraft version restored on a newer server is upgraded by the newer server and cannot go back [18]. Mojang's notes for 26.1 Snapshot 6 say that worlds are upgraded to the new folder layout and can then no longer be loaded in an older version at all [6], so test an older backup on a copy first. This is the one step you must not skip.
A worked example (commands, not a test)
This is how a setup could look. The paths, host and file names are examples, and we did not run this against a real Minecraft server for this article. We only checked the control flow in a sandbox with a stand-in for mcrcon: when we simulated a failure at save-off, at the flush and at tar, save-on was still sent each time. Assume the server lives in /srv/minecraft, runs as the user minecraft, has enable-rcon=true on 127.0.0.1:25575, and mcrcon is installed.
First the environment file with the secrets, readable only by its owner:
sudo touch /etc/mc-backup.env
sudo chmod 600 /etc/mc-backup.env
sudoedit /etc/mc-backup.envIt contains two lines. The first is the value of rcon.password, the second is where the offsite copy goes, in the [USER@]HOST:DEST form:
MCRCON_PASS=the-value-of-rcon.password
OFFSITE=backup@backup-host.example:/srv/mc-backups/Then the script in /usr/local/sbin/mc-backup.sh, executable, owned by root and not writable for the server user:
#!/usr/bin/env bash
# Needs MCRCON_PASS and OFFSITE from the environment file. Never put the password in here.
set -euo pipefail
umask 077
SERVER_PARENT=/srv
SERVER_NAME=minecraft
DEST=/srv/backups/minecraft
: "${MCRCON_PASS:?MCRCON_PASS is not set}"
: "${OFFSITE:?OFFSITE is not set}"
export MCRCON_HOST=127.0.0.1 MCRCON_PORT=25575 MCRCON_PASS
NAME="mc-$(date +%F_%H-%M).tar.gz"
ARCHIVE="$DEST/$NAME"
saving_off=0
cleanup() {
if [ "$saving_off" = 1 ]; then
mcrcon save-on || echo "WARNING: run save-on by hand" >&2
fi
}
trap cleanup EXIT
mkdir -p "$DEST"
saving_off=1
mcrcon save-off
mcrcon "save-all flush"
status=0
nice -n 19 tar -C "$SERVER_PARENT" -czf "$ARCHIVE" \
--exclude="$SERVER_NAME/logs" --exclude="$SERVER_NAME/crash-reports" \
"$SERVER_NAME" || status=$?
mcrcon save-on
saving_off=0
if [ "$status" -gt 1 ]; then
echo "tar failed with exit code $status" >&2
exit "$status"
fi
if [ "$status" -eq 1 ]; then
echo "tar reports changed files; confirm with a restore test" >&2
fi
tar -tzf "$ARCHIVE" > /dev/null
( cd "$DEST" && sha256sum "$NAME" > "$NAME.sha256" )
rsync -a -- "$ARCHIVE" "$ARCHIVE.sha256" "$OFFSITE"
# Example value: keep two weeks locally. Choose it by how long damage could go unnoticed.
find "$DEST" -name 'mc-*.tar.gz*' -mtime +14 -deleteThe service and the timer, as /etc/systemd/system/mc-backup.service and /etc/systemd/system/mc-backup.timer:
[Unit]
Description=Minecraft world backup
[Service]
Type=oneshot
User=minecraft
EnvironmentFile=/etc/mc-backup.env
ExecStart=/usr/local/sbin/mc-backup.sh[Unit]
Description=Nightly Minecraft backup
[Timer]
OnCalendar=*-*-* 04:15:00
Persistent=true
[Install]
WantedBy=timers.targetEnable it, run it once by hand and read the result:
sudo systemctl daemon-reload
sudo systemctl enable --now mc-backup.timer
sudo systemctl start mc-backup.service
journalctl -u mc-backup.serviceThe restore test, with an example archive name:
mkdir -p /tmp/mc-restore-test
tar -xzf /srv/backups/minecraft/mc-2026-10-25_04-15.tar.gz -C /tmp/mc-restore-test
cd /tmp/mc-restore-test/minecraft
sed -i 's/^server-port=.*/server-port=25566/; s/^enable-rcon=.*/enable-rcon=false/' server.propertiesStart the server from that directory with your usual command, for example java -jar server.jar --nogui for the jar you run, and join on port 25566. If your plugins talk to a shared database or an external service, switch that off in the test copy too.
For one area: block (1500, -600) is chunk (93, -38) and therefore region (2, -2), by our calculation with the formula from the Wiki [7]. You would restore r.2.-2.mca from region, entities and poi of the right dimension, with the server stopped.
If you want versions, deduplication and encryption instead of plain archives, replace the tar step with a restic or Borg run. That is covered in the article on homelab backups with restic or Borg, and the offsite side in the article on offsite backups for your homelab. The 3-2-1 rule frames all of it.
Checklist
- Back up the whole server directory, not only the world folder, and leave out logs and crash reports.
- Run
save-off, thensave-all flush, copy, andsave-on, or stop the server and copy. - Send the commands over RCON bound to localhost, and never open the RCON port to the internet.
- Store the RCON password in a file with mode 600 and read it through the environment, never in the script or in
-p. - Add a
traponEXITsosave-onruns after a failed copy, and test it by making the copy fail once. - Check the tar exit code: treat 2 as failure and 1 as a warning that needs a restore test.
- List the archive with
tar -tzf, write a checksum and copy both to a second machine. - Protect the archive like a key, because it contains
server.propertieswith the RCON password. - Unpack the newest archive into a test directory, change
server-port, turn RCON off and join. - Write down the date, the archive name and how long the restore took, and repeat the test after every big change and regularly.
- Check weekly that the newest archive is recent and that the offsite copy arrived.