Hosting Guides

Infrastructure as Code: Minecraft PaperMC via Docker Compose (Ubuntu 24.04)

6 min readUbuntu 24.04Docker ComposeUnity
Once your server is online, jump to the Minecraft (Java Edition) command and config reference.

If you have ever rebuilt a game server on a new host, you know the pain: hunting down the right launch flags, recreating the directory tree, remembering which plugin versions you patched, and praying you did not miss the one custom config that kept the world from corrupting. Docker Compose eliminates that entire class of failure. The entire server definition, environment, ports, volumes, and even JVM tuning lives in a single version controlled text file. Copy the project directory to any machine with Docker installed, run one command, and the server comes up identically every time.

PaperMC is the only reasonable choice for a modern Minecraft Java survival server. It is a drop in replacement for the official Mojang jar that rewrites the chunk loading pipeline, the entity ticking engine, and the netcode layer for asynchronous execution. The result is measurably higher TPS (ticks per second) under load, faster world generation, and dramatically reduced memory pressure during heavy redstone contraptions or sprawling mob farms. Pairing PaperMC with Docker Compose on Ubuntu 24.04 gives you an immutable, repeatable deployment that behaves like Infrastructure as Code rather than a fragile hand tuned installation.

Prerequisites

  • A fresh Ubuntu 24.04 LTS installation on a VPS or dedicated box.
  • Root access (or a sudo capable user) to install Docker Engine and Docker Compose.
  • Docker Engine installed and the docker group configured for your user.
  • At least 8 GB of RAM allocated to the host, with 6 GB dedicated to the JVM heap after OS overhead.

Step 1: System Preparation and Directory Structure

Docker Compose mounts host directories into containers as bind volumes. The mapping is literal and unforgiving: if the host path does not exist when the container starts, Docker creates it with root ownership, which locks out any non-root user trying to manage files later. The correct workflow is to create the project directory tree as your normal user first, then let the container write into those pre existing folders.

PaperMC stores world data, player inventories, plugin configs, and logs inside the container at /data. By mounting a local data directory into that path, every world save, plugin jar, and configuration file lands on the host filesystem where you can back it up with standard Linux tools.

root@host
mkdir -p /opt/minecraft/data
chown -R $USER:$USER /opt/minecraft
cd /opt/minecraft
pwd

Step 2: Crafting the Docker Compose File

The itzg/minecraft-server image is the de facto standard for containerized Minecraft. It handles EULA acceptance, version resolution, server type selection, and health checks automatically. The image supports Paper natively by setting a single environment variable, which triggers an internal routine that downloads the latest stable PaperMC build for the requested Minecraft version and wires it as the server jar.

Create the compose file in the project root. This single file is the entire contract for the deployment.

docker-compose.yml
services:
mc:
image: itzg/minecraft-server:java21
container_name: minecraft-paper
tty: true
stdin_open: true
restart: unless-stopped
ports:
- "25565:25565"
environment:
EULA: "TRUE"
TYPE: "PAPER"
VERSION: "1.21.4"
MEMORY: "6G"
volumes:
- ./data:/data

Breaking that down: tty and stdin_open are required so the container can attach to an interactive console for RCON or direct operator commands. restart: unless-stopped ensures the server comes back up after a host reboot or an unexpected container crash. The VERSION field pins the Minecraft release to a specific semantic version, so future pulls do not silently upgrade the world format and risk plugin breakage. The ./data:/data volume mount is what makes the server stateful: everything the jar writes to /data inside the container persists on the host at /opt/minecraft/data.

Step 3: JVM Tuning and Aikar's Flags

Default Java memory allocation is hostile to Minecraft. The stock JVM grows its heap lazily, triggers full stop garbage collection at the worst possible moments (mid combat, during massive redstone updates), and produces the infamous "TPS drop to 5" stutter that ruins gameplay. Aikar's Flags are a community benchmarked set of JVM options that replace the naive defaults with a generational garbage collector (G1GC), aggressively tuned pause targets, and explicit memory regions sized for server workloads. The result is sub 50 millisecond garbage collection pauses even under heavy chunk loading.

The itzg/minecraft-server image exposes dedicated environment variables for memory so you do not need to hand edit raw JVM arguments for the common case. For production, you override the default MEMORY variable and pass the full flag string through JVM_OPTS.

Update the compose file to include the tuned memory block. This is the production configuration you should ship.

docker-compose.yml (production)
services:
mc:
image: itzg/minecraft-server:java21
container_name: minecraft-paper
tty: true
stdin_open: true
restart: unless-stopped
ports:
- "25565:25565"
environment:
EULA: "TRUE"
TYPE: "PAPER"
VERSION: "1.21.4"
INIT_MEMORY: "6G"
MAX_MEMORY: "6G"
JVM_OPTS: >-
-XX:+AlwaysPreTouch
-XX:+DisableExplicitGC
-XX:+ParallelRefProcEnabled
-XX:+PerfDisableSharedMem
-XX:+UnlockExperimentalVMOptions
-XX:+UseG1GC
-XX:G1HeapRegionSize=16M
-XX:G1NewSizePercent=30
-XX:G1MaxNewSizePercent=40
-XX:G1ReservePercent=20
-XX:MaxGCPauseMillis=200
-XX:MaxMetaspaceSize=512M
volumes:
- ./data:/data

The critical additions here are INIT_MEMORY and MAX_MEMORY both locked to 6 GB. Pinning the initial and maximum heap to the same value prevents the JVM from resizing its heap during runtime, which eliminates resize induced pauses and lets -XX:+AlwaysPreTouch zero the entire allocation at startup. On an 8 GB host, this leaves roughly 2 GB for the OS, Docker daemon overhead, and any background monitoring agents. If your host has 16 GB, scale both values to 10G or 12G.

-XX:+UseG1GC enables the G1 garbage collector, which splits the heap into 16 MB regions and performs most collection work in parallel with the application threads. The G1NewSizePercent and G1MaxNewSizePercent bounds keep the young generation large enough to absorb allocation spikes from chunk generation without spilling into expensive old generation collections. The result is a server that holds a flat 20 TPS even when six players are simultaneously flying in different directions and loading fresh terrain.

Step 4: Deployment and Server Lifecycle

With the compose file saved, bring the stack up. The first pull downloads the Java 21 base image and the PaperMC bootstrap layer (roughly 600 MB), then the container initializes the /data directory structure on your host volume and begins the first world generation.

Deploy the server
docker compose up -d

The -d flag detaches the container so it runs in the background. You can now disconnect from the SSH session safely; the server stays up until you explicitly stop it.

Watch the live log output to confirm the server finishes initialization and binds to port 25565.

Follow live logs
docker compose logs -f

Once you see the "Done" message and the tick rate holding steady, detach from the log stream with Ctrl-C. This only detaches your terminal; the container continues running in the background.

To gracefully stop the server (triggering a world save and clean shutdown rather than a hard kill), run:

Graceful shutdown
docker compose down

The compose down command sends a SIGTERM to the Java process, which PaperMC intercepts and handles by flushing every loaded chunk to disk before exiting. If you need to issue an operator command directly inside the running container, attach to its console:

Attach to console
docker attach minecraft-paper

Type commands as you would on a native server console. Detach without stopping the container by pressing Ctrl-P then Ctrl-Q. Do not press Ctrl-C, that sends an interrupt signal and shuts the server down.

After the first boot, your directory tree looks like this:

Host filesystem
/opt/minecraft/
├── docker-compose.yml
└── data/
├── banned-ips.json
├── banned-players.json
├── bukkit.yml
├── eula.txt
├── ops.json
├── paper-global.yml
├── paper-world-defaults.yml
├── permissions.yml
├── plugins/
├── server.properties
├── spigot.yml
├── usercache.json
├── whitelist.json
└── world/
├── level.dat
├── region/
└── ...

Conclusion

You now have a fully containerized PaperMC survival server running on Ubuntu 24.04 with Aikar's garbage collection flags, a pinned Java 21 runtime, and an explicit 6 GB heap. Because the entire deployment is defined in a single compose file, disaster recovery is trivial: back up the /opt/minecraft directory (the compose file plus the data volume), and you can restore the exact same server on any Docker capable host in minutes.

To customize the server, edit files directly in the host data directory. Drop plugin jars into data/plugins/ before starting the container, or edit data/server.properties to change the MOTD, difficulty, view distance, and max players. After any configuration change, restart the container with docker compose restart so PaperMC picks up the new settings on the next boot.

For routine maintenance, script a cron job or systemd timer that stops the container, runs rsync or tar against the data folder to a secondary mount or remote bucket, then brings the server back online. Because the container is the only process writing to that directory, you get clean point in time snapshots with no locked file issues. That is the power of Infrastructure as Code applied to game servers: declarative, portable, and ruthlessly repeatable.