Compatibility
Minecraft: Java Edition
Platforms
Links
Tags
Creators
Details
PyRunner 🐍
Run your Python Discord bot directly alongside your Minecraft server — no external hosting, no VPS, no hassle.
PyRunner is a Paper plugin that manages a Python process (typically a Discord bot) as a child process of your Minecraft server. It handles everything automatically: downloading Python, installing dependencies from requirements.txt, restarting on crash, streaming logs to a live web dashboard, and sending Discord notifications — all configured from a single config.yml.
🆕 What's new in 1.2.0
Dashboard — Settings & Bot Management
The web dashboard has been completely overhauled. It is no longer just a log viewer — it now gives you full control over the plugin without ever touching config.yml or SSHing into the server.
New Settings tab — every config value is editable live from the browser:
- Process behaviour: auto-restart toggle, restart delay, max restarts, crash window, startup delay
- Python runtime: version pin and custom executable path
- Discord webhook: URL, enable/disable, and individual toggles for each event type (start, stop, crash, log lines)
- Dashboard: log history line count and password (changing the password immediately invalidates all active sessions)
New Bots tab — full bot instance management with TunnelMC-style modal dialogs:
- Add a new bot (name, entry file, env vars) — starts automatically
- Edit an existing bot — stops it, applies the new config, and restarts
- Delete a bot — stops it and removes it from
config.yml - All changes persist to disk immediately
Download logs — a new button on the sidebar downloads the current bot.log as a file attachment.
SSE push-based streaming — log lines are now pushed directly to the browser the moment they are written, rather than polled. This eliminates latency and CPU overhead.
Log counter fix — the line counter now reflects what is actually visible in the DOM (capped at 3000) rather than a stale incrementing integer.
switchBot() race fixed — historical logs are fully loaded before the SSE stream connects, preventing out-of-order or duplicate lines when switching bots.
Bug Fixes
-
failedAttemptsmap unbounded growth — swept hourly alongside the session map. -
JSON injection in status/bots API — bot names and entry file paths serialised with Gson instead of string concatenation.
-
setsidpath now covers Alpine Linux / Pterodactyl — checks/usr/bin/setsid,/bin/setsid, and/usr/local/bin/setsid. Previously process group killing silently did nothing on Alpine-based containers. -
FileReaderplatform charset —/proc/<pid>/statusread with explicit UTF-8. -
FileUtils.deleteDir()returnsboolean— failures logged per-file instead of silently swallowed. -
GET endpoints reject non-GET requests — 405 returned for wrong-method calls.
-
Log timestamps include the date —
MM-dd HH:mm:ssformat so logs spanning midnight are unambiguous. -
Python downloader fetches 20 releases — up from 5, so rare platform/version combos are found correctly.
-
Content-Security-Policy header added to all dashboard responses.
-
Gradle performance flags —
parallel,caching, andG1GCJVM args added togradle.properties. -
Fixed state race condition —
BotStateis now managed viaAtomicReferenceinstead of a plainvolatilefield, eliminating a class of bugs where background threads could write the state without memory visibility guarantees. -
Fixed
forceReinstall()not actually reinstalling — The command now passes--force-reinstallto pip, guaranteeing packages are freshly installed. Previously it only deleted the flag file and ran a normalpip install, which would skip already-installed packages. -
Fixed
restart()busy-wait — The polling loop has been replaced with aCountDownLatch, so restarts are more reliable and use zero CPU while waiting for the process to fully stop. -
Fixed GitHub API rate limiting causing confusing errors —
findReleaseAsset()now checks the HTTP status code before attempting to parse JSON. A 403 or 429 from GitHub now produces a clear "rate limit reached" message instead of a crypticJsonParseException. -
Fixed config values not validated — All numeric config getters (
restart-delay-seconds,max-restarts,crash-window-seconds,startup-delay-seconds) now clamp to valid ranges, preventingThread.sleep(-N)exceptions from negative values. -
Fixed log-history cap not enforced —
getDashboardLogHistory()is now capped at 500 (matchingLogManager's ring buffer size), so requesting more lines than the buffer holds no longer silently returns a truncated result without warning. -
Fixed static
instancenotvolatile— The singleton is now declaredvolatilefor safe cross-thread visibility, and is nulled out inonDisable()to prevent classloader leaks on plugin reload. -
Fixed Discord embed JSON escaping — Webhook embeds now use Gson's
toJson()for string serialisation instead of a manualreplace()chain, correctly escaping all Unicode control characters (\u0000–\u001F) that could cause Discord to reject payloads. -
Fixed malformed bot entries crashing plugin load — Each entry in the
bots:YAML list is now parsed inside a try-catch; a bad entry logs a warning and is skipped rather than throwing aClassCastExceptionthat aborts the whole enable sequence. -
Added default password warning — A prominent console warning is now printed at startup if the dashboard is enabled and the password is still set to
"changeme". -
Improved
getBotDir()fallback — Whenentry-filehas no directory component (e.g.main.pyinstead ofbot/main.py), the plugin now derives the directory from the actual file path and logs a descriptive warning, rather than silently assuming"bot". -
Fixed dashboard SSE exhausting the HTTP thread pool — The server now uses Java 21 virtual threads (
newVirtualThreadPerTaskExecutor) instead of a fixed 8-thread pool. Previously, 8+ simultaneous SSE log-stream connections could fill the pool and cause all other API requests to hang indefinitely. -
Fixed
DashboardServer.jsonString()unsafe escaping — Dashboard log API responses now useGSON.toJson()instead of a manualreplace()chain, correctly handling Unicode control characters (\u0000–\u001F) that could previously produce malformed JSON. -
Fixed memory cache non-atomic reads — Three separate
volatilefields replaced with a singlevolatile MemCacherecord swapped atomically, preventing threads from observing a new timestamp with stale memory values. -
Fixed SSE handler busy-polling —
while (!heartbeatFuture.isDone()) Thread.sleep(1000)replaced withheartbeatFuture.get(), eliminating unnecessary 1-second wakeups while waiting for client disconnection. -
Fixed session map growing without bound — A scheduled hourly sweep now removes expired sessions. Previously, repeated logins accumulated stale entries indefinitely.
-
Fixed dashboard password using string equality — Login now uses
MessageDigest.isEqual()for constant-time comparison, preventing timing attacks. -
Fixed duplicate
deleteDir()logic — A new sharedutil/FileUtils.deleteDir()(NIO-based) replaces the identical recursive-delete duplicated acrossPyBotCommandandDashboardServer. -
Fixed
plugin.ymlapi-versionoutdated — Updated from'1.13'to'1.21'to match the actual target platform and suppress Paper deprecation warnings on load. -
Fixed Gson used as implicit transitive dependency — Gson is now declared explicitly as
compileOnlyinbuild.gradle.kts. -
Fixed legacy
§color codes in commands — All in-game messages inPyBotCommandnow use the Paper Adventure API (MiniMessage), eliminating deprecation warnings and ensuring correct rendering in modern clients.
Config changes
No new config keys. Existing configs remain fully compatible.
✨ Features
🐍 Zero-Touch Python Setup
No Python installed on your server? No problem. PyRunner automatically detects your OS and CPU architecture and downloads a self-contained Python binary via python-build-standalone on first start. Works on Linux (x64 & ARM64), macOS (x64 & ARM64), and Windows (x64). The binary is downloaded once and cached locally.
📦 Automatic Dependency Installation
If your bot has a requirements.txt, PyRunner will automatically run pip install -r requirements.txt before launching whenever the deps haven't been installed yet. Delete bot/.pyrunner_deps_installed to force a reinstall on next start.
🔁 Crash Recovery & Loop Protection
The bot process is monitored constantly. If it crashes, PyRunner waits a configurable delay then restarts it automatically. If it crashes too many times in a short window, crash-loop protection kicks in and stops retrying — preventing infinite restart spam while alerting you.
🖥️ Live Web Dashboard
A built-in HTTP server hosts a sleek, password-protected control panel accessible from any browser. Features include:
- Live log streaming via Server-Sent Events (SSE) — no refresh needed
- Start / Stop / Restart / pip install buttons
- Real-time status, uptime, PID, and restart count
- Memory monitoring — live RSS memory usage display
- Color-coded log lines (errors, warnings, system messages, tracebacks)
- Auto-scroll with toggle
- stdin input bar — send commands directly to the bot process
- Multi-bot selector — switch between bots from the dashboard (auto-detected)
- CSRF protection on all mutating API endpoints
- Brute-force login protection — lockout after 5 failed attempts
🔔 Discord Webhook Notifications
Get notified in a Discord channel when your bot starts, stops, crashes, or when pip install runs — all via a standard Discord webhook URL. No bot token required for notifications. Each event type is individually toggleable.
🎮 In-Game Commands
Full control from the Minecraft console or in-game with the /pybot command. No need to SSH into your server just to restart the bot.
🤖 Multiple Bot Support
Run more than one Python bot simultaneously — each with its own process, log file, and independent crash recovery. Control each bot individually with /pybot start <n>, /pybot stop <n>, etc.
📌 Python Version Pinning
Pin a specific Python minor version (e.g. "3.11", "3.12") instead of always downloading the latest release.
💻 stdin Passthrough
Send input directly to the bot process from the web dashboard without restarting it — useful for bots that accept console commands via stdin.
📊 Memory Monitoring
The web dashboard shows live memory usage (RSS) of the bot process, read directly from /proc/<pid>/status on Linux.
🔗 TunnelMC Integration
If TunnelMC is installed and has an active tunnel on the dashboard port, PyRunner will print the public URL to console when the bot starts — purely informational, TunnelMC is never auto-started.
📥 Installation
- Download
PyRunner.jarand place it in yourplugins/folder - Start the server once to generate config files
- Place your bot code in
plugins/PyRunner/bot/(should includerequirements.txtfor dependencies) - Edit
plugins/PyRunner/config.yml— at minimum set your bot token underenv: - Restart the server, or run
/pybot start
PyRunner will handle the rest — Python download, pip install, and bot launch happen automatically.
📂 File Structure
plugins/
PyRunner/
config.yml ← All plugin settings
bot/ ← Your bot code goes here
main.py
requirements.txt
.pyrunner_deps_installed ← Created after successful pip install
logs/
bot.log ← Live bot output (rotates at 5MB, keeps 3 backups)
bot-<n>.log ← Per-bot log files when using multiple bots
python/ ← Auto-downloaded Python binary (do not edit)
python.properties ← Cached binary paths
🎮 Commands & Permissions
Permission node: pyrunner.admin (default: OP)
| Command | Description |
|---|---|
/pybot start [name] |
Start the bot (or a specific bot by name) |
/pybot stop [name] |
Stop the bot (or a specific bot by name) |
/pybot restart [name] |
Restart the bot (or a specific bot by name) |
/pybot status [name] |
Show status of all bots or a specific one |
/pybot install [name] |
Force re-run pip install |
/pybot reload |
Reload config.yml |
🌐 Web Dashboard
Open http://<your-server-ip>:<port> in any browser (default port: 8080).
The dashboard is password protected. Change the default password in config.yml.
🔔 Discord Webhook Setup
- Open the Discord channel you want notifications in
- Go to Edit Channel → Integrations → Webhooks → New Webhook
- Copy the Webhook URL
- Paste it into
config.ymlunderdiscord.webhook.url - Set
discord.webhook.enabled: true - Restart the server or run
/pybot reload
📊 Compatibility
| Primary Target | Paper 1.21.1 |
| Backwards Compatible | Paper / Spigot 1.13+ |
| Java | 21+ |
| OS | Linux, macOS, Windows |
| Architectures | x64, ARM64 |
| Hosting | Self-hosted, Pterodactyl, any VPS |
⚠️ Java Version Note: Although the plugin API is compatible with 1.13+, the JAR is compiled with Java 21 which means your server must be running Java 21 or higher regardless of Minecraft version. Older Minecraft versions (1.13–1.16) typically run on Java 8 or 11 and will fail to load this plugin with an
UnsupportedClassVersionError. In practice, Java 21 + Paper 1.21.x is the recommended and fully tested setup.
🔗 Related Projects
NodeRunner — The Node.js equivalent of this plugin. Run a Node.js Discord bot alongside your Minecraft server.
TunnelMC — Expose any server port to the internet via ngrok tunnels. If your host doesn't provide multiple port allocations, TunnelMC pairs perfectly with PyRunner to make the web dashboard publicly accessible without needing an extra port.
📄 License
All Rights Reserved — This plugin and its source code are proprietary.
Developed by Spider


