skip to content

Minecraft Server Setup for Development

How I personally setup development based minecraft servers for ease of use.

6 min read

Overview

Minecraft plugin work gets a lot faster once your local server becomes boring. Same folders, same steps, same loop every single time. This is the setup I use for data-heavy plugin work, the kind that’s mostly game state, tracking, and persistence.

Feel free to copy the whole thing, or just steal the parts that fit your plugin.

Goal

Everything here serves one thing: a short feedback loop. Write code, build the jar, get it into the server, reload or restart, test. That’s the whole cycle.

Every step below exists to make that loop shorter and less annoying.

Context

For some context, my plugins are about state, not flashy combat tweaks. They collect data, store it, and hand it to other parts of a game mode.

That kind of work puts stability first. I care way more about version choice, schema safety, and readable logs than I do about particle effects.

IDE And Project Setup

I use IntelliJ IDEA, and I’m not really taking arguments on this one. The Gradle support is great, navigation is fast, and the built-in decompiler comes in handy whenever you need to poke around inside someone else’s plugin.

The first pain point you’ll hit is jar copying. A normal build drops the jar in build/libs, but your server wants it in server/plugins. You could copy it by hand after every build, or you could fix it once with a small Gradle task.

Gradle Build Script With Auto Copy

This minimal script copies the built jar into server/plugins after each build. Swap in your own group, version, API version, and jar name.

plugins { java }
group = "me.yourname"
version = "1.0"
repositories {
mavenCentral()
maven("https://papermc.io/repo/repository/maven-public/")
}
dependencies {
compileOnly("io.papermc.paper:paper-api:1.20.4-R0.1-SNAPSHOT")
}
java {
toolchain.languageVersion.set(JavaLanguageVersion.of(17))
}
tasks.register<Copy>("copyToServer") {
val jarName = "MyPlugin-${project.version}.jar"
from(buildDir.resolve("libs/$jarName"))
into(layout.projectDirectory.dir("server/plugins"))
doLast { println("Copied $jarName to server/plugins") }
}
tasks.build { finalizedBy("copyToServer") }

You can get fancier later, like finding the newest jar automatically, deleting old snapshots, or failing fast when the server folder is missing. This version covers the core annoyance.

Version Strategy

Don’t chase a new Minecraft release on day one. I pick a Paper version that passes three tests: it matches the production server, it has the API hooks my plugin actually uses, and the community still supports it.

If a client wants some odd version, ask why. A supported baseline has saved me hours of rework more than once.

My checklist looks like this:

  • The target server actually runs the version.
  • The plugin APIs I need exist and aren’t about to change.
  • Paper still publishes patches for that line.
  • Dependencies like LuckPerms and Spark publish builds for it.
  • The Java toolchain matches what the server wants, like Java 17 or newer.

Local Server Folder Layout

I keep the dev server inside the project itself. Relative paths stay stable, and Gradle can copy the jar without guessing where the server lives.

MyPluginProject/
├── build.gradle.kts
├── settings.gradle.kts
├── src/
├── server/
│ ├── paper-1.20.4.jar
│ ├── eula.txt
│ ├── server.properties
│ └── plugins/

Only the basics live in server/: the Paper jar, eula.txt, server.properties, and plugins. If you need to test against multiple versions, make folders like server-1.20.4 and server-1.21, then point the copy task at the right one with a project property.

Key Config Tweaks

Open server.properties and change the handful of settings that actually matter for dev:

  • Set online-mode=false if you’re testing with a custom launcher. Turn it back on for production, obviously.
  • Leave enable-command-block=false unless a test genuinely needs command blocks.
  • Drop the view distance to save your local CPU some pain.
  • Allocate memory on purpose. 2G is plenty for a lean dev server.

Whatever you do, keep production tuning out of the dev config. If the file doesn’t fit on one screen, something snuck in that shouldn’t be there.

Utility Plugins

A few plugins make debugging way less painful.

PlugManX

PlugManX loads, unloads, and reloads a plugin without a full server restart, which is great for iterating on commands and simple listeners.

That said, use full restarts for memory tests and anything with persistent caches. Class loaders love holding onto old singletons and static state after a reload, and then you’re debugging a ghost.

Your Test Plugin

Write yourself a throwaway helper plugin. Use it to dump events, trigger edge cases, or seed test data.

It’s so much faster than stuffing temporary logic into the real plugin and hoping you remember to take it out later.

Spark

Spark profiles tick cost, thread usage, and performance hot spots. I run it after bigger refactors so regressions show up early instead of on someone’s live server.

LuckPerms

Add LuckPerms early. Basic groups let you test real permission gates instead of doing everything as op and finding out about the gaps later.

Other tools can come whenever you actually need them: timing helpers, log tailing, a database viewer for embedded data, that kind of thing.

Fast Iteration Loop

The loop is the same every time:

  • Make a code change.
  • Run build, which triggers the copy task.
  • Reload with PlugManX or restart the server.
  • Run the test command or scenario.
  • Watch the logs and the state.

If a reload leaves old listeners or stale state hanging around, restart instead. And keep an eye on how long the loop takes. Once it creeps past a minute, something in there is worth automating.

Startup Scripts

One command to start the server. That’s the bar.

macOS And Linux

#!/usr/bin/env bash
cd "$(dirname "$0")"
java -Xms2G -Xmx2G -jar paper-1.20.4.jar nogui

Make it executable:

Terminal window
chmod +x start.sh

Windows

Terminal window
@echo off
cd /d %~dp0
java -Xms2G -Xmx2G -jar paper-1.20.4.jar nogui
pause

One note on JVM flags: skip the Aikar flags until the server shows a real memory or garbage collection problem. Dev servers rarely need them.

Common Pitfalls

Things that have personally bitten me:

  • Wrong Java version: the build passes, then the server rejects the plugin anyway.
  • Forgetting a version bump: the old jar stays cached, so your change looks like it did nothing.
  • Reloading for every test: stale static state starts lying to you.
  • The copy task running too early: finalizedBy keeps the order right.
  • Testing on a day-one Minecraft release: new versions love breaking API assumptions.

Future Improvements

Stuff that’s on my list but hasn’t earned its spot yet:

  • Docker, once more than one person needs this setup.
  • Building jars in CI and attaching them to releases.
  • Integration tests that spin up a headless server.
  • Incremental hot swap with a debug agent.
  • A multi-version test matrix for plugins that support several branches.

Wrap Up

Keep the loop tight, automate the copy step, and only run the plugins that make debugging clearer.

Get the boring base right first. Feature work gets a lot easier once the dev server stops wasting your time.