Compatibility
Minecraft: Java Edition
Platforms
Links
Tags
Creators
Details
NodeRunner 🚀
Run your Node.js Discord bot directly inside your Minecraft server — no VPS, no extra hosting, no SSH.
NodeRunner is a Paper plugin that manages a Node.js process as a child of your Minecraft server. It handles everything automatically: downloading Node.js, installing dependencies, restarting on crash, live log streaming via a web dashboard, and Discord notifications — all from one config.yml.
🆕 What's new in 1.3.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
- Node.js 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 that could exceed the cap.
switchBot() race fixed — historical logs are now fully loaded before the SSE stream connects, preventing out-of-order or duplicate lines when switching bots.
Bug Fixes
-
failedAttemptsmap unbounded growth — the brute-force attempt tracker is now swept hourly alongside the session map, preventing unbounded accumulation of stale IP entries. -
JSON injection in status/bots API — bot names and entry file paths are now serialised with Gson instead of string concatenation, preventing malformed JSON if a name contains quotes or backslashes.
-
setsidpath now covers Alpine Linux / Pterodactyl — checks/usr/bin/setsid,/bin/setsid, and/usr/local/bin/setsidbefore falling back gracefully. Previously only/usr/bin/setsidwas checked, so process group killing silently did nothing on Alpine-based containers. -
FileReaderplatform charset —/proc/<pid>/statusis now read with explicit UTF-8 instead of the JVM default charset. -
FileUtils.deleteDir()now returnsboolean— failures are logged per-file instead of silently swallowed. -
GET endpoints reject non-GET requests —
/api/status,/api/bots,/api/logs/recent,/api/logs/streamnow return 405 for wrong-method requests. -
Log timestamps include the date — entries now use
MM-dd HH:mm:ssformat so logs spanning midnight are unambiguous. -
Content-Security-Policy header added to all dashboard responses.
-
Gradle performance flags —
parallel,caching, andG1GCJVM args added togradle.propertiesfor faster local builds. -
Fixed state race condition —
BotStateis now managed viaAtomicReferenceinstead of a plainvolatilefield, eliminating a class of bugs where background threads could set the state back toRUNNINGafter a stop was already requested. -
Fixed child process orphaning on stop — NodeRunner now launches the bot with
setsidon Linux/macOS and sendskill -TERM -<pgid>on stop, matching PyRunner's behaviour. Previously, any subprocesses spawned by the bot (workers, shell scripts, etc.) were left running after the parent was killed. -
Fixed
extractTardouble-reading the archive — The extracted directory is now located by diffing the folder listing before and after extraction. The old approach rantar tfon the archive file a second time, which could fail if the archive had already been deleted. -
Fixed
restart()busy-wait — The polling loop (Thread.sleep(200)in awhileloop) has been replaced with aCountDownLatch, so restarts are both more reliable and use zero CPU while waiting. -
Fixed
HttpClientcreated per request — A single sharedHttpClientinstance is now reused across all Node.js version and download requests, rather than constructing a new one (with its own thread pools and connection pool) for each call. -
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.index.jsinstead ofbot/index.js), 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 (e.g. multiple open tabs or reconnect storms) would fill the pool and cause all other API requests — status polls, button clicks — to hang indefinitely. -
Fixed
DashboardServer.jsonString()unsafe escaping — Dashboard log API responses now useGSON.toJson()instead of a manualreplace()chain. Log lines containing Unicode control characters (\u0000–\u001F) could previously produce malformed JSON that failed to parse in the browser. -
Fixed memory cache non-atomic reads — The three separate
volatilefields (cachedMemKb,cachedMemPid,cachedMemTime) have been replaced with a singlevolatile MemCacherecord, swapped atomically. A thread could previously observe a new timestamp alongside stale memory values from the previous cycle. -
Fixed SSE handler busy-polling — The
while (!heartbeatFuture.isDone()) Thread.sleep(1000)loop is replaced withheartbeatFuture.get(), eliminating the 1-second wake-up overhead while waiting for clients to disconnect. -
Fixed session map growing without bound — A
ScheduledExecutorServicenow sweeps expired sessions every hour. Previously, repeated logins without a server restart would accumulate stale entries in the map indefinitely. -
Fixed dashboard password using string equality — The login check now uses
MessageDigest.isEqual()for constant-time comparison, preventing theoretical timing attacks on the password. -
Fixed duplicate
deleteDir()logic — A new sharedutil/FileUtils.deleteDir()(NIO-based) replaces the identical recursive-delete method that was duplicated acrossNodeBotCommandandDashboardServer. -
Fixed
plugin.ymlapi-versionoutdated — Updated from'1.13'to'1.21'to match the actual target platform and suppress Paper's deprecation warnings on load. -
Fixed Gson used as implicit transitive dependency — Gson is now declared explicitly as
compileOnlyinbuild.gradle.ktsso the build doesn't silently rely on Paper's bundled copy, which Paper reserves the right to relocate or remove. -
Fixed legacy
§color codes in commands — All in-game messages inNodeBotCommandnow use the Paper Adventure API (MiniMessage) instead of deprecated§-prefixed color codes, eliminating deprecation warnings and ensuring correct rendering in modern clients.
Changes
- Dashboard now uses a bounded thread pool (8 threads) instead of an unbounded cached pool
- SSE heartbeats use a shared scheduled executor instead of per-connection threads
- NodeDownloader is now a shared singleton — multiple bots no longer trigger duplicate downloads
- Webhook shutdown is now handled cleanly on plugin disable
- Restart now waits for the process to fully stop before starting again (prevents race conditions)
Bug Fixes
- Fixed stop command not waiting for the process to actually terminate before reporting "stopped"
- Fixed auto-restart thread not being interrupted when manually stopping a bot
Config changes
New option added to config.yml:
# Per-bot env vars (in bots: list)
bots:
- name: "modbot"
entry-file: "bot/modbot/index.js"
env:
BOT_TOKEN: "modbot-token-here"
Existing configs remain fully compatible — all new keys use their defaults if not present.
✨ Features
⚡ Zero-Touch Node.js Setup
No Node.js on your server? No problem. NodeRunner auto-detects your OS and CPU architecture and downloads the correct Node.js LTS binary on first launch. Works on Linux (x64 & ARM64), macOS, and Windows. Downloaded once (~30MB), cached forever.
📦 Automatic Dependency Installation
Got a package.json? NodeRunner runs npm install automatically before launch whenever node_modules is missing. No manual intervention needed.
🔁 Crash Recovery & Loop Protection
Your bot is monitored constantly. On crash, NodeRunner waits a configurable delay and restarts it. If it crashes too many times within a short window, crash-loop protection halts retries and alerts you — no infinite restart spam.
🖥️ Live Web Dashboard
A built-in, password-protected control panel accessible from any browser:
- Live log streaming via Server-Sent Events (no page refresh)
- Start / Stop / Restart / npm install buttons
- Real-time status, uptime, PID, restart count, and memory usage
- stdin input bar — send commands directly to the bot process
- Multi-bot selector — switch between bots from the dashboard (auto-detected)
- Color-coded log output with auto-scroll toggle
- CSRF protection on all mutating endpoints
- Brute-force login protection — lockout after 5 failed attempts
🔔 Discord Webhook Notifications
Get pinged in Discord when your bot starts, stops, crashes, or when npm install runs — via a standard webhook URL. No bot token required. Every event type is individually toggleable.
🎮 In-Game Commands
Full control from the Minecraft console or in-game via /nodebot. No SSH session needed just to restart your bot.
🤖 Multiple Bot Support
Run more than one Node.js bot simultaneously — each with its own process, log file, and independent crash recovery. Control each bot individually with /nodebot start <name>, /nodebot stop <name>, etc.
📌 Node.js Version Pinning
Pin a specific Node.js major version (e.g. "22") instead of always downloading the latest LTS. Useful if your bot requires a specific runtime version.
💻 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, NodeRunner will print the public URL to console when the bot starts — purely informational, TunnelMC is never auto-started.
🐋 Pterodactyl Compatible
Fully tested on Pterodactyl panels. Uses .tar.gz for Node.js extraction (no xz dependency), and correctly injects the node binary into the process PATH for npm compatibility inside containers.
📥 Installation
- Drop
NodeRunner.jarinto yourplugins/folder - Start the server once to generate config files
- Place your bot code in
plugins/NodeRunner/bot/(needsindex.js+package.json) - Set your bot token and any other secrets under
env:inconfig.yml - Restart, or run
/nodebot start
NodeRunner handles the rest — Node.js download, npm install, and bot launch all happen automatically.
📂 File Structure
plugins/
NodeRunner/
config.yml ← All plugin settings
bot/ ← Your bot code goes here
index.js
package.json
logs/
bot.log ← Live output (rotates at 5MB, keeps 3 backups)
bot-<name>.log ← Per-bot log files when using multiple bots
nodejs/ ← Auto-downloaded Node.js binary (don't touch)
🎮 Commands & Permissions
Permission: noderunner.admin (default: OP)
| Command | Description |
|---|---|
/nodebot start [name] |
Start the bot (or a specific bot by name) |
/nodebot stop [name] |
Stop the bot (or a specific bot by name) |
/nodebot restart [name] |
Restart the bot (or a specific bot by name) |
/nodebot status [name] |
Show status of all bots or a specific one |
/nodebot install [name] |
Force re-run npm install |
/nodebot reload |
Reload config.yml |
🌐 Web Dashboard
Access at http://<your-server-ip>:8080 (port is configurable).
Password protection is enabled by default — change the default password in config.yml before going public.
Pterodactyl users: The dashboard needs a second allocated port. Ask your host to add one, or use TunnelMC or a Cloudflare Tunnel to expose it without one.
🔔 Discord Webhook Setup
- Go to your Discord channel → Edit Channel → Integrations → Webhooks → New Webhook
- Copy the Webhook URL
- Paste it into
config.ymlunderdiscord.webhook.url - Set
discord.webhook.enabled: true - Restart or run
/nodebot reload
🐋 Pterodactyl Notes
NodeRunner is fully tested on Pterodactyl. Keep in mind:
- The plugin uses
.tar.gzfor Node.js to avoid thexzdependency missing in most container images - If a previous install failed, delete
plugins/NodeRunner/nodejs/and restart to re-download - Set your bot token and other secrets via
env:inconfig.yml— do not hardcode them in your bot files - The dashboard needs a separate allocated port — ask your host to add one
📊 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. Servers running older Minecraft versions on Java 8 or 11 will fail to load this plugin with an
UnsupportedClassVersionError. Java 21 + Paper 1.21.x is the recommended and fully tested setup.
🔗 Related
TunnelMC — Expose any server port to the internet via ngrok tunnels. Pairs perfectly with NodeRunner to make the web dashboard publicly accessible without needing an extra port allocation.
PyRunner — The Python equivalent of this plugin. Run a Python Discord bot alongside your Minecraft server.
📄 License
All Rights Reserved — This plugin and its source code are proprietary. You may not copy, redistribute, modify, or decompile this software without explicit written permission from the author.
Developed by Spider


