Minecraft is fun.
Managing Minecraft manually? Absolutely not.
Not in 2025. Not with my attention span.
Also not when SaltStack exists and can do it for me while I sit there giggling like “hehe ops go brrr”.
This blog post is the full story of how I took a Minecraft server, tore out every possible manual step, declarativized its entire existence, and turned it into a system where I can redeploy the whole thing from scratch with one Salt command.
Yes, it’s extra.
Yes, it’s industrial.
And yes, it absolutely slaps.
Structure
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
┌──────────────────────────────────────┐
│ Salt Master │
│ (holds pillars + states + templates) │
└──────────────────────────────────────┘
│
state.apply │ pushes config + files
▼
┌────────────────────────────────────────┐
│ Minecraft Host VM │
└────────────────────────────────────────┘
│
┌──────────────────┼───────────────────────┐
│ │ │
▼ ▼ ▼
┌────────────────┐ ┌──────────────────┐ ┌────────────────────┐
│ Java 21 (JDK) │ │ File Structure │ │ Firewall (UFW) │
│ installed via │ │ created via Salt │ │ config via Salt │
│ pkg.installed │ │ /opt/minecraft │ │ opens port 25565 │
└────────────────┘ │ /opt/minecraft/. │ └────────────────────┘
└──────────────────┘
│
▼
┌──────────────────────────┐
│ Paper Server Binary │
│ server.jar deployed by │
│ file.managed │
└──────────────────────────┘
│
▼
┌──────────────────────────┐
│ Config Templates (J2) │
│ - server.properties │
│ - bukkit.yml │
│ - spigot.yml │
│ - paper configs │
│ - whitelist.json │
│ - ops.json │
│ - eula.txt │
│ rendered from pillars │
└──────────────────────────┘
│
▼
┌───────────────────────────┐
│ systemd Unit │
│ minecraft.service created │
│ by Salt + auto-start │
└───────────────────────────┘
│
systemctl start
▼
┌─────────────────────────────────────────┐
│ Running Minecraft Server (Paper) │
│ - reads configs │
│ - loads world from /opt/minecraft-data │
│ - auto restarts on crash │
│ - restarts on state changes │
└─────────────────────────────────────────┘
│
▼
┌───────────────────────────────────┐
│ Clients Connect │
│ mc.example.com:25565 → firewall │
│ → systemd → Paper → world data │
└───────────────────────────────────┘
Pillars: The Big Bag of Truth for My Minecraft World
Before Salt does anything, I built a pillar structure that describes everything the Minecraft server should be.
No partial configs.
No hidden values.
No “I’ll just edit this one thing manually” traps.
It’s all in one hierarchical YAML tree that looks roughly like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
application:
minecraft:
server_jar_url: https://example.org/paper.jar
server_ip: mc.example.com
port: 25565
eula: true
config:
server_properties:
difficulty: hard
whitelist: true
max_players: 100
view_distance: 32
sim_distance: 32
paper_global:
# performance tuning stuff
world_defaults:
auto_replenish: true
player:
playername:
id: 00000000-0000-0000-0000-000000000000
op: true
allowed: true
This pillar data drives every template, every config, every file. It’s the golden source of truth.
Adding operators? → Pillar.
Adjusting Paper’s global config? → Pillar.
Changing MOTD? → Pillar.
Switching to a new Paper build? → Pillar.
This one file dictates the whole server’s personality.
It also helps prevent Weekend-Yuri from forgetting what Weekday-Yuri configured three days ago.
We love that for us.
Automated Installation of Java 21 (Because Minecraft Loves Moving Targets)
Minecraft 1.21+ requires Java 21, and I refuse to manually install Java versions like some Windows tutorial YouTuber.
So Salt handles:
- downloading the correct Oracle JDK
.deb - installing it via
pkg.installed - pinning the version through the state system
- keeping the machine consistent no matter how often I rebuild it
Any machine Salt touches becomes Minecraft-ready instantly.
Also, this eliminates the classic server-admin experience of:
“why is the server on fire?”
“oh it’s running on the wrong Java version again lol”
Not on my watch.
Directory Structure: Order Among the Chaos
Minecraft installs quickly turn into digital junk drawers if you’re not careful. Salt makes sure that literally every directory I use exists, has correct permissions, and is reproducible across machines.
My structure:
1
2
3
/opt/minecraft → runtime directory
/opt/minecraft-data → world + persistent data
/opt/minecraft/config → Paper configs split logically
Salt ensures these are:
- created if missing
- owned by root
- mode 0755
- available before any configs are rendered
This means I can provision a new machine and the layout is identical every time. If you host systems long enough, deterministic structure feels like a warm hug.
Deploying the Paper JAR Fully Automatically
Instead of me manually downloading Paper, scp‘ing it, forgetting where I put it, updating it incorrectly, and crying…
Salt simply:
- Reads the JAR URL from the pillar
- Downloads it to
/opt/minecraft/server.jar - Ensures correct mode
- Triggers a service restart only if it changes
So updating the server is:
- change URL
state.apply- go drink something hydrating
Salt handles the rest.
I basically offloaded “download Minecraft updates” to a programmable butler. It’s giving DevOps Fantasy Princess.
The Real Magic: Templating ALL Config Files With Jinja
This is where things get beautifully ridiculous.
Nearly every Minecraft config format is either YAML, JSON, or key-value. Perfect for Jinja templating.
Salt now manages:
- server.properties
- bukkit.yml
- spigot.yml
- paper-global.yml
- paper-world-defaults.yml
- whitelist.json
- ops.json
- eula.txt (Salt signs the EULA on my behalf like a lawyer)
Why this is sick:
- Configs regenerate on every deploy
- Player data is synced automatically
- All configs stay consistent with pillar data
- If I add/remove a player, both whitelist and ops list update in one go
- Paper’s internal tuning is fully reproducible
The ops + whitelist generation is especially satisfying.
Salt loops through my player: pillar entries and dynamically writes JSON arrays for both files.
Whenever I add a player to pillar, Salt is like:
“okay queen, I updated the whitelist AND gave them operator status, mwah.”
The amount of manual JSON editing I have avoided… unbelievable.
Branding the Server Automatically (Yes, The Icon Matters)
Salt also deploys my custom server-icon.png straight into /opt/minecraft.
Because:
- A Minecraft server without a cute icon is morally wrong
- Automation should include aesthetics
- I want players to see something adorable when they connect
- DevOps but cute
It’s small, but it absolutely delivers serotonin.
The systemd Unit: Turning Minecraft Into a Real Grown-Up Service
Salt pushes a templated systemd service file that:
- runs the Paper JAR
- sets working directories
- pipes output to a proper log
- restarts on failure
- auto-starts at boot
- reloads systemd automatically when the unit file changes
This gives Minecraft the same dignity as any other production daemon.
If configs change?
Salt triggers a restart.
If the JAR updates?
Restart.
If the unit file changes?
systemctl daemon-reload happens automatically.
Minecraft has never been this well-behaved in its entire life.
The Firewall State: Because I Always Forget they Exists
Listen.
I will configure an entire multi-node infrastructure with HA routing, Kubernetes, and CI/CD…
…but I will forget a firewall rule every. single. time.
Salt solves this with a simple check:
- if the port rule exists → do nothing
- if not → add it
Idempotency is love.
Idempotency is life.
Apply State → Server Materializes Out of the Void
My deployment flow is now:
1
salt 'minecraft-host' state.apply application.minecraft
Salt will:
- install Java
- create directories
- deploy server JAR
- render all config templates
- generate whitelist + ops
- install systemd unit
- reload systemd if needed
- open the firewall
- start the server
- restart on config changes
It’s like Terraform but for vibes and creepers.
I can take a completely clean VM, run one command, and summon a fully-formed, tuned Minecraft server into existence.
I basically built a fully automated, declarative, reproducible Minecraft stack where:
- everything is driven by pillars
- configs never drift
- updates are trivial
- server behavior is deterministic
- crashes don’t matter because systemd picks it up
- deployment is one command
- adding new players takes 5 seconds
- moving to a new machine is painless
This is the closest you can get to “Minecraft as Infrastructure-as-Code” without writing your own orchestration layer.
Is this overkill?
Absolutely.
Did I do it anyway?
Also absolutely.
Does it bring me joy every time I run state.apply and watch a perfect Minecraft server spawn out of thin air?
You have no idea.