Skip to content

Latest commit

ย 

History

45 Commits

Folders and files

NameName
Last commit message
Last commit date
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

Self-Hosted Minecraft Bedrock Server

A self-hosted Bedrock Dedicated Server in Docker, exposed to the internet through a playit.gg tunnel (no port-forwarding, works for consoles), wrapped in a batteries-included operations layer:

  • ๐Ÿ“ฒ Notifications โ€” phone push (ntfy) on every join/leave; optional macOS desktop alerts
  • ๐Ÿ“ˆ Monitoring โ€” crash / tunnel-down / high-CPU-mem alerts, a daily digest, and a 4-panel uptime graph
  • ๐Ÿ’ฌ Two-way Discord chat โ€” in-game chat appears in #in-game-chat, and Discord messages appear in-game
  • ๐ŸŽฎ Commands anywhere โ€” !cmd in-game and /cmd (or !cmd) in Discord share one rate-limited surface
  • ๐Ÿ—บ๏ธ World map โ€” render the overworld to a hill-shaded PNG and post it to #map
  • ๐Ÿ—„๏ธ Backups โ€” consistent world snapshots, self-rotating, with one command
  • ๐Ÿฉบ Health check โ€” just doctor (also !doctor / /doctor) for an at-a-glance status report
  • โ™ป๏ธ Self-healing โ€” a watchdog auto-restarts the server if it goes unhealthy; !restart / /restart <reason> forces it by hand any time (warns online players first, logs who & why, rate-limited per person)
  • ๐Ÿ›ก๏ธ Safety guards โ€” nothing restarts/stops the server while a player is online
  • ๐Ÿง™ just setup โ€” an interactive wizard that gets a fresh clone running

Runs on an Apple-Silicon Mac (the x86 Bedrock server runs under box64). The server stack is Docker; a few host-side helpers run as macOS launchd agents โ€” see Architecture.

Version: date-based (CalVer YYYY.MM.DD) โ€” current in VERSION, history in CHANGELOG.md.


Quick start

Requirements

Tool Needed for Install
Docker (+ daemon running) the server stack OrbStack or Docker Desktop
just the task runner brew install just
Go the just setup wizard only brew install go (or go.dev/dl)
python3 chat bot + uptime graph (optional features) brew install python3
terminal-notifier macOS desktop alerts (opt-in only) brew install terminal-notifier

Set it up

git clone <this-repo> && cd minecraft-server
just setup

just setup launches an interactive, idempotent wizard that:

  1. Checks your machine for the tools above and prints install hints for anything missing.
  2. Walks you through .env one prompt at a time (pre-filled from any existing .env โ€” accept to keep, or edit). Only playit's secret key is required; everything else is optional and skippable.
  3. Writes .env (creating it from .env.example on a fresh clone, or merging in place without clobbering existing values/comments).
  4. Offers to start the stack and install the background agents โ€” each step is safe to re-run, and anything already done is marked as such.

Re-run just setup any time; it never breaks what's already configured or running.

Don't want the wizard? cp .env.example .env, fill it in, and run docker compose up -d. See Manual setup.

Get the public address

just tunnel        # prints your playit address, e.g. <random>.ply.gg:12345

Give that host + port to your friends. See Connecting.


Architecture

Two homes, by design (see why below):

๐Ÿณ Docker containers (docker compose up -d starts all five)

Service Image Job
bedrock itzg/minecraft-bedrock-server the game server
playit playit-agent outbound tunnel โ†’ public address
monitor docker:cli reads logs/stats, fires health alerts, writes CSVs (mounts the docker socket read-only)
bridge curlimages/curl subscribes to the ntfy bus, fans events out to Discord webhooks
autoheal willfarrell/autoheal restarts bedrock if its healthcheck goes unhealthy (mounts the docker socket read-write โ€” must issue restart)

๐Ÿ–ฅ๏ธ Host launchd agents (macOS, start at login, auto-restart)

Agent Manage with Job
com.mcserver.notify just notify-* join/leave โ†’ phone push (+ opt-in desktop alert)
com.mcserver.uptime just uptime-* daily uptime graph โ†’ #monitoring
com.mcserver.chatbot just bot-* Discord #in-game-chat โ†’ in-game chat

Why some things run on the host (not in containers):

  • macOS desktop notifications can't be sent from a Linux container โ€” that's a host-only API.
  • The chat bot drives docker exec โ€ฆ send-command (read-write daemon control). A writable docker socket inside a container is root-equivalent on the host; running the controller outside what it controls is safer and avoids a lifecycle paradox.
  • Host agents keep observing/announcing even while the stack is down or being recreated.

(The monitor container is the exception that proves the rule: it only reads the daemon, so it mounts the socket read-only and is safely containerized. The autoheal container is the one deliberate read-write exception โ€” it does exactly one thing, docker restart an unhealthy container, which is why it's a tiny purpose-built image rather than something with shell access.)


Connecting

Addresses below are examples โ€” get yours with just tunnel.

PC (Minecraft for Windows โ€” Bedrock)

Play โ†’ Servers tab โ†’ Add Server โ†’ name it, enter the tunnel host + port โ†’ Save โ†’ Join.

PS5 / Xbox / Switch (via BedrockConnect)

Consoles have no "Add Server", so they use a DNS redirect into the featured-server list:

  1. Console network settings โ†’ set DNS to Manual:
    • Primary: 45.55.68.52 (Xbox/Switch can use 104.238.130.180)
    • Secondary: 8.8.8.8
  2. Launch Minecraft โ†’ Play โ†’ Servers โ†’ open any featured server โ†’ BedrockConnect loads.
  3. Connect to a Server โ†’ enter your tunnel host + port โ†’ check Add to server list โ†’ connect.
  4. When done, set console DNS back to Automatic.

Quick check: phone on mobile data (Wi-Fi off) โ†’ Add Server โ†’ tunnel host+port. If the phone connects, the full internet path works and consoles will too.


Everyday commands

just                 # list every recipe
just up              # start (guarded; announces return to #server-status with ๐ŸŸข)
just down "reason"   # stop (guarded; kicks players cleanly first; announces ๐Ÿ› ๏ธ)
just restart         # bounce bedrock only (keeps logs)
just status          # container status        just players   # who's online
just tunnel          # public address          just logs      # follow all logs
just backup          # consistent world snapshot (auto-rotates oldest)
just doctor          # one-shot health report   just map       # render the overworld โ†’ world-map.png
just tp Steve Alex   # teleport a player to a player or "x y z" โ€” mount-aware (horse/boat/โ€ฆ comes along)

Personal per-machine helpers can live in scripts/.local.just โ€” it's gitignored and auto-imported when present, so they show up in just --list without touching the shared repo.

Timestamps everywhere are UK time (TZ=Europe/London), matching Discord and the graphs. just up/down/restart prompt for a #server-status message (Enter for a default); the announcement is prefixed ๐ŸŸข (up) or ๐Ÿ› ๏ธ (down).


Features

๐Ÿ“ฒ Notifications (phone + opt-in desktop)

scripts/notify.sh (the notify agent) tails the server log and announces joins/leaves. The two output channels are independent:

  • Phone push via ntfy.sh โ€” the default. On whenever NTFY_TOPIC is set.
  • macOS desktop alert โ€” opt-in, off by default. Enable with NOTIFY_MACOS=1 in .env (uses terminal-notifier).
just notify install|uninstall|status      # manage the permanent agent
just notify test          # prove the pipeline without logging in

NTFY_TOPIC doubles as a pub/sub bus: publishers POST events, and subscribers (your phone's ntfy app, the bridge container โ†’ Discord) react.

๐Ÿ“ˆ Monitoring, alerts & Discord routing

The monitor container detects and publishes: server down/up, the box64 heap-corruption crash family (invalid next size / corrupted size vs / double free), tunnel-down (playit heartbeat silence), high CPU/memory, and a daily digest. Thresholds via .env (MONITOR_CPU_ALERT %, MONITOR_MEM_ALERT MiB, MONITOR_ALERT_COOLDOWN s).

The bridge routes each event to a per-channel Discord webhook by category:

Category Events Webhook (.env) โ†’ channel
player joins / leaves DISCORD_WEBHOOK_PLAYER โ†’ #player-activity
alert crash, up/down, tunnel, CPU/mem DISCORD_WEBHOOK_SERVER_STATUS โ†’ #server-status
monitor daily digest, uptime graph DISCORD_WEBHOOK_MONITOR โ†’ #monitoring
chat in-game chat relay DISCORD_WEBHOOK_CHAT โ†’ #in-game-chat

Any category left unset falls back to DISCORD_WEBHOOK_URL. Reload the bridge after editing webhooks (never touches bedrock): docker compose up -d --no-deps bridge.

๐Ÿ“Š Uptime graph

A 4-panel daily PNG (status strip ยท players ยท CPU ยท memory) in UK time โ†’ #monitoring. The status strip is driven by healthchecks.io flip history (so it reflects real player-reachability, not just "is the container up"). Needs HEALTHCHECK_URL, HEALTHCHECK_API_KEY, DISCORD_WEBHOOK_MONITOR.

just uptime               # post the last 24h now
just uptime-preview       # render locally WITHOUT posting
just uptime-agent install # schedule the daily 00:05 post (launchd)

The daily post lands at 00:05 Europe/London wherever the host machine is. launchd can't be told a timezone โ€” StartCalendarInterval fires on the Mac's local clock, and launchd fixes that offset when the job is bootstrapped rather than re-reading it when the machine's timezone changes. So the agent ticks every 15 minutes instead and scripts/uptime-tick.sh decides, in London time, whether the post is due. If the Mac is asleep at UK midnight the digest goes out on the first tick after it wakes rather than being skipped.

๐Ÿ’ฌ Two-way Discord โ†” in-game chat

  • In-game โ†’ Discord: a Bedrock behavior pack (bedrock-data/behavior_packs/chat-bridge) posts chat to #in-game-chat via DISCORD_WEBHOOK_CHAT. (Advanced: needs the world's "Beta APIs" experiment enabled + the pack added to the world โ€” already configured on the bundled world.)

  • Discord โ†’ in-game: the chat bot (com.mcserver.chatbot) listens on the Discord Gateway and injects messages with tellraw. Loop-safe: it only relays human messages from the one channel (ignores bots/webhooks), and tellraw never re-triggers the outbound relay.

    # .env: CHAT_BOT_TOKEN + IN_GAME_CHAT_CHANNEL_ID  (bot needs MESSAGE CONTENT INTENT)
    just bot run         # foreground test
    just bot install     # permanent agent     just bot logs|status|uninstall
  • From the console:

    just say  "dinner in 10"                       # broadcast to everyone (also โ†’ #in-game-chat)
    just say-raw "ยง6ยงlHeads up!ยงr ยง7dinner in 10"  # styled broadcast (ยง colour codes)
    just tell Steve "your base is on fire"         # private whisper (not relayed)

๐Ÿ—„๏ธ Backups

just backup            # flush + consistent snapshot (no player disconnect) โ†’ #backups
just backups           # list (๐Ÿ”’ = saved/exempt)     just restore <name>    # restore one
just backup-save <name> / backup-unsave <name>        # protect a snapshot from rotation

Rotation is automatic. After each just backup, the newest BACKUP_KEEP (.env, default 10) unsaved snapshots are kept and older ones pruned. Mark any snapshot with just backup-save <name> to keep it forever โ€” ๐Ÿ”’ saved snapshots are exempt from rotation and don't count toward the limit.

Players can also trigger a backup themselves via !backup (in-game or Discord), rate-limited to one per requester every BACKUP_COOLDOWN (default 10 min).

๐Ÿฉบ Health report

just doctor            # full report: containers, health, restart counts, CPU/mem, disk, webhooks
just doctor --brief    # one compact line (what the in-game !doctor replies with)
just doctor --discord  # digest-style Markdown report with a last-hour glance (what Discord !doctor / /doctor reply with)

Runnable from in-game (!doctor, brief one-liner) and Discord (!doctor or /doctor โ€” a digest-style report: traffic-light status, uptime, who's online, CPU/RAM sparklines for the last hour, joins/leaves, tunnel blips, command usage, crashes, disk and last backup).

Every chat-command use (in-game ! or Discord !//) is recorded to bedrock-data/monitoring/commands.csv (time,source,who,command) โ€” Discord uses are appended by the chat bot, in-game uses are scraped from the pack's CMDLOG log lines by the monitor. The Discord doctor shows the last hour's uses and the daily digest shows a per-person "who ran what, when" breakdown for the day.

โ™ป๏ธ Self-healing

The x86 Bedrock server runs under box64, which occasionally hits a heap-corruption crash. Sometimes the process dies but its wrapper keeps the container's PID 1 alive โ€” so the container stays running while the game is dead, and Docker's restart: unless-stopped policy (which only fires when a container exits) never kicks in. Two layers cover this:

  • Automatic โ€” the autoheal watchdog. bedrock's healthcheck pings the live game port; after ~90s of failed pings (3 misses) the container goes unhealthy and autoheal docker restarts it. A generous start_period (180s) keeps a slow world-load in starting, not unhealthy, so a normal boot is never restarted, and just down removes the container so the watchdog never fights maintenance.
  • Manual โ€” !restart <reason> / /restart (Discord). Forces a restart any time โ€” including a server that looks healthy but is wedged (e.g. players show in /players but can't get past the loading screen). It's safe by construction: when players are online it runs the in-game 60sโ†’10s countdown (and pings #server-status) before bouncing, it logs who asked and why to bedrock-data/restart.log, and a per-requester cooldown (RESTART_COOLDOWN, default 600s / 10m) stops one person bounce-looping. A reason is required on Discord. Discord-only by design: a crashed server has no in-game chat to type into.

๐ŸŽฎ Commands (in-game + Discord)

One command surface, two front-ends. In-game chat uses the ! prefix; Discord accepts the same ! prefix and native / slash commands (in #in-game-chat or #map). Because there's no clean Discordโ†”Minecraft identity mapping, player-specific commands take the target as an argument in Discord.

Command Does Notes
!commands (alias !help) list available commands
!players (alias !online) list who's online right now in-game reads the live player list; Discord queries the server
!coords [player] report a player's coordinates pass the target in Discord
!backup trigger a world snapshot rate-limited per requester (BACKUP_COOLDOWN)
!map render the overworld โ†’ #map rate-limited per requester (MAP_COOLDOWN)
!doctor health report in-game replies with the --brief one-liner; Discord gets a digest-style report with a last-hour glance
!restart <reason> restart the server (any time) Discord only โ€” reason required; warns online players (60s countdown) + logs who/why; rate-limited per requester (RESTART_COOLDOWN, default 10m)
!mail <player> <msg> leave in-game mail delivered on the recipient's next join
!sleep skip the night to morning in-game only โ€” forces the skip even when players are AFK; only works after dusk; also clears storms
!shrug ยฏ\_(ใƒ„)_/ยฏ

Slash equivalents: /commands (alias /help), /players (alias /online), /map, /backup, /doctor, /restart, /coords, /mail, /shrug.

!restart / /restart is Discord-only by design โ€” a crashed server has no in-game chat to type into, so the recovery command lives where it's reachable when the game is down. A reason is required and logged. The autoheal watchdog handles the automatic case; this is the manual override when the server is wedged but not yet flagged unhealthy.

The in-game ! commands live in the chat-bridge behavior pack and activate on the next server reload; the Discord side is served by the chatbot agent.

๐Ÿ—บ๏ธ World map

just map reads the live world's LevelDB (in a throwaway mc-tools container), extracts a surface heightmap, and renders a north-up, hill-shaded terrain PNG (world-map.png, gitignored โ€” it's a render of the real world). !map / /map post it directly to #map via DISCORD_WEBHOOK_MAP.

Live renders also overlay players: a green marker + gamertag + live coordinates for everyone currently online (positions queried from the running server), plus a red "last seen" marker for offline players at their saved position โ€” named via the gitignored gamertagโ†’ServerId map (unmapped players get an opaque short id, since the world DB stores no gamertags). Rendering a backup db skips the overlay.

just map                                   # render the live world
just map "bedrock-data/backups/<name>/db"  # render a specific backup (guaranteed-consistent)

๐Ÿ›ก๏ธ Safety guards

  1. Interactive guard (scripts/guard.sh) runs before up/down/restart/recreate. If players are online it lists them and makes you type the exact count to proceed. Bypass for automation: FORCE=1.
  2. Claude Code hook (.claude/) intercepts Claude's own disruptive commands and prompts you first.
  3. Optional shutdown countdown โ€” set SHUTDOWN_COUNTDOWN=60 (seconds) in .env and down/restart/recreate will warn online players in-game every 10s (60s, 50s, โ€ฆ 10s) before stopping. Off by default; skipped when nobody's online. One-off: SHUTDOWN_COUNTDOWN=60 just restart "patch incoming".

Read-only commands and monitor/bridge-only ops are never gated.


Manual setup (no wizard)

  1. cp .env.example .env and fill it in (only PLAYIT_SECRET_KEY is required). Get a playit key: sign up at playit.gg โ†’ New agent (Docker) โ†’ copy the secret (shown once).
  2. docker compose up -d โ€” then confirm the tunnel: docker compose logs -f playit (want secret key valid โ†’ agent registered โ†’ tunnel running).
  3. Create the tunnel at playit.gg/account/tunnels โ†’ New Tunnel โ†’ Minecraft Bedrock โ†’ Free Network โ†’ assign your agent โ†’ Origin: Local Address 172.20.0.10, Port 19132 (it defaults to 127.0.0.1, which is wrong โ€” the server is a separate container).
  4. Optional features: set the relevant .env vars and install the agents (just notify install, just uptime-agent install, just bot install).

The world persists in ./bedrock-data (a bind mount) and survives docker compose down. To wipe it: docker compose down && rm -rf ./bedrock-data.


Fun commands

just cmd "gamerule showcoordinates true"
just cmd "execute as @a at @s run setblock ~ ~5 ~ minecraft:diamond_block"

Note on crashes: the box64 heap-corruption crashes (free(): invalid next size, etc.) are an emulation artifact of running x86 Bedrock on Apple Silicon. Restarts clear them; the real cure is native x86 hosting. The monitor detects and reports them.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages