Minecraft Linux Server Auto-Update

Every time Mojang ships a new Bedrock build, your server goes stale. Clients update themselves, refuse to connect to an older server, and you are back to SSH, hunting down a zip, and copying files over your install while trying not to touch your worlds folder.

This script does that for you. It checks the official download API, does nothing at all if you are already current, and if you are not, it swaps in the new server files while leaving your worlds and your configuration alone. Bedrock Dedicated Server on Linux only, not Java Edition.

What the script does

On every run it:

  1. Asks the official Minecraft download API for the current Linux Bedrock build.
  2. Compares that version against a small version file inside your server folder.
  3. Exits immediately if they match. Nothing is downloaded, nothing is touched.
  4. Otherwise downloads the zip to a temp folder, tests it for corruption, and confirms the server binary is in there.
  5. Copies the new files over your install with rsync, skipping your configs and your worlds.
  6. Records the new version number and cleans up.

What it does not do:

  • It does not restart your server. The files get replaced, but the running process keeps serving the old version until it restarts. There is a section on this below.
  • It does not back up your worlds. It never touches them, but take backups anyway.
  • It does not remove files that disappeared from newer releases.

Run it as the user that owns the server files. Nothing here needs root.

Step 1: Install Dependencies

sudo apt update
sudo apt install curl jq unzip rsync wget

Step 2: Create the Update Script

Save the following as ~/update-bedrock.sh:

#!/usr/bin/env bash
set -euo pipefail

HOME_DIR="$HOME"
INSTALL_DIR="$HOME_DIR/bedrock-server"
TMP_DIR="$HOME_DIR/bedrock-tmp"
API_URL="https://net-secondary.web.minecraft-services.net/api/v1.0/download/links"
ZIP_NAME="bedrock.zip"

command -v jq >/dev/null || { echo "jq is required"; exit 1; }

echo "Fetching version info..."
DATA=$(curl -fsSL "$API_URL")
DOWNLOAD_URL=$(echo "$DATA" | jq -r '.result.links[] | select(.downloadType == "serverBedrockLinux") | .downloadUrl')
VERSION=$(basename "$DOWNLOAD_URL" | grep -o '[0-9.]\+')

[[ -z "$DOWNLOAD_URL" || -z "$VERSION" ]] && { echo "Failed to extract URL/version"; exit 1; }

[[ -f "$INSTALL_DIR/bedrock-server.version" && "$(cat "$INSTALL_DIR/bedrock-server.version")" == "$VERSION" ]] && {
  echo "Already up to date"
  exit 0
}

mkdir -p "$TMP_DIR"
wget -q -O "$TMP_DIR/$ZIP_NAME" "$DOWNLOAD_URL"
unzip -t "$TMP_DIR/$ZIP_NAME" >/dev/null || { echo "ZIP test failed"; exit 1; }
unzip -oq "$TMP_DIR/$ZIP_NAME" -d "$TMP_DIR/unpacked"

[[ -x "$TMP_DIR/unpacked/bedrock_server" ]] || { echo "Missing server binary"; exit 1; }

rsync -a \
  --exclude='autoupdate.sh' \
  --exclude='server.properties' \
  --exclude='permissions.json' \
  --exclude='whitelist.json' \
  --exclude='ops.json' \
  --exclude='allowlist.json' \
  --exclude='valid_known_packs.json' \
  --exclude='worlds/' \
  "$TMP_DIR/unpacked/" "$INSTALL_DIR/"

echo "$VERSION" > "$INSTALL_DIR/bedrock-server.version"
rm -rf "$TMP_DIR"
echo "Updated to version $VERSION"

How it works

The API returns a plain JSON list of the current download links, one per platform. The jq filter picks the entry tagged serverBedrockLinux. Swap that string for serverBedrockPreviewLinux if you want to follow preview builds, and keep preview builds away from worlds you care about. This is why the script does not scrape the download page. The old scraping scripts all broke when bot protection went up in front of it, and the API does not care what your user agent is.

bedrock-server.version is a one line file the script writes into your server folder. It is not part of the official zip and the server never reads it. It is the script’s only memory, and it is what makes a daily cron job cheap: on a server that is already current, the entire run is one HTTPS request and an exit. Delete that file to force a clean reinstall of the current version.

Everything downloads into a temp folder, never into the live server folder. unzip -t tests the archive before a single file is copied, so a truncated download or a full disk dies there instead of halfway through overwriting your server binary.

Then rsync copies the new files in, and the excludes are what keep your server yours:

  • server.properties is your port, difficulty, gamemode, view distance, and everything else you tuned. The zip ships a default copy that would flatten it.
  • permissions.json and ops.json are your operator lists.
  • allowlist.json is the allowlist, and whitelist.json is what it used to be called. Both are excluded, so this works on old installs and new ones.
  • valid_known_packs.json lists the packs the server knows about. The server rewrites this file itself. If a release ever makes it complain about packs, delete the file and let it rebuild.
  • worlds/ is your saves. The one that matters.

There is no --delete, so files that vanish from newer releases just stay behind. That wastes a little disk and it avoids the entire class of accident where one wrong exclude pattern eats a world.

One quirk worth knowing: the character class in grep -o '[0-9.]\+' also matches the dot in front of zip, so the version comes out as 1.26.33.2 with a trailing dot on the end. Nothing breaks, because the same expression writes the version file and reads it back. If it bothers you in the log, use grep -oE '[0-9]+(\.[0-9]+)+' instead.

Step 3: Make It Executable

chmod +x ~/update-bedrock.sh

Run it once by hand before you trust it to cron. On a clean box it doubles as the installer: the server folder does not exist yet, so rsync creates it and you never have to download the zip yourself.

Step 4: Automate with Cron (Optional)

Edit your crontab with crontab -e and add this line to check daily at 3 AM:

0 3 * * * ~/update-bedrock.sh >> ~/bedrock-update.log 2>&1

Put that in the crontab of the user that owns the server, not root. The script builds every path from $HOME, and root’s home is /root.

Restart the server after an update

This is the part that catches people out. rsync renames each new file into place, so replacing files under a live process does not crash it, but the running server keeps its old binary open. It carries on serving the old version, players still get told the server is out of date, and the script has already cheerfully printed Updated.

The update is not finished until the server process restarts.

Assuming your server runs as a systemd service called bedrock, add this just above the rsync block:

SERVICE="bedrock"
RESTART=0
if systemctl is-active --quiet "$SERVICE"; then
  echo "Stopping $SERVICE..."
  sudo systemctl stop "$SERVICE"
  RESTART=1
fi

And this at the very end, after the version file is written:

if [[ "$RESTART" == 1 ]]; then
  echo "Starting $SERVICE..."
  sudo systemctl start "$SERVICE"
fi

Because the script already exited earlier when there was nothing to do, this only ever fires on a real update. Your server is not bounced nightly for no reason.

For cron to run those two commands without a password, add a narrow sudo rule with sudo visudo -f /etc/sudoers.d/bedrock-update, using your own username:

minecraft ALL=(root) NOPASSWD: /usr/bin/systemctl stop bedrock, /usr/bin/systemctl start bedrock

The server is stopped at that point anyway, which makes it the right moment for a world backup. Drop this in right after the stop:

tar -czf "$HOME_DIR/worlds-$(date +%F).tar.gz" -C "$INSTALL_DIR" worlds

Checking it worked

What your install thinks it is running:

cat ~/bedrock-server/bedrock-server.version

What is actually current:

curl -fsSL https://net-secondary.web.minecraft-services.net/api/v1.0/download/links \
  | jq -r '.result.links[] | select(.downloadType=="serverBedrockLinux") | .downloadUrl'

Troubleshooting

Failed to extract URL/version. The API is unreachable, or its JSON changed shape. Run the curl above by hand and look at what comes back. Your install has not been touched.

ZIP test failed. Truncated download or a full disk. Check df -h and run it again.

It says Updated but players still see the old version. The server was never restarted.

Permission denied during the rsync. You ran the script as the wrong user. If you ran it once as root, fix the ownership with sudo chown -R minecraft:minecraft ~/bedrock-server.

Cron never runs it. Check that the script is executable and that crontab -l shows the line under the right account. An empty log usually means the crontab belongs to another user.

Done

Your server now spots new releases on its own, refuses to touch anything if the download looks wrong, keeps your worlds and settings across every update, and comes back up on the new version without you logging in. Check the log now and then to make sure it is still saying what you expect.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *