SameOS ~/tools/minecraft-fabric-docker.md

Build Your Own Minecraft Server, Part 2: A Fabric Mod Server, Run With Docker

Published 2026-07-21 · SameOS Tools

Part 1 got a vanilla server running; now it is time for mods and automated operation. My server uses Fabric as the mod loader and runs entirely inside a Docker container. The move bought two things - no Java needed on the host (the container brings its own), and crash/reboot recovery now works the same way as my Palworld and Satisfactory servers. It also introduced one trap around file ownership, so here is the exact setup I run, trap included.

Installing Fabric - it builds a server launcher for you

Fabric works by running its installer once, which produces a server launcher (fabric-server-launch.jar). From then on you execute that instead of server.jar, and everything in the mods folder loads with the game. Drop Fabric API - the base mod nearly every Fabric mod depends on - into mods as well. Mods must match the game version exactly; one version off and the server dies mid-startup. This is also why Part 1 pinned the server to 26.1.2 instead of the newest release - the mods you want to run decide the server version.

cd ~/minecraft

# 1) Get the Fabric installer and run it in server mode (match your game version)
curl -LO https://maven.fabricmc.net/net/fabricmc/fabric-installer/1.0.1/fabric-installer-1.0.1.jar
java -jar fabric-installer-1.0.1.jar server -mcversion 26.1.2 -downloadMinecraft

# 2) Put Fabric API into mods/ (fabricmc.net → "use" page, version-matched)
mkdir -p mods
# downloaded file example: fabric-api-0.154.2+26.1.2.jar → into mods/

# 3) The launch file changes - instead of server.jar:
java -Xms2G -Xmx6G -jar fabric-server-launch.jar nogui
# more mods, more memory - I went from 4G vanilla to 6G

Moving into Docker - one ownership trap to dodge

Mount the whole server folder into a container and run it on an official Java image (eclipse-temurin). The trap is ownership: Docker creates files as root by default, so every world or config file the container writes becomes something your own account cannot edit. Setting the user field to your account id (usually 1000) makes the container create files as you from the start, and the problem never appears. The 90-second stop grace period is the same lesson the Satisfactory article paid for - cut the process before the world save finishes and progress is gone.

# compose.yaml
services:
  minecraft:
    image: eclipse-temurin:25-jre    # ships Java 25 - none needed on the host
    container_name: minecraft
    # run as your account id so files stay editable by you
    # (find yours with: id -u)
    user: "1000:1000"
    restart: unless-stopped
    stop_grace_period: 90s           # wait for the world save before stopping
    working_dir: /server
    command: ["java", "-Xms2G", "-Xmx6G", "-jar", "fabric-server-launch.jar", "nogui"]
    stdin_open: true                 # allows console commands
    tty: true
    volumes:
      - ./:/server                   # the server folder, as-is
    ports:
      - "25565:25565/tcp"            # game traffic - the only public port
      # NEVER expose RCON (25575) to the internet - one password grants op-level
      # control. If you need admin tooling, reach it over an internal Docker
      # network only.

# Start:    docker compose up -d
# Logs:     docker compose logs -f
# Console:  docker attach minecraft   (detach with Ctrl-p Ctrl-q)

Automated world backups - safe without stopping the server

A running Minecraft server rewrites world files constantly, so a naive copy can capture half-written files. The correct dance is to pause saving just before the copy: console commands save-off (stop writing) and save-all (flush everything so far), copy, then save-on to resume. You need a path into the console for that - the compose stdin above or RCON both work; the script below uses the Docker console so there is nothing extra to configure. Put it in cron and it stacks up daily.

#!/bin/bash
# minecraft-backup.sh - world backup (daily cron recommended)
set -eu
SERVER_DIR=~/minecraft
DEST=~/minecraft-backups
KEEP_DAYS=14

mkdir -p "$DEST"
cd "$SERVER_DIR"

console() { docker exec minecraft sh -c "echo '$1' > /proc/1/fd/0" 2>/dev/null || true; }

console "save-off"       # pause writes so files do not change mid-copy
console "save-all flush" # flush all progress to disk
sleep 5

tar -czf "$DEST/world-$(date +%Y%m%d-%H%M).tar.gz" world

console "save-on"        # resume saving

# prune backups older than 14 days
find "$DEST" -name 'world-*.tar.gz' -mtime +$KEEP_DAYS -delete
echo "backup done: $(ls -t "$DEST" | head -1)"

# cron example (daily at 04:30):
#   30 4 * * * /home/YOU/minecraft-backup.sh >> /home/YOU/minecraft-backup.log 2>&1

A few operating notes

Adding or removing mods always requires a restart, and players need the same mod set (server-side-only mods are the exception - optimization mods can live on the server alone). Memory grows with the world, so manage max-players and view-distance together. Before installing any large mod, run the backup script once by hand to create a restore point - once a mod has rewritten your world, a backup is the only way back.

Wrapping up

Minecraft now runs the same way as my Palworld and Satisfactory servers - self-recovering under Docker, with cron handling backups. Three different games, but the skeleton of server operation turned out to be the same, and that is the real conclusion of this series: match versions exactly, give shutdowns time to save, keep admin ports private, and automate backups.

View the compose file and backup script on GitHub

Read Part 3: Settings That Actually Reduce Lag Read Part 1: A Java Server on Linux