How to Run a Minecraft Server in Docker

Why Docker Is the Right Way to Run Minecraft

I have set up Minecraft servers on bare metal, on VMs, on Raspberry Pis, and in Docker containers. Docker wins. Not because it is trendy, but because it solves every annoyance you will hit with a bare-metal installation.

From the homelab: Minecraft servers were one of the first things I set up in my homelab. Running it in Docker means you can manage it alongside your other services without it consuming the whole host. Spinning up a test world is as easy as changing a volume path.

Updating to a new Minecraft version? Change one line in your compose file and recreate the container. Your world data is on a volume, untouched. Want to run a second server for creative mode alongside your survival world? Copy the compose file, change the port, done. Need to back up everything? Snapshot the volume. Want to nuke the server and start fresh? Delete the container, keep the volume. Want to nuke everything? Delete both. The entire thing rebuilds in about 60 seconds.

The itzg/minecraft-server image is the community standard. It has been maintained since 2014, has over 10,000 GitHub stars, and handles everything: Java version selection, server type (Vanilla, PaperMC, Forge, Fabric), EULA acceptance, memory allocation, whitelist management, and automatic updates. It is one of the best-maintained Docker images in the entire ecosystem.

This guide assumes you have Docker and Docker Compose installed. If not, follow the Docker installation guide for Ubuntu 24.04 first.

Terminal showing docker compose up output for a Minecraft server container

Step 1: Basic Docker Compose Setup

Create a directory for your Minecraft server and the compose file:

mkdir -p ~/minecraft-server && cd ~/minecraft-server

Create docker-compose.yml:

services:
  minecraft:
    image: itzg/minecraft-server:latest
    container_name: minecraft
    restart: unless-stopped
    ports:
      - "25565:25565"
    environment:
      EULA: "TRUE"
      TYPE: "PAPER"
      VERSION: "LATEST"
      MEMORY: "4G"
      MOTD: "A Dockerised Minecraft Server"
      MAX_PLAYERS: 10
      DIFFICULTY: "normal"
      MODE: "survival"
      VIEW_DISTANCE: 10
      SIMULATION_DISTANCE: 8
      ENABLE_COMMAND_BLOCK: "false"
      SPAWN_PROTECTION: 16
      ONLINE_MODE: "true"
    volumes:
      - minecraft-data:/data
    stdin_open: true
    tty: true

volumes:
  minecraft-data:

Start the server:

docker compose up -d

That is it. The container will download PaperMC, accept the EULA, apply your configuration, and start the server. Watch the logs to see when it is ready:

docker compose logs -f

When you see a line like Done (12.345s)! For help, type "help", the server is ready to accept connections. Press Ctrl+C to stop following the logs (the server keeps running).

The itzg/minecraft-server image handles Java version selection automatically based on the Minecraft version. You do not need to worry about installing Java 21 or managing JDK versions. The image takes care of it. This is one of the key advantages of containerising the server.

Understanding the Environment Variables

Every server.properties setting can be controlled via environment variables. The image translates them automatically. Here are the important ones:

  • EULA=TRUE – Accepts Minecraft’s EULA. Required, the container will not start without it.
  • TYPE=PAPER – Uses PaperMC instead of vanilla. Other options: VANILLA, FORGE, FABRIC, SPIGOT, PURPUR. I recommend PAPER for most use cases.
  • VERSION=LATEST – Uses the latest Minecraft release. Pin to a specific version (e.g., 1.21.4) if you want stability.
  • MEMORY=4G – Sets both -Xmx and -Xms. For separate values, use JVM_XX_OPTS or MAX_MEMORY and INIT_MEMORY.
  • ONLINE_MODE=true – Authenticates players against Mojang. Leave this on.

For the full list of environment variables, check the official documentation. There are hundreds. You can control practically every aspect of the server without ever touching a config file directly.

Step 2: Whitelist and Ops Management

There are two approaches. The simpler option for a small server is environment variables:

    environment:
      # ... other variables ...
      WHITELIST: "Player1,Player2,Player3"
      OPS: "Player1"
      ENFORCE_WHITELIST: "true"

This sets the whitelist and operator list at container startup. If you change these values, recreate the container:

docker compose down && docker compose up -d

The world data is on the volume, so nothing is lost. The server restarts with the updated player lists.

For a larger server where you need to manage players without restarts, use the server console:

# Attach to the server console
docker attach minecraft

# Then type commands:
whitelist add PlayerName
op PlayerName

# Detach without stopping: Ctrl+P, then Ctrl+Q

Be careful with docker attach. If you press Ctrl+C instead of the detach sequence (Ctrl+P, Ctrl+Q), you will stop the server. The stdin_open and tty options in the compose file enable this interactive console, but the detach sequence is easy to forget. An alternative is to use RCON: set ENABLE_RCON=true and RCON_PASSWORD=something_secure, then use an RCON client to send commands without attaching to the container.

Step 3: Proper Volume Configuration

The compose file above uses a Docker named volume (minecraft-data). This is simple and works, but you might want to bind-mount a specific directory instead, especially for easier backups:

    volumes:
      - ./data:/data

This maps a data directory in your project folder to the container’s /data directory. All server files, including the world, config, and logs, live here. You can browse them, back them up, or edit config files directly.

With a bind mount, the directory structure inside ./data will look like:

data/
  server.properties
  whitelist.json
  ops.json
  world/
  world_nether/
  world_the_end/
  logs/
  plugins/         (if using PaperMC)
  config/          (PaperMC config)

If you use a bind mount and see permission errors, the container runs as UID 1000 by default. Set ownership accordingly: sudo chown -R 1000:1000 ./data. You can also change the container’s UID with the UID and GID environment variables.

Step 4: Running Multiple Servers

This is where Docker really shows its value. Want a creative mode server alongside your survival world? Create a second compose file or add a second service:

services:
  survival:
    image: itzg/minecraft-server:latest
    container_name: mc-survival
    restart: unless-stopped
    ports:
      - "25565:25565"
    environment:
      EULA: "TRUE"
      TYPE: "PAPER"
      VERSION: "LATEST"
      MEMORY: "4G"
      MOTD: "Survival Server"
      MODE: "survival"
      DIFFICULTY: "hard"
      VIEW_DISTANCE: 10
      ONLINE_MODE: "true"
      WHITELIST: "Player1,Player2,Player3"
      OPS: "Player1"
      ENFORCE_WHITELIST: "true"
    volumes:
      - survival-data:/data
    stdin_open: true
    tty: true

  creative:
    image: itzg/minecraft-server:latest
    container_name: mc-creative
    restart: unless-stopped
    ports:
      - "25566:25565"
    environment:
      EULA: "TRUE"
      TYPE: "PAPER"
      VERSION: "LATEST"
      MEMORY: "2G"
      MOTD: "Creative Server"
      MODE: "creative"
      DIFFICULTY: "peaceful"
      VIEW_DISTANCE: 12
      ONLINE_MODE: "true"
      WHITELIST: "Player1,Player2,Player3"
      OPS: "Player1"
      ENFORCE_WHITELIST: "true"
    volumes:
      - creative-data:/data
    stdin_open: true
    tty: true

volumes:
  survival-data:
  creative-data:

The key is the port mapping: the survival server uses the default 25565, the creative server is mapped to 25566. In Minecraft, connect to the creative server by adding the port: your-server-ip:25566.

Each server has its own volume, so worlds, configs, and plugins are completely isolated. You can start, stop, update, or delete either server independently.

# Start both
docker compose up -d

# Stop just the creative server
docker compose stop creative

# View logs for survival only
docker compose logs -f survival

Step 5: Backup Strategy

Backups are where Docker makes your life significantly easier. With bind-mounted data, you can back up without stopping the server:

#!/bin/bash
# backup-minecraft.sh
BACKUP_DIR="/opt/backups/minecraft"
DATA_DIR="/home/$(whoami)/minecraft-server/data"
TIMESTAMP=$(date +%Y%m%d-%H%M%S)

mkdir -p "$BACKUP_DIR"

# Tell the server to save and disable autosave temporarily
docker exec minecraft rcon-cli save-all
docker exec minecraft rcon-cli save-off

# Copy the world
tar -czf "$BACKUP_DIR/minecraft-$TIMESTAMP.tar.gz" -C "$DATA_DIR" world world_nether world_the_end

# Re-enable autosave
docker exec minecraft rcon-cli save-on

# Keep last 7 days of backups
find "$BACKUP_DIR" -name "minecraft-*.tar.gz" -mtime +7 -delete

echo "Backup complete: minecraft-$TIMESTAMP.tar.gz"

For the RCON commands to work, add these environment variables to your compose file:

      ENABLE_RCON: "true"
      RCON_PASSWORD: "your-secure-password-here"

Schedule the backup with cron:

chmod +x backup-minecraft.sh
crontab -e

Add:

0 4 * * * /home/your-user/minecraft-server/backup-minecraft.sh

If you are using Docker named volumes instead of bind mounts, you can back up volumes directly:

# Backup a named volume
docker run --rm -v minecraft-data:/source -v /opt/backups:/backup \
  alpine tar -czf /backup/minecraft-$(date +%Y%m%d).tar.gz -C /source .

Always use save-all and save-off before backing up a running server. Without this, you risk copying world files mid-write, which can corrupt the backup. The backup script above handles this automatically via RCON. If you do not have RCON enabled, stop the server before backing up: docker compose stop minecraft.

Step 6: Updating to New Minecraft Versions

With Docker, updates are trivial:

# Pull the latest image
docker compose pull

# Recreate the container with the new image
docker compose up -d

If you set VERSION=LATEST, the container will download and run the newest Minecraft release on next startup. Your world data is on the volume and is not affected by the container being recreated.

If you want to control when version changes happen (which you should for a server with active players), pin the version:

      VERSION: "1.21.4"

Then to update, change the version number in the compose file and run:

docker compose up -d

Back up your world before updating, even with Docker. The container protects you from a bad image or broken config (just roll back the compose file), but Minecraft itself sometimes changes the world format between major versions, and that change happens to the data on your volume. Keep a pre-update backup so you can restore to the old version if something breaks.

Step 7: Firewall and Remote Access

If you are running UFW on the Docker host:

sudo ufw allow 25565/tcp comment 'Minecraft Server'
# If running a second server:
sudo ufw allow 25566/tcp comment 'Minecraft Creative'

Docker manipulates iptables directly, which means it can bypass UFW rules. By default, Docker-published ports are accessible from any network regardless of your UFW config. This is a well-known Docker gotcha. To prevent it, either bind to localhost ("127.0.0.1:25565:25565") and use a reverse proxy, or configure Docker’s iptables behaviour in /etc/docker/daemon.json with "iptables": false. Be aware that disabling Docker’s iptables management breaks container-to-container networking and outbound connectivity unless you write your own rules.

Advanced: JVM Tuning via Environment

The itzg image supports passing custom JVM flags through environment variables:

    environment:
      # ... other variables ...
      MEMORY: ""
      JVM_XX_OPTS: >-
        -Xmx4G -Xms2G
        -XX:+UseG1GC
        -XX:+ParallelRefProcEnabled
        -XX:MaxGCPauseMillis=200
        -XX:+UnlockExperimentalVMOptions
        -XX:+DisableExplicitGC
        -XX:G1NewSizePercent=30
        -XX:G1MaxNewSizePercent=40
        -XX:G1HeapRegionSize=8M
        -XX:G1ReservePercent=20
        -XX:InitiatingHeapOccupancyPercent=15
        -XX:G1MixedGCLiveThresholdPercent=90
        -XX:SurvivorRatio=32
        -XX:MaxTenuringThreshold=1

Set MEMORY to empty when using JVM_XX_OPTS to avoid conflicting heap settings. These are Aikar’s garbage collection flags, which are widely recommended in the Minecraft server community for reducing lag spikes caused by GC pauses.

Why Docker Is the Best Approach

After running Minecraft servers both ways, here is what you gain with Docker:

  • Clean separation – No Java installation on the host. No files scattered across the filesystem. Everything is in a container and a volume.
  • Reproducible setup – Your compose file is your documentation. Someone else can run the same server by cloning a single file.
  • Easy updates – One command. No hunting for JAR files, no worrying about Java version mismatches.
  • Multiple servers – Running several servers on different ports is trivial. Try doing that cleanly with bare-metal installations and systemd units.
  • Portability – Move the compose file and volume data to any Docker host and it works. Different OS, different hardware, does not matter.
  • Clean teardowndocker compose down -v removes everything. No orphaned config files, no leftover Java processes, no stale systemd units.

Docker Compose is how production applications are deployed across the industry. The workflow you have just learned – defining services in YAML, managing them with compose commands, persisting data in volumes, backing up, and updating – is identical to how teams manage databases, web applications, message queues, and monitoring stacks. You are not just running a game server. You are practising the deployment patterns used in every modern infrastructure team.

Watch out: If players cannot connect from outside your network, check your router’s port forwarding. Minecraft uses TCP port 25565 by default, and you need to forward that to the Docker host’s IP, not the container’s internal IP.

Key Takeaways

  • The itzg/minecraft-server Docker image is the community standard, handling Java, server types, EULA, and configuration through environment variables
  • Use TYPE=PAPER for PaperMC and VERSION=LATEST or pin a specific version for stability
  • Bind-mount volumes (./data:/data) for easier backups and direct config file access
  • Running multiple servers is just a matter of different port mappings and separate volumes
  • Use RCON with save-all and save-off for safe backups of a running server
  • Updates are a single docker compose pull && docker compose up -d with world data preserved on the volume
  • Docker bypasses UFW by default. Be aware of this if your server faces the internet.

Related Guides

If you found this useful, these guides continue the journey:

The RTM Essential Stack - Gear I Actually Use

Enjoyed this guide?

New articles on Linux, homelab, cloud, and automation every 2 days. No spam, unsubscribe anytime.

Scroll to Top