Back Up a Minecraft Server to DigitalOcean Spaces
Create consistent, encrypted-in-transit Minecraft backups, upload them to a private DigitalOcean Spaces bucket, and prove you can restore one.
Decide what a successful backup means before you automate it
A backup is only useful if it restores the world your players expect. For a typical Java server, that usually means the world directories, configuration, whitelist, operators list, plugins or mods, and the server version or container definition needed to run them. Write down where those live before you write a script; many hosts place the world outside the directory that contains server.jar.
This guide targets a Linux-hosted Java server and uses the AWS CLI because Spaces accepts S3-compatible requests. It does not make a live copy of a database or replace a tested disaster-recovery plan. If your server uses a panel, Docker volume, or a modpack with its own data directories, add those paths deliberately and test the result on a separate machine or directory.
- A private Spaces bucket in a region you have chosen intentionally.
- A dedicated limited Spaces access key with Read/Write/Delete access to that one backup bucket for scheduled backups and restore tests. Use a separate full-access key only while configuring versioning or lifecycle rules.
- The AWS CLI, tar, and enough local disk space to create one archive.
- A systemd unit whose stop action safely saves and shuts down Minecraft.
Create a private bucket and a key just for the backup job
Set up the storage boundary before putting any credential on the game server. Keep the bucket private: do not enable public file listing or use the CDN for backups.
- In DigitalOcean, open Spaces Object Storage and create a bucket such as minecraft-backups-your-server. Choose the region carefully: bucket names are globally unique, the bucket region cannot be changed later, and the regional endpoint is part of every CLI command.
- Open the Spaces Access Keys tab and create a limited key for this bucket only with Read/Write/Delete permission. Save its secret when it appears; the control panel displays it only once.
- Store that limited key only on the game server with the command below. It can upload, download, list, and delete backups, and can be rotated or revoked without affecting another application.
- Keep a separate full-access key on an administrator workstation for the one-time versioning and lifecycle configuration in the next steps. Do not add an S3 bucket policy: limited keys and bucket policies cannot be used together in this setup.
sudo install -d -o minecraft -g minecraft -m 700 /etc/minecraft-backupsudo install -d -o minecraft -g minecraft -m 700 /var/backups/minecraftsudo -u minecraft nano /etc/minecraft-backup/spaces.envSave the two credential variables, exit the editor, then continue.
- Replace every highlighted value before running this command.
AWS_ACCESS_KEY_ID=your_spaces_key AWS_SECRET_ACCESS_KEY=your_spaces_secret sudo chmod 600 /etc/minecraft-backup/spaces.env
Enable versioning before the first upload
Versioning is an extra recovery layer, not a substitute for retention. With it enabled, an overwrite or ordinary delete creates history you can inspect and restore. Once enabled, a bucket cannot return to an unversioned state; it can only be suspended. Decide on noncurrent-version retention before enabling it. DigitalOcean requires the regional endpoint here: use nyc3.digitaloceanspaces.com, for example, not the bucket origin endpoint that includes your bucket name.
Run this one-time configuration from an administrator workstation where a full-access Spaces key is already configured for the AWS CLI. Set AWS_DEFAULT_REGION to us-east-1 for the CLI’s required client-side setting; the Spaces endpoint, not that value, selects the actual bucket region. Replace the placeholders below with your bucket and Spaces region.
export AWS_DEFAULT_REGION=us-east-1- Replace every highlighted value before running this command.
aws s3api put-bucket-versioning --bucket YOUR_BUCKET --endpoint-url https://YOUR_REGION.digitaloceanspaces.com --versioning-configuration Status=Enabled - Replace every highlighted value before running this command.
aws s3api get-bucket-versioning --bucket YOUR_BUCKET --endpoint-url https://YOUR_REGION.digitaloceanspaces.comConfirm that the returned status is Enabled before continuing.
Archive a consistent world, then upload it
This path uses an existing systemd-managed server and a planned interruption. Set SERVICE to your actual unit and confirm that systemctl stop performs a graceful Minecraft shutdown. Read its journal and wait for saving to finish in a manual rehearsal before scheduling. The script runs as root, stops the service, archives it, then restarts it only if it was running.
Set SERVER_DIR and WORLD_NAME to the installation and level-name from server.properties. The include list covers existing world, configuration, plugin and mod directories. World save-off controls alone do not freeze arbitrary plugin files or external databases. Back up external plugin databases separately using their supported procedure.
#!/usr/bin/env bash
set -euo pipefail
PATH=/usr/local/bin:/usr/bin:/bin
export PATH
SERVER_DIR=/srv/minecraft
WORLD_NAME=world # Match level-name in server.properties.
BACKUP_DIR=/var/backups/minecraft
BUCKET=YOUR_BUCKET
REGION=YOUR_REGION
PREFIX=java-server
SERVICE=minecraft.service # Replace with your systemd unit.
was_running=false
resume_server() {
if [[ "$was_running" == true ]]; then systemctl start "$SERVICE"; fi
}
trap resume_server EXIT
set -a
. /etc/minecraft-backup/spaces.env
set +a
export AWS_DEFAULT_REGION=us-east-1
mkdir -p "$BACKUP_DIR"
STAMP=$(date -u +%Y-%m-%dT%H-%M-%SZ)
ARCHIVE="$BACKUP_DIR/minecraft-$STAMP.tar.gz"
[[ -d "$SERVER_DIR/$WORLD_NAME" ]] || {
echo "Set SERVER_DIR and WORLD_NAME before running this backup." >&2
exit 1
}
[[ -f "$SERVER_DIR/server.properties" ]] || {
echo "server.properties is missing from SERVER_DIR." >&2
exit 1
}
INCLUDE=("$WORLD_NAME" server.properties)
for path in \
"${WORLD_NAME}_nether" "${WORLD_NAME}_the_end" \
whitelist.json ops.json banned-players.json banned-ips.json plugins mods config; do
[[ -e "$SERVER_DIR/$path" ]] && INCLUDE+=("$path")
done
systemctl cat "$SERVICE" > /dev/null
if systemctl is-active --quiet "$SERVICE"; then
was_running=true
systemctl stop "$SERVICE"
fi
state=$(systemctl show -p ActiveState --value "$SERVICE")
[[ "$state" == inactive ]] || { echo "Server is not fully stopped: $state" >&2; exit 1; }
tar -C "$SERVER_DIR" -czf "$ARCHIVE" \
--exclude=logs --exclude=cache --exclude=backups \
"${INCLUDE[@]}"
resume_server
was_running=false
trap - EXIT
aws s3 cp "$ARCHIVE" "s3://$BUCKET/$PREFIX/$(basename "$ARCHIVE")" \
--endpoint-url "https://$REGION.digitaloceanspaces.com" --only-show-errors
rm -f "$ARCHIVE"sudo chown root:minecraft /usr/local/sbin/backup-minecraft-to-spacessudo chmod 750 /usr/local/sbin/backup-minecraft-to-spaces
Schedule it and give failures somewhere to go
Run the script manually as root and verify the archive in Spaces before scheduling. Create /var/log/minecraft-backup.log with sudo install -o root -g root -m 600 /dev/null /var/log/minecraft-backup.log. Save the cron block in /etc/cron.d/minecraft-backup, owned by root with mode 644 and a final newline. It uses server local time. Arrange an alert for a nonzero exit or a missing daily archive.
A daily backup is a reasonable starting point for a quiet personal server, but it is not a universal answer. Choose frequency from your acceptable data loss. If losing an evening of building is unacceptable, back up more often and balance that against archive size, upload time, and retention cost.
# /etc/cron.d/minecraft-backup SHELL=/bin/bash PATH=/usr/local/bin:/usr/bin:/bin 17 4 * * * root /usr/local/sbin/backup-minecraft-to-spaces >> /var/log/minecraft-backup.log 2>&1
Apply retention without deleting your only recovery point
Spaces lifecycle rules can expire objects after a chosen number of days and remove incomplete multipart uploads. Set a period that matches your recovery needs, then confirm what it covers before turning it on. A simple 30-day rule is easy to explain, but it may be too short for a world where damage is noticed weeks later.
Versioning changes the deletion story: deleting an object can leave prior versions and delete markers. A current-object expiry alone can leave noncurrent versions indefinitely. Review versioning and lifecycle behavior together, especially before relying on automatic cleanup. Keep at least one independently tested recovery path for a world you cannot afford to lose.
The command below replaces the bucket’s complete lifecycle configuration. Use a dedicated backup bucket, or first retrieve and merge any rules the bucket already has; do not paste it unchanged into a shared bucket.
Before continuing: This replaces the bucket lifecycle configuration and can expire recoverable versions. Review the bucket and existing rules first.
{ "Rules": [ { "ID": "expire-minecraft-backups", "Filter": { "Prefix": "java-server/" }, "Status": "Enabled", "Expiration": { "Days": 30 }, "NoncurrentVersionExpiration": { "NoncurrentDays": 30 }, "AbortIncompleteMultipartUpload": { "DaysAfterInitiation": 1 } } ] }Before continuing: This command replaces the bucket’s complete lifecycle configuration. Verify the bucket, lifecycle.json, and existing rules before running it.
Replace every highlighted value before running this command.aws s3api put-bucket-lifecycle-configuration --bucket YOUR_BUCKET --endpoint-url https://YOUR_REGION.digitaloceanspaces.com --lifecycle-configuration file://lifecycle.jsonRetrieve the configuration afterward and verify its prefix and retention.
Prove a restore works before the emergency
Pick a recent archive and restore it into an empty test directory. Check that the archive contains the expected world and configuration files before you point a Minecraft process at it. For a stronger test, start an isolated copy on a different port with the same server version and let an administrator join it.
Do not restore over the production directory while the production server is running. Stop the server, keep the damaged directory until the restored world has been verified, and then swap directories in a planned maintenance window.
set -a && . /etc/minecraft-backup/spaces.env && set +a && export AWS_DEFAULT_REGION=us-east-1restore_dir=$(mktemp -d /tmp/minecraft-restore-test.XXXXXX)- Replace every highlighted value before running this command.
archive="$restore_dir/minecraft-YYYY-MM-DDTHH-MM-SSZ.tar.gz" - Replace every highlighted value before running this command.
aws s3 cp "s3://YOUR_BUCKET/java-server/minecraft-YYYY-MM-DDTHH-MM-SSZ.tar.gz" "$archive" --endpoint-url "https://YOUR_REGION.digitaloceanspaces.com" tar -tzf "$archive" | sed -n '1,40p'The listing contains the expected world and configuration files.
tar -xzf "$archive" -C "$restore_dir"The archive extracts into the new temporary directory without touching production.
Check your result
- Expected result
- The remote archive exists under the intended private bucket prefix, and the restored test directory contains the expected world and configuration files.
- Stop if
- Stop if the archive is missing, the target is the production server directory, the bucket is public, or the archive contents are not what the server needs.
- Next step
- Start the restored copy on a different port when practical and verify an administrator can join before relying on the backup schedule.