Skip to content
Guide

Run a Minecraft server in Docker on Linux, step by step

A complete itzg/minecraft-server setup with Docker Compose or Podman: the compose file explained, first start, RCON, backups, safe upgrades, and the errors you will hit.

· 8 min read

Why a container instead of a bare java -jar

A Minecraft server is one Java process and one directory. That is simple enough to run directly, until you hit the parts that are not the server: the right Java for the Minecraft version (17 for 1.17 through 1.20.4, 21 from 1.20.5 on), a restart when it crashes at 3 a.m., a clean way to move to the next release, and a backup that captures everything that matters. A container packages those concerns.

  • Java is the image's problem. The itzg/minecraft-server image ships the JDK the requested version needs, so a version bump never turns into a system Java upgrade.
  • Restarts are a policy, not a script. restart: unless-stopped brings the server back after a crash or a host reboot.
  • Upgrades are a one-line change. Change VERSION, recreate the container, and the image downloads the matching server jar.
  • State lives in one place. Everything the server writes goes under /data. Back up that directory and you have the world, configs, whitelist, and plugin data.

The image also handles the details you would otherwise script yourself: EULA acceptance, server.properties generation from environment variables, and a graceful stop when the container receives SIGTERM.

Before you start

You need a Linux host with Docker Engine and the Compose plugin, or Podman 4.x or newer. Check with docker compose version or podman --version. Two TCP ports matter: 25565 is the game port and the only one players need. 25575 is RCON, the admin protocol; keep it on the host's loopback interface and never forward it to the internet.

Memory sizing is mostly about player count and the size of the loaded world. As a starting point for a vanilla server:

PlayersHeap (MEMORY)Host RAM to leave free
1 to 52Gat least 3 GB
5 to 104Gat least 5 GB
10 to 206Gat least 8 GB
Modded (Forge or Fabric packs)6G to 10Gheap plus 2 GB

The JVM uses memory beyond the heap, so the heap must be smaller than what the container is allowed. Going above roughly 12G on a stock JVM rarely helps and lengthens garbage-collection pauses.

The compose file, line by line

Create a directory, save this as compose.yaml, and read the notes underneath before starting it.

yaml
services:
  mc:
    image: itzg/minecraft-server:latest
    container_name: minecraft-server
    environment:
      EULA: "TRUE"
      TYPE: "VANILLA"
      VERSION: "1.21.1"
      MEMORY: "4G"
      TZ: "America/New_York"
      ENABLE_RCON: "true"
      RCON_PASSWORD: "change-this-to-something-long"
      MOTD: "Friends server"
    ports:
      - "25565:25565"
      - "127.0.0.1:25575:25575"
    volumes:
      - ./data:/data
    restart: unless-stopped
    stop_grace_period: 2m
    tty: true
    stdin_open: true
    healthcheck:
      test: mc-health
      start_period: 1m
      interval: 5s
      retries: 20
  • EULA: "TRUE" is required. Without it the container prints a notice and exits. Setting it means you accept Mojang's EULA.
  • TYPE picks the server software. VANILLA is the default; PAPER, PURPUR, FABRIC, FORGE, and NEOFORGE are the common alternatives. Pin one deliberately.
  • VERSION is the Minecraft version. The default is LATEST, which is convenient on day one and a surprise on the day a new release drops and your friends' clients have not updated. Pin it.
  • MEMORY sets both initial and maximum heap. The default is 1G, too small for anything beyond a solo test.
  • TZ is the JVM's time zone, so log timestamps match your clock. The default is UTC.
  • ENABLE_RCON and RCON_PASSWORD: RCON is on by default in this image, and if you do not set a password the image generates a random one at each start. Set your own so tools outside the container can authenticate.
  • ports publishes the game port on all interfaces. RCON is bound to 127.0.0.1 on the host, so only processes on the host machine can reach it.
  • volumes maps ./data on the host to /data in the container. This is the only path you need to back up.
  • restart: unless-stopped restarts after a crash or a host reboot, but respects an explicit docker compose stop.
  • stop_grace_period is how long Docker waits after SIGTERM before it kills the process. The default is 10 seconds, which is not enough for a large world to save. Two minutes is safe.
  • tty and stdin_open let you use docker attach to reach the console directly if RCON is ever unavailable.
  • healthcheck: mc-health is bundled in the image and performs a real status ping, so docker ps shows healthy only when players could actually connect. The image already declares a healthcheck; this block makes the timing explicit.

The same thing on Podman

Podman runs the identical image and compose file. The differences are about who owns the process.

  • Rootless Podman, the default on Fedora, works here: 25565 is above the unprivileged port limit, so no root and no sysctl change are needed.
  • podman compose up -d delegates to podman-compose or docker-compose, whichever is installed. Install one of them first.
  • File ownership: the image runs as UID 1000 inside the container. In a rootless setup that UID is mapped into your user's subordinate range, so a bind-mounted ./data can show odd ownership on the host. Add userns_mode: "keep-id" to the service, or use a named volume instead of a bind mount. On SELinux systems, append :Z to the bind mount (./data:/data:Z).
  • For a server that should start at boot without a compose tool, write a Quadlet unit at ~/.config/containers/systemd/minecraft.container with the same image, environment, ports, and volume, then run systemctl --user daemon-reload and systemctl --user start minecraft. Enable lingering (loginctl enable-linger) so it starts without you logged in.

First start, logs, and letting players in

  1. Start it: docker compose up -d (or podman compose up -d).
  2. Follow the log: docker compose logs -f mc. The first start downloads the server jar and generates the world. Wait for a line like Done (12.345s)! For help, type "help".
  3. Confirm health: docker ps should show (healthy) within a minute or two.
  4. From another machine on your LAN, add a server using the host's local IP (ip -4 addr on the host) and port 25565.
  5. For friends outside your network, forward TCP 25565 on your router to that same local IP, and give them your public IP or a DNS name. Forward only 25565.
  6. If the host runs a firewall, open the port: sudo ufw allow 25565/tcp, or sudo firewall-cmd --permanent --add-port=25565/tcp followed by sudo firewall-cmd --reload.

One Docker-specific caveat: Docker manages its own iptables chain for published ports, so a ufw rule that denies a port will not block a published one. The reliable way to keep a port local is to bind it to 127.0.0.1 in the compose file, as this guide does for RCON.

Day-two work: console, backups, upgrades

RCON from the host

RCON is how you administer the server without attaching a terminal to the process: whitelist, op, kick, ban, say, save-all, stop. The image bundles an rcon-cli binary that reads the password from the container's environment:

bash
docker exec -i minecraft-server rcon-cli list
docker exec -i minecraft-server rcon-cli whitelist add Steve
docker exec -i minecraft-server rcon-cli

The last form opens an interactive session. Any Source-RCON client on the host can also connect to 127.0.0.1:25575 with the password from your compose file. A console matters less for the commands themselves than for when you need them: a griefer online at midnight, a friend locked out by a whitelist typo. Having one a click or a command away is the difference between fixing it now and hearing about it tomorrow.

Backups: volume snapshot or in-game save

The whole server is ./data, so the simplest backup is a tar of the world inside it:

bash
docker exec -i minecraft-server rcon-cli save-off
docker exec -i minecraft-server rcon-cli save-all flush
tar -czf mc-backup-$(date +%Y%m%d-%H%M).tar.gz -C ./data world
docker exec -i minecraft-server rcon-cli save-on

save-off pauses the server's own writes, save-all flush forces everything to disk, and save-on resumes. Skipping those steps still produces a usable backup most of the time, but it is crash-consistent rather than clean: a chunk written mid-copy can come back inconsistent. A copy of the whole data directory with the server stopped is the safest option and the right one before an upgrade.

Upgrading the version

  1. Take a backup with the server stopped.
  2. Change VERSION in compose.yaml.
  3. Run docker compose up -d. Compose recreates the container, the image downloads the new server jar, and the world upgrades on first load.
  4. Watch the log for the Done line and join to verify.

World upgrades are one-way. If the new version misbehaves, restore the backup rather than setting VERSION back, because a world saved by a newer version will not load on an older one.

Where MineUI fits

Everything above works from a terminal, and MineUI does not replace the compose file. It is a native desktop app that attaches to this exact container and gives it a dashboard. In Advanced mode you enter the container name (minecraft-server is the default) and the RCON port and password; MineUI finds the container through the podman or docker CLI, Podman first. From there it:

  • starts, stops, and restarts the container, and streams logs --follow into a log viewer;
  • shows player count and version from a status ping on 25565, and builds join/leave history from the log;
  • connects to RCON on 127.0.0.1:25575 behind a command allowlist (list, whitelist, op, deop, kick, ban, pardon, say, save-all, stop, tps, and a few others);
  • edits server.properties and files under config/ inside /data;
  • lists jars in /data/mods and /data/plugins and lets you upload or download new ones;
  • creates .tar.gz backups of the world into /data/backups, and restores them with the server stopped, keeping the previous world alongside;
  • reads the container's CPU, memory, network, and block IO.

Two things it deliberately does not do: it never creates or pulls a container, and it does not manage the compose file. Both stay yours. The Getting Started page covers installation and the attach flow.

Troubleshooting

The container exits immediately with an EULA message. The log says the EULA must be accepted. Check that EULA: "TRUE" is under environment with the value quoted as a string; an unquoted TRUE in YAML is a boolean and some tooling rejects it.

bind: address already in use on 25565. Something else is listening, often an old container or a server you started by hand. sudo ss -ltnp | grep 25565 shows the process. Stop it, or change the host side of the port mapping ("25566:25565").

Can't keep up! Is the server overloaded? The server is missing ticks. First check that the host is not swapping (free -h) and the heap is not maxed. Then reduce VIEW_DISTANCE (10 is plenty for friends), enable USE_AIKAR_FLAGS: "true" for better garbage-collection tuning, and if the load is entity or redstone heavy, consider TYPE: "PAPER", which keeps vanilla behavior for players while doing less work per tick.

Chunks reset or the world fails to load after a hard stop. A power cut or a docker kill during a save can leave a region file half-written. Prevention is the stop_grace_period above and stopping with docker compose stop, never kill. Recovery is your most recent backup; failing that, deleting the specific corrupt region file named in the log costs those chunks but saves the world.

Next steps

Frequently asked questions

Do I need to install Java on the host?
No. The itzg/minecraft-server image bundles the Java runtime the requested Minecraft version needs and picks it for you. The host only needs Docker or Podman.
Can I run two servers on one machine?
Yes. Give each service its own container_name, its own host-side port (25565 and 25566, for example), and its own data directory. Players connect with host:port for the second one.
Is it safe to expose RCON so I can administer from elsewhere?
No. RCON sends the password in plain text and has no rate limiting. Keep it bound to 127.0.0.1 and reach it over SSH when you are away, either with rcon-cli through docker exec or by tunneling port 25575.
Does MineUI need Docker or Podman specifically?
Either. MineUI auto-detects the runtime through its CLI, trying Podman first and then Docker, and Settings has a manual override if both are installed. It attaches to an existing container and never creates one.