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.
This backup uses a planned interruption: gracefully stop the Minecraft service, archive its world and recovery files, restart it if it was running, then upload to a private bucket and test the restore.
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.
# Replace minecraft with the account that runs the server and scheduled job.
sudo install -d -o minecraft -g minecraft -m 700 /etc/minecraft-backup
sudo install -d -o minecraft -g minecraft -m 700 /var/backups/minecraft
sudo -u minecraft nano /etc/minecraft-backup/spaces.env
# Add these two lines, then save:
# AWS_ACCESS_KEY_ID=your_spaces_key
# AWS_SECRET_ACCESS_KEY=your_spaces_secret
sudo chmod 600 /etc/minecraft-backup/spaces.env
Storage controls: Spaces key scopes, versioning and regional endpoints, lifecycle configuration. Keep the full-access administrator key on the workstation; never source the game server's limited-key file for bucket configuration.
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.
# Run this on the administrator workstation, using its already configured
# full-access Spaces key. Do not source the game server’s limited-key file.
export AWS_DEFAULT_REGION=us-east-1
aws s3api put-bucket-versioning --bucket YOUR_BUCKET \
--endpoint-url https://YOUR_REGION.digitaloceanspaces.com \
--versioning-configuration Status=Enabled
aws s3api get-bucket-versioning --bucket YOUR_BUCKET \
--endpoint-url https://YOUR_REGION.digitaloceanspaces.com
Before the first backup, create recovery-kit/ inside SERVER_DIR. Put the exact server distribution there, including the server JAR, loader/libraries and launcher files required by your modpack. Add README.txt with the Minecraft/server/modpack versions, Java vendor and version, original download URLs, launch command and any data outside SERVER_DIR. Generate SHA256SUMS for the actual artifacts. A version name or a link to ‘latest’ is not enough. Do not copy credentials or caches into this kit.
For a plain server whose executable really is server.jar, this is the minimum example; a modded installation must add its complete dependencies before continuing:
cd /srv/minecraft
sudo install -d -m 700 recovery-kit
sudo cp -- server.jar recovery-kit/server.jar
sudo sh -c 'cd /srv/minecraft/recovery-kit && sha256sum server.jar > SHA256SUMS'
sudo nano recovery-kit/README.txt
Add the kit to the script's required archive inputs as shown in the script below. On the isolated restore host, run sha256sum -c SHA256SUMS inside the restored kit, install the recorded Java runtime, rebuild the documented launcher path, and load the world with these exact artifacts. Do not silently download a newer server during recovery.
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
}
[[ -s "$SERVER_DIR/recovery-kit/README.txt" && -s "$SERVER_DIR/recovery-kit/SHA256SUMS" ]] || {
echo "Complete recovery-kit/README.txt and SHA256SUMS before backing up." >&2
exit 1
}
(cd "$SERVER_DIR/recovery-kit" && sha256sum -c SHA256SUMS)
INCLUDE=("$WORLD_NAME" server.properties recovery-kit)
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"
# After saving the script:
sudo chown root:root /usr/local/sbin/backup-minecraft-to-spaces
sudo chmod 700 /usr/local/sbin/backup-minecraft-to-spaces
sudo /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
A log file cannot alert you when the game server is offline. Run the read-only check below on an independent monitor every hour, using a separate bucket-limited Read key. For the daily 04:17 job, the example threshold is 30 hours (MAX_AGE_SECONDS=108000); choose another threshold if your recovery objective differs. Connect a nonzero exit to an alert route you already receive, and alert separately if the checker itself stops reporting. Test an empty prefix, a deliberately too-low threshold and an invalid key before relying on it. This detects a missing, stale or unreadable nonempty object; it does not prove world consistency or successful restoration.
Save as check-minecraft-backup-freshness.sh, set BUCKET and REGION in that monitor's protected environment, and run it with Bash:
#!/usr/bin/env bash
# Run on the independent monitor with a read-only Spaces key already configured.
set -euo pipefail
: "${BUCKET:?Set the dedicated Minecraft backup bucket}"
: "${REGION:?Set the Spaces region}"
export AWS_DEFAULT_REGION=us-east-1
export AWS_PAGER=""
PREFIX=${PREFIX:-java-server}
MAX_AGE_SECONDS=${MAX_AGE_SECONDS:-108000}
export MAX_AGE_SECONDS
listing=$(mktemp)
trap 'rm -f "$listing"' EXIT
aws s3api list-objects-v2 --bucket "$BUCKET" --prefix "$PREFIX/minecraft-" \
--endpoint-url "https://$REGION.digitaloceanspaces.com" --output json > "$listing"
python3 - "$listing" <<'PY'
import datetime, json, os, sys
objects = json.load(open(sys.argv[1])).get('Contents', [])
archives = [x for x in objects if x.get('Key', '').endswith('.tar.gz') and x.get('Size', 0) > 0]
if not archives:
sys.exit('CRITICAL: no nonempty Minecraft backup archive found')
def timestamp(obj):
return datetime.datetime.fromisoformat(obj['LastModified'].replace('Z', '+00:00'))
latest = max(archives, key=timestamp)
age = (datetime.datetime.now(datetime.timezone.utc) - timestamp(latest)).total_seconds()
if age < -300 or age > int(os.environ['MAX_AGE_SECONDS']):
sys.exit(f'CRITICAL: newest Minecraft archive age is {age / 3600:.1f} hours')
print(f'OK: newest nonempty Minecraft archive is {age / 3600:.1f} hours old')
PY
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.
# Run this from an administrator workstation with a full-access key.
export AWS_DEFAULT_REGION=us-east-1
cat > lifecycle.json <<'JSON'
{
"Rules": [
{
"ID": "expire-java-server-backups",
"Filter": { "Prefix": "java-server/" },
"Status": "Enabled",
"Expiration": { "Days": 30 },
"NoncurrentVersionExpiration": { "NoncurrentDays": 30 },
"AbortIncompleteMultipartUpload": { "DaysAfterInitiation": 1 }
},
{
"ID": "remove-expired-java-server-delete-markers",
"Filter": { "Prefix": "java-server/" },
"Status": "Enabled",
"Expiration": { "ExpiredObjectDeleteMarker": true }
}
]
}
JSON
aws s3api put-bucket-lifecycle-configuration \
--bucket YOUR_BUCKET \
--endpoint-url https://YOUR_REGION.digitaloceanspaces.com \
--lifecycle-configuration file://lifecycle.json
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-1
restore_dir=$(mktemp -d /tmp/minecraft-restore-test.XXXXXX)
archive="$restore_dir/minecraft-YYYY-MM-DDTHH-MM-SSZ.tar.gz"
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'
tar -xzf "$archive" -C "$restore_dir"
(cd "$restore_dir/recovery-kit" && sha256sum -c SHA256SUMS)
Frequently asked questions
Should a Minecraft backup bucket be public?
No. Backups can contain player data, configuration, and server secrets. Keep the bucket private and use a dedicated limited access key for the backup job.
Can I back up a running Minecraft server?
The script in this guide does not copy a running server. It uses a planned graceful stop and verifies that the systemd service is inactive before archiving. A save-off/flush-only procedure would need separate checks for every plugin, mod and external database writer.
Does Spaces versioning replace backups?
No. It protects against some overwrites and deletes within the bucket. You still need retention rules, a restore test, and—when the world is critical—an independent recovery copy outside the same storage system.