Minecraft Server in Docker: the Compose File, Booted and Explained Line by Line
To run a Minecraft Java server in Docker, use the itzg/minecraft-server image: save the compose file below as compose.yaml in an empty directory and run docker compose up -d. In our test the server was ready 28 seconds later, when the log printed Done.
We ran it on 4 October 2026 with the image built the day before: a Paper server was booted, written to, stopped, restarted, upgraded from Minecraft 26.2 to 26.3, backed up, deliberately destroyed and restored from that backup, and finally squeezed until the kernel killed it. Every log line, timing and error message below comes from that session. Where a statement comes from the image's documentation instead, it says so.
The compose file
name: minecraft
services:
mc:
image: itzg/minecraft-server:java25
container_name: mc
environment:
EULA: "TRUE"
TYPE: "PAPER"
VERSION: "26.3"
MEMORY: "1G"
TZ: "Europe/Warsaw"
ports:
- "25565:25565/tcp"
volumes:
- mcdata:/data
restart: unless-stopped
stop_grace_period: 2m
tty: true
stdin_open: true
deploy:
resources:
limits:
memory: 2g
volumes:
mcdata:
We ran exactly this file with three cosmetic differences, because it was someone's workstation: the project and container were called scwiki and scwiki-mc, and the port line was "127.0.0.1:25599:25565" so that the test server could not be reached from outside the machine. The first boot used VERSION: "26.2" and MEMORY: "1536M" on purpose, so that the update and memory sections further down could show a real before and after.
The file, line by line
name: minecraftis the project name. Compose prefixes the volume and the network with it, so the world ends up in a volume calledminecraft_mcdata. You will need that name for backups.image: itzg/minecraft-server:java25is the community image this whole page is about, pinned to the tag that ships Java 25. Minecraft 26.x needs Java 25 (see our Java 21 versus Java 25 guide); for a 1.20.5 to 1.21.x server usejava21. Thelatesttag also carries Java 25 today, but the documentation says it moves to whatever Java the newest Minecraft requires, which is not what you want to discover after a reboot.container_name: mcgives the container a fixed name, sodocker exec mc rcon-cli listworks without looking anything up.EULA: "TRUE"accepts the Minecraft EULA. Without it the container prints an error and exits with code 1; the exact output is in the troubleshooting section. Keep the quotes: unquoted, YAML turnsTRUEinto a boolean and some tooling then passes it on in a form the image does not expect.TYPE: "PAPER"picks the server software. The default isVANILLA; how to choose is under Choosing TYPE below.VERSION: "26.3"pins the Minecraft version. It takesLATEST(the default),SNAPSHOT, or a pin such as26.3or1.21.11, and the documentation is explicit that withLATEST"an upgrade can be performed by simply restarting the container". A world that has been opened by a newer version does not go back, so a pin is the difference between an upgrade you chose and one you got.MEMORY: "1G"is the Java heap, both-Xmsand-Xmx; it defaults to1Gand acceptsM,Gor a percentage. It is not the container's memory. We started at1536Mand measured why that was too much for a 2 GB container; the numbers are in the memory section.TZ: "Europe/Warsaw"sets the timezone, so the log timestamps and the backup file names match your clock. Without it everything is in UTC. Pick your own.ports: "25565:25565/tcp"publishes the game port as host:container. Only the game port is published. RCON stays inside the container on purpose; the console section explains why.volumes: mcdata:/datamounts a named volume at/data, which is where the image keeps everything: the server jar, the world,server.properties, plugins, logs. A bind mount such as./data:/dataworks too and makes the files easy to browse; the permission problem it can cause is in the troubleshooting section.restart: unless-stoppedbrings the server back after a crash or a host reboot, but not after you stop it yourself. It also rescued one of our boots from a failed download, described in the troubleshooting section.stop_grace_period: 2mis how long Docker waits between asking the container to stop and killing it. The Compose default is 10 seconds, while the image's own shutdown routine waits up to 60 seconds for the server to save, so the default lets Docker kill the save halfway. Measured timings are in the stop section.tty: trueandstdin_open: trueletdocker compose attach mcshow the live server console. They cost nothing when you do not use them.deploy.resources.limits.memory: 2gis the container's hard memory ceiling, applied by a plaindocker compose up:docker inspectshowedMemory=2147483648on the running container. Without it the server can eat the host's memory; cross it and the kernel kills the Java process, not gracefully.volumes: mcdata:at the bottom declares the named volume so Compose creates and tracks it.
What you will see on first boot
docker compose up -d returned after 1.6 seconds. That is the container being created, not the server being ready. Follow the log with docker compose logs -f. This is the first boot, trimmed to the lines that matter, with the Compose prefix removed:
[init] Running as uid=1000 gid=1000 with /data as 'drwxr-x--- 2 1000 1000 4096 Oct 2 14:04 /data'
[init] Image info: buildtime=2026-10-03T21:25:57.319Z,version=java25,revision=b94d51517ddf1b87e71a2ece5f3e7151efdf9f64
[init] Resolving type given PAPER
[mc-image-helper] 17:44:08.448 INFO : Downloaded /data/paper-26.2-129.jar
[init] Creating server properties in /data/server.properties
[mc-image-helper] 17:44:11.116 INFO : Created/updated 4 properties in /data/server.properties
[init] Setting initial memory to 1536M and max to 1536M
[init] Starting the Minecraft server...
Downloading mojang_26.2.jar
Applying patches
Starting org.bukkit.craftbukkit.Main
[17:44:20 INFO]: [bootstrap] Running Java 25 (OpenJDK 64-Bit Server VM 25.0.4.1+1-LTS; Eclipse Adoptium Temurin-25.0.4.1+1) on Linux 6.17.0-41-generic (amd64)
[17:44:20 INFO]: [bootstrap] Loading Paper 26.2-129-ver/26.2@9240f58 (2026-09-23T18:49:01Z) for Minecraft 26.2
[17:44:23 INFO]: No existing world data, creating new world
[17:44:24 INFO]: Starting minecraft server version 26.2
[17:44:25 INFO]: Starting Minecraft server on *:25565
[17:44:25 INFO]: Preparing level "world"
[17:44:30 INFO]: Prepared spawn area in 4582 ms
[17:44:30 INFO]: Done preparing level "world" (4.835s)
[17:44:30 INFO]: Starting remote control listener
[17:44:30 INFO]: RCON running on 0.0.0.0:25575
[17:44:30 INFO]: Done (10.795s)! For help, type "help"
The line that means the server accepts players is the last one: Done (10.795s)! For help, type "help". The 10.8 seconds is the server's own count from the moment Java started. From docker compose up -d to that line took 28 seconds: two seconds for the container to come up, four more to download the 62 MB Paper jar, a few seconds for Paper to fetch the matching Mojang jar and apply its patches, then the world generation you see above. The spawn area took 4.6 seconds because the world was new. The second boot of the same volume took 17 seconds and reported Done (6.828s): no download, the jar was already in /data, and the spawn chunks already existed.
The first line shows the server running as user 1000, not root. That is the default and it is why bind mounts need the right owner. The image also wrote server.properties itself, and the Paper configuration files were downloaded as defaults. The image's documentation says the container carries a health check that queries the server status, and that docker ps shows a status like Up 41 seconds (healthy) once the start period of one minute has passed; we did not rely on it and watched for the Done line instead.
Other settings worth knowing
| Setting | What it does | Value used here | What goes wrong if you get it wrong |
|---|---|---|---|
STOP_DURATION |
Image: how long its wrapper waits for the server to finish saving after sending stop. Default 60 seconds. |
default | Only effective if stop_grace_period is at least as long; otherwise Docker kills the container first. |
RCON_PASSWORD |
Password for the RCON console. Randomly generated when not set, and written where the bundled rcon-cli finds it. |
not set | If you publish port 25575 you must set a strong one. If you do not publish it, the random one is fine and never leaves the container. |
UID and GID |
The user the server runs as. Default 1000 and 1000. | default | A bind mount owned by another user makes the image fail at the EULA file, before the server even starts. |
MOTD, MAX_PLAYERS, DIFFICULTY, VIEW_DISTANCE and the rest of server.properties |
The image maps variables to properties and writes them at each start. See the server.properties reference for what the values mean. | none set; defaults were max-players=20, view-distance=10, difficulty=easy |
A property you set as a variable is rewritten on every start, so editing it in the file is pointless. One you never set as a variable survives hand edits, as the persistence section shows. |
USE_AIKAR_FLAGS |
Applies the usual G1 tuning flags. Default false. |
default | Not harmful, but it changes nothing about the heap size question. Read Aikar's flags before switching it on. |
MEMORY versus the container limit, measured
MEMORY is the Java heap. The container limit is everything the process touches: heap, class metadata, thread stacks, the garbage collector's own tables, compressed class space, native buffers for networking and compression. The image's documentation says the memory settings "only set the Java heap limits" and that the container's limit "should also account for non-heap memory usage", suggesting 25 percent extra. Our RAM and sizing guide explains heap versus total footprint in depth.
First run, MEMORY: "1536M" in a 2 GB container, nobody online, eight minutes after boot: docker stats reported 2.033GB / 2.147GB, 94.7 percent. Inside the container, the Java process had 1.98 GB of anonymous memory resident, and the cgroup's memory.peak equalled the limit exactly. The server was alive only because the kernel kept evicting the file cache. A single player, a plugin or a generous view distance would have pushed it over. In MiB: the heap was 1536, the process was 1938 resident, so the overhead on Java 25 with Paper 26.2 was about 400 MiB, and the documentation's 25 percent rule, which budgets 1920 MiB for that heap, was already slightly exceeded with nobody online.
Second run, MEMORY: "1G", same container: 1.53GB / 2.147GB, 71 percent, with the Java process at 1458 MiB resident. Roughly 430 MiB above the heap again, and this time with over half a gigabyte of real headroom. The rule that falls out of the two measurements: on a small server give the heap about half of the container limit, and never more than two thirds. If you want a 2 GB heap, give the container 3 GB. If the kernel does kill it, the signature is in the troubleshooting section, because it does not look like a Java out of memory error.
The console: rcon-cli from the host
The image ships rcon-cli and, when you leave RCON_PASSWORD unset, generates a 24 character password, writes it into server.properties as rcon.password and into /data/.rcon-cli.env, where the tool reads it. So the console needs no password from you and no published port. These are real exchanges:
$ docker compose exec mc rcon-cli list
There are 0 of a max of 20 players online:
$ docker compose exec mc rcon-cli whitelist add Notch
Added Notch to the whitelist
$ docker compose exec mc rcon-cli whitelist list
There are 1 whitelisted player(s): Notch
$ docker compose exec mc rcon-cli say hello from the host
$ docker compose exec mc rcon-cli seed
Seed: [-6405006804593860432]
docker exec mc rcon-cli list does the same thing without Compose. For an interactive session use docker exec -i mc rcon-cli; it shows a > prompt and runs each line you type. The documentation says the -i is required for interactive use and not needed for one shot commands, and that is exactly how it behaved. With tty and stdin_open in the file you can also use docker compose attach mc for the raw server console, detaching with Control-p Control-q (from the documentation; we used rcon-cli throughout).
Negative coordinates need care. Our first attempt at rcon-cli forceload add -256 -256 255 255 came back with unknown shorthand flag: '2' in -256, because rcon-cli parsed the coordinate as one of its own options. Either put a double dash (--) between rcon-cli and the command, which tells it that everything after is the command, or quote the whole command as one argument:
$ docker compose exec mc rcon-cli "setblock -5 201 -5 minecraft:gold_block"
Changed the block at -5, 201, -5
$ docker compose exec mc rcon-cli "execute if block -5 201 -5 minecraft:gold_block"
Test passed
RCON listens on 25575 inside the container, and because the compose file does not publish it, docker port mc lists only the game port and a connection to 127.0.0.1:25575 on the host is refused. That is the right default. If you need RCON from another machine, read the RCON setup guide first and set a real password.
A graceful stop, timed
What happens on docker compose stop: Docker sends the stop signal, the image's wrapper (mc-server-runner) connects to RCON and issues stop, the server saves players and worlds, the Java process exits, and the wrapper exits after it. This is the log of the first stop, on a fresh world with nobody online:
17:52:13.163 INFO mc-server-runner gracefully stopping server...
2026/10/04 17:52:13 Stopping with rcon-cli
[17:52:13 INFO]: [Rcon: Stopping the server]
17:52:13.168 INFO mc-server-runner Waiting for completion...
[17:52:13 INFO]: Stopping server
[17:52:13 INFO]: Saving players
[17:52:13 INFO]: Saving worlds
[17:52:13 INFO]: Saving chunks for level 'ServerLevel[world]'/minecraft:overworld
[17:52:13 INFO]: [ChunkHolderManager] Saved 729 block chunks, 25 entity chunks, 0 poi chunks in world 'minecraft:overworld' in 0.13s
[17:52:13 INFO]: ThreadedAnvilChunkStorage: All dimensions are saved
[17:52:13 INFO]: Waiting for all RegionFile I/O tasks to complete...
[17:52:13 INFO]: All RegionFile I/O tasks to complete
17:52:13.842 INFO mc-server-runner Done
From "gracefully stopping" to "Done" took 0.68 seconds; docker compose stop returned after 0.95 seconds, and docker inspect showed exit code 0. We then force loaded 1024 chunks to make the save heavier and stopped again: 1.37 seconds in total, with the save line reading Saved 3351 block chunks, 1273 entity chunks, 25 poi chunks in world 'minecraft:overworld' in 0.59s. The gold block placed with setblock was there after every restart.
Those stops took about a second, and the grace period is still two minutes, because the cost is lopsided. A long grace period costs nothing: Docker returns the instant the process exits, 0.95 seconds here with two minutes allowed. A short one costs the last save. Our test world was tiny and on an idle NVMe disk; a real server with twenty players, a dozen plugins flushing their databases, and tens of thousands of loaded chunks takes considerably longer than a second, and the image itself budgets 60 seconds for it. With the Compose default of 10 seconds, Docker kills the container while that budget is still running. We saw what the kill looks like once, when we stopped with a one second grace period while the server was still starting: the runtime reported that the stop signal failed to stop the container in 1 second and resorted to SIGKILL, docker inspect showed ExitCode=137, and the log ended without the wrapper's Done line. If you ever see 137 after a stop, that is what happened to your save.
Persistence: what down does and what down -v does
Everything lives in /data, which is the named volume. We placed a gold block at 0, 200, 0, whitelisted a player, edited motd in server.properties by hand, then ran docker compose down. It stopped and removed the container and the network, and docker volume ls still listed scwiki_mcdata. docker compose up -d created a new container on the same volume, booted in 17 seconds without downloading anything, and:
$ docker compose exec mc rcon-cli execute if block 0 200 0 minecraft:gold_block
Test passed
$ docker compose exec mc rcon-cli whitelist list
There are 1 whitelisted player(s): Notch
$ docker exec mc grep ^motd= /data/server.properties
motd=edited by hand in the volume
The hand edit survived. The image logged Created/updated 1 property in /data/server.properties on every start, so it does touch the file each time, but it left our motd alone because no variable claimed it. The documentation describes the mechanism the other way round: every server.properties entry can be managed by a variable, and a managed entry is written at each start. Set MOTD in the compose file and the variable wins on every start instead. Pick one place for each setting and stay with it.
docker compose down -v is a different command. It removes the containers, the network and every named volume declared in the file, with no confirmation. For this file that means all of /data: the jar, the world, the whitelist, the properties and the logs. It does not touch a bind mounted directory, which is another argument for bind mounts. We ran it once, deliberately, in the restore test below, and the output was two lines, Volume scwiki_mcdata Removing and Volume scwiki_mcdata Removed, after which docker volume ls showed nothing. Never type it without a backup you have tested.
Updating: change VERSION, recreate, and what to back up first
The update is one line in the file and one command. We changed VERSION: "26.2" to "26.3" (and MEMORY to 1G at the same time), ran docker compose up -d, and Compose noticed the configuration had changed and printed Recreate, Recreated, Starting, Started. The log:
[init] Resolving type given PAPER
[mc-image-helper] 17:54:59.628 INFO : Downloaded /data/paper-26.3-151.jar
[mc-image-helper] 17:54:59.660 INFO : Removing 28 deprecated installed libraries
[mc-image-helper] 17:54:59.686 INFO : Removing old file paper-26.2-129.jar
[init] Setting initial memory to 1G and max to 1G
[init] Starting the Minecraft server...
Downloading mojang_26.3.jar
Applying patches
[17:55:10 INFO]: [bootstrap] Loading Paper 26.3-151-main@6e88e46 (2026-10-04T13:36:03Z) for Minecraft 26.3
[17:55:14 INFO]: Starting minecraft server version 26.3
[17:55:15 INFO]: Loading 25 persistent chunks for level 'minecraft:overworld'...
[17:55:16 INFO]: Done (6.953s)! For help, type "help"
Ready in 23 seconds, gold block and whitelist intact. Three things in that log decide what you back up first. The image deleted the old jar and 28 old libraries, so the previous version is no longer in /data. The world was opened and written by 26.3, so it is now a 26.3 world. And the Paper build it fetched was built that same afternoon, labelled 26.3.build.151-beta in the startup banner: with a pinned VERSION you still get the newest build of that version on every recreate, which is normally what you want but means two recreates a week apart can run different builds. PAPER_BUILD pins the build if you need that.
Rolling back is therefore not "change the number back". It is: stop, restore the backup taken before the upgrade, set VERSION back, start. Which is why the backup comes first, while the server is stopped, as in the next section. If a plugin or mod does not have a build for the new version yet, the server will tell you on start; our upgrade guide covers the checks to make before you touch the version line.
Backups you can restore
Cold backup, server stopped
The simplest correct backup of a named volume is a tar of /data taken from a throwaway container while the server is stopped:
docker compose stop
docker run --rm -v minecraft_mcdata:/data:ro -v "$PWD":/backup alpine \
tar czf /backup/mc-data-$(date +%F).tgz -C /data .
docker compose start
Replace minecraft_mcdata with your project name followed by _mcdata. On our fresh server this took 8 seconds and produced a 229 MB archive with 499 entries. Most of that is not the world: the world was 6.6 MB, the Paper jar 62 MB, the libraries directory 81 MB and cache 59 MB. All of it is reproducible except the world, the properties, the whitelist, the ops and your plugin folder, but a full copy of /data is small, it restores in one step, and it is what you want at two in the morning. The read only mount (:ro) is there so that a typo in the tar command cannot write into the live data. Keep a copy off the machine as well.
The restore, proven
A backup is a theory until it has been restored. After the upgrade we placed a second gold block at -5, 201, -5, so there was one block that existed before the backup and one that did not. Then:
docker compose down -v
docker compose create
docker run --rm -v minecraft_mcdata:/data -v "$PWD":/backup alpine \
tar xzf /backup/mc-data-2026-10-04.tgz -C /data
docker compose up -d
docker compose create makes the empty volume and the container without starting anything; the extract took 2 seconds; the image chowns /data to user 1000 at start, so ownership inside the archive is not a worry. After the boot:
$ docker compose exec mc rcon-cli execute if block 0 200 0 minecraft:gold_block
Test passed
$ docker compose exec mc rcon-cli "execute if block -5 201 -5 minecraft:gold_block"
Test failed
$ docker compose exec mc rcon-cli whitelist list
There are 1 whitelisted player(s): Notch
The block from before the backup is there, the block from after it is not, and the whitelist and the seed match. The archive still held the 26.2 jar while the file said 26.3, so the image downloaded 26.3 again and threw the old jar away again; a true rollback would have set VERSION back to 26.2 in the same step. This boot also hit a failed download first; see Download failed at start in the troubleshooting section.
Live copy, server running
Stopping the server for a nightly backup is not always acceptable. The classic sequence turns autosave off, forces a full save to disk, copies, and turns autosave back on. Over rcon-cli, verbatim:
$ docker compose exec mc rcon-cli save-off
Automatic saving is now disabled
$ docker compose exec mc rcon-cli save-all flush
Saving the game (this may take a moment!)Saved the game
$ docker exec mc tar czf - -C /data world > world-live-$(date +%F).tgz
$ docker compose exec mc rcon-cli save-on
Automatic saving is now enabled
The copy took 0.17 seconds for a 1 MB world and 83 files. The two responses that matter are Saved the game before you copy and Automatic saving is now enabled after; if your script dies between them, the server runs without autosave until someone notices, so run save-on from a trap, not from the happy path. On Paper 26.3 all three dimensions lived under world/dimensions/minecraft/, so the one directory was the whole world; check your own layout with docker exec mc ls /data and include any separate dimension folders you find. World management covers what the files inside mean.
Choosing TYPE
TYPE is where you choose between a plugin server and a modded server, and that choice is bigger than anything else in the file. The image accepts VANILLA (the default), PAPER, SPIGOT, FABRIC, FORGE, NEOFORGE and more. Paper is the right default for a survival server with friends, which is why it is in the file above. If you are installing a modpack, the pack itself tells you whether it needs Fabric, Forge or NeoForge, and the image's TYPE values follow those names. Changing TYPE on an existing world is a software migration, not a setting: plugins do not survive a move to a mod loader, and mods do not survive the move back. Read the server software comparison and come back with a decision.
Troubleshooting from what went wrong
The container exits immediately: EULA
Run without EULA, or with EULA=false, the output is identical and the exit code is 1:
[init] Running as uid=1000 gid=1000 with /data as 'drwxr-x--- 2 1000 1000 4096 Oct 2 12:04 /data'
[init] Image info: buildtime=2026-10-03T21:25:57.319Z,version=java25,revision=b94d51517ddf1b87e71a2ece5f3e7151efdf9f64
[init]
[init] [ERROR] Please accept the Minecraft EULA at
[init] [ERROR] https://account.mojang.com/documents/minecraft_eula
[init] [ERROR] by adding the following immediately after 'docker run':
[init] [ERROR] -e EULA=TRUE
In Compose that is the EULA: "TRUE" line. With a restart policy the container will loop on this, so docker compose ps shows it restarting and docker compose logs shows the block above repeated.
Port already in use
We started a second container publishing the same host port while the first was running. The container was created but never started, and the error named the port: Failed to bind port 25599 (Address already in use). Docker's wording differs slightly and says the bind for the address failed because the port is already allocated, but the shape is the same: the host side of the ports line is taken. Find out by whom with ss -ltnp | grep 25565; it is usually a previous server, a Bedrock proxy or an old container that was never removed. Change the left side of the mapping, for example "25566:25565/tcp", and players connect with that port in the address. The right side stays 25565 because that is what the server inside listens on.
Permission denied on a bind mount
With a bind mount the image first tries to take ownership of the directory and then runs as user 1000. If it cannot write there, the failure is immediate and clear. We mounted a directory without write permission:
[init] Changing ownership of /data to 1000 ...
[init] Running as uid=1000 gid=1000 with /data as 'dr-xr-xr-x 2 1000 1000 4096 Oct 4 15:55 /data'
/image/scripts/start-utils: line 527: /data/eula.txt: Permission denied
[init] [ERROR] Unable to write eula to /data. Please make sure attached directory is writable by uid=1000
Exit code 2. The fix is to make the directory writable by that user: sudo chown -R 1000:1000 ./data, or set UID and GID in the environment to your own ids so the server runs as you and the files on the host are yours. On an SELinux host the documentation says to append :Z to the volume mapping. A named volume, as in the file above, never has this problem.
The server vanished: killed by the memory limit
To see this on purpose we ran a server with MEMORY=768M inside a 768 MB container, the mistake of setting the heap equal to the limit. It did not die at once. It ran for 35 minutes at 97 to 98 percent of its limit with the kernel evicting the file cache around it, through the generation of two thousand chunks, and died only when we moved the budget by 256 MB. In real life players and plugins move the budget for you. The signature when it happens:
$ docker inspect mc --format "{{.State.Status}} exit={{.State.ExitCode}} OOMKilled={{.State.OOMKilled}}"
exited exit=255 OOMKilled=true
$ docker logs mc | tail -2
WARN mc-server-runner Minecraft server failed. Inspect logs above for errors that indicate cause. DO NOT report this line as an error. {"exitCode": -1}
INFO mc-server-runner Done
$ journalctl -k | grep -i "out of memory"
Memory cgroup out of memory: Killed process 149036 (java) total-vm:7851656kB, anon-rss:490192kB, file-rss:32200kB, shmem-rss:0kB
Three things to recognise. There is no Java stack trace and no OutOfMemoryError in the server log; the log just stops mid sentence and the wrapper notes that the server failed. OOMKilled=true in docker inspect is the reliable signal. And the kernel log names the process and how much anonymous memory it held when it was killed. The fix is in the memory section: lower MEMORY or raise the limit until the heap is at most two thirds of the container. If the server instead prints java.lang.OutOfMemoryError: Java heap space and writes a crash report, that is the other problem, a heap that is too small for the world, and the sizing guide covers it.
Download failed at start
On the restore boot the first attempt to download Paper hung for two and a half minutes and failed with 'install-paper' command failed and io.netty.handler.timeout.ReadTimeoutException, after which the container exited: the image could not reach the download API for the server jar. restart: unless-stopped started it again, the second attempt downloaded in a minute, and the server came up about five minutes after up -d. If it keeps failing, check that the container has internet access and that the project's download service is up. A pinned version does not make you independent of the download servers when the jar is not already in /data. A server whose jar is in /data and whose VERSION has not changed does not download anything, so pin the version and this only ever bites on a fresh install or an upgrade.
When Docker at home is the right tool, and when it is not
Docker on a machine you own is a good way to run a Minecraft server for a few friends. The file above gives you a reproducible server, a clean upgrade path, a backup that is one command, and a shutdown that saves the world properly, for the cost of the electricity.
What it does not give you is everything around the container. There is no public address: the server sits behind your router, so you forward a port, your home IP changes, and every change is a message to the group. There is nothing between the internet and that port: no DDoS filtering, so one annoyed player with a cheap DDoS service takes down the server and your household's connection with it. And there is nobody on call: when the server is killed at three in the morning because the heap crept past the limit, it stays down until you notice.
If the server is for a community rather than a friend group, if it has to be reachable by people you do not know, or if the cost of it being down is a problem, the container is the easy part and the rest is the job. Choosing a Minecraft host lays out what that job involves and what you are paying for when someone else does it.
Related guides
- How much RAM a Minecraft server needs: heap versus total memory
- Which server jar to run: vanilla, Paper, Purpur, Spigot, Fabric, Forge, NeoForge
- RCON setup and security
- Upgrading a Minecraft server without losing the world
- World management and the files inside a world
- server.properties reference
- Aikar's flags for Paper and Spigot
- Headless monitoring for a dedicated server
- Common Minecraft server errors
Run the same Paper, Fabric, Forge or NeoForge server on Supercraft Minecraft hosting, with a public address, DDoS protection, daily backups, full file access and 4 region options, and keep the compose file for your test world at home.