Skip to content

feat(backup): scheduled server backups to Depot - #15

Merged
BK1031 merged 1 commit into
mainfrom
bk1031/server-backups
Oct 1, 2026
Merged

BK1031 merged 1 commit into
mainfrom
bk1031/server-backups

Conversation

@BK1031

@BK1031 BK1031 commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

Backs the whole game server up on a configurable cron schedule and uploads it to the minecraft bucket in Depot.

How it works

The world lives on a ReadWriteOnce volume mounted only into the minecraft pod, so Warden cannot read it. The plugin does the archiving: Warden mints a presigned upload URL from Depot and hands it over the existing plugin WebSocket, and the .tar.gz goes from the game server straight to S3 — through neither Warden nor Depot.

Warden ──presign──> Depot ──URL──> Warden ──ws──> plugin ──PUT──> S3 ──> plugin ──> Warden ──complete──> Depot

Service

  • warden_backup_job tracks each run: pending → archiving → uploading → succeeded | failed | timed_out, with size, split archive/upload timings, and the Depot file id
  • warden_backup_schedule is a single editable row, so the cron can change without a redeploy. Default 0 4 * * * America/Los_Angeles
  • Cron is parsed with robfig/cron/v3 and the zone is bound into the expression, so 4am stays 4am across a DST shift
  • One backup at a time, enforced by a pg_advisory_xact_lock around the check-and-insert plus a unique index on scheduled_for
  • A watchdog times out jobs the game server never reported the end of, and a boot reaper closes out anything a restart orphaned — otherwise one lost report blocks every future backup
  • Warden verifies the object with Depot rather than trusting the reported size

Plugin

  • Flushes every world and disables autosave for the length of the archive only, not the upload
  • Hand-rolled ustar + PAX tar writer so the jar stays dependency-free and unshaded (verified round-trip against bsdtar, Python tarfile, and a real S3 download — byte-identical, including >100-byte paths and UTF-8 names)
  • Refuses to start without roughly half the server's size free, and clears abandoned staging files so the volume cannot fill one dead backup at a time
  • gzip at BEST_SPEED: region files are already zlib-compressed internally, so the slow levels buy a couple of percent for several times the CPU, and that CPU competes with the tick loop

Portal

  • /backups is visible to everyone; "Back up now" and the schedule editor are MinecraftAdmins only
  • Next three runs are previewed as a timeline with live countdowns, computed server-side — a cron parser in the browser would eventually disagree with the scheduler, and a confidently wrong preview is worse than none
  • Download links are MinecraftAdmins only: a world archive holds every player's inventory and every sign on the map

Notifications

Discord and game chat get the warning, the start, and the finish. Note that notices posted to Discord do not reach game chat on their own — onDiscordMessage ignores bot and webhook messages, which is what stops every relayed game line echoing back. So announcements are pushed to both explicitly, with emoji and markdown transliterated for Minecraft's font.

Scheduled runs warn at T-5m; "Back up now" warns at T-10s.

Also

The plugin socket no longer requires Discord to be configured. It is now a control channel, not just a chat relay, so bridge.Start() always serves it and the Discord half is optional.

Verified

Ran a full cycle against the dev stack with a simulated game server: warning → backup_start → progress → real presigned PUT to gr-depot-prod-usw2 → Depot finalize → completion notice with the size Depot reported.

Three test files are now in the minecraft bucket (one 3 MB, two 121 B) under backups/_pipeline-test/ and backups/2026/10/. Depot files are append-only so I could not remove them.

Notes

  • Depot's PRESIGN_EXPIRY is 15m. S3 checks X-Amz-Expires against the request start, not its completion, so a long upload that begins in time is fine — but a game server that cannot start within 15 minutes of the command will fail.
  • Depot returns http:// in the download URL it builds from the request; the client rejoins the path onto the configured https:// origin rather than trusting it.
  • The minecraft PVC is 30Gi. Staging needs roughly half the world's size free, which is the practical ceiling on how large the world can get before backups start refusing.

Archives the whole game server on a cron schedule and uploads it straight
to the minecraft bucket in Depot.

The world lives on a ReadWriteOnce volume mounted only into the game
server pod, so the plugin is the only party that can read it. Warden
mints a presigned upload URL from Depot and hands it over; the archive
goes from the game server to object storage directly, through neither
Warden nor Depot.

Players are warned five minutes before a scheduled backup, ten seconds
before a manual one, and again when it starts and finishes. Notices go to
Discord and to game chat separately, because the Discord relay ignores
bot messages by design.

The plugin socket no longer depends on Discord being configured: it now
carries Warden's commands as well as chat.
@BK1031
BK1031 merged commit 9dbe907 into main Oct 1, 2026
11 checks passed
@BK1031
BK1031 deleted the bk1031/server-backups branch October 1, 2026 07:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant