How to Deploy a Minecraft Server on a VPS
Updated Oct 2026
verified on Ubuntu 26.04 · Oct 2026Run a Minecraft Java server on a small VPS with the itzg Docker image — EULA acceptance, heap and container memory sizing, port 25565 and the Docker-versus-ufw gotcha, a whitelist, safe backups of the world, upgrades and troubleshooting.
- A VPS with 4 GB+ RAM for a small vanilla server (2 GB of Java heap plus overhead; more for mods or many players) and fast single-core performance
- A fresh Ubuntu 24.04 or 26.04 server with root/sudo SSH access
- A Minecraft: Java Edition account for each player, and a willingness to read and accept Mojang's EULA yourself
What this sets up
This guide runs a Minecraft: Java Edition server in Docker using the
community-maintained itzg/minecraft-server image. The image downloads the
official server from Mojang at startup, runs it on the right Java version,
handles the first-run files and exposes settings as environment variables, so
the whole server is one Compose file and one data volume. By default it is the
vanilla server; the same image can also run Paper, Fabric, Forge and others,
which is where you go after the basics work. The image's own documentation
(linked at the end) is the reference for every option.
This is not an app in the catalog, so the facts come from the image's documentation plus a verification run on a fresh Ubuntu server. It picks up from "you have a server" and ends with a running, whitelisted server, a backup that does not corrupt the world, and an upgrade routine. If you are still choosing a provider, see Best VPS for Minecraft for the ranked picks, and Best VPS for Docker for Docker-friendly tiers. The Java Edition server is separate from Bedrock; this guide covers Java only.
Server sizing
Minecraft's server is mostly single-threaded, so clock speed per core matters more than core count, and RAM matters in two layers.
The first layer is the Java heap, set with the MEMORY variable
(INIT_MEMORY and MAX_MEMORY set the two ends separately). The image
defaults to a 1 GB heap. The second is the container limit: the image's
documentation warns that heap settings only limit the heap, and that the
container also needs room for non-heap memory, suggesting an extra 25% as a
general rule. So a 2 GB heap belongs in a container limited to about 3 GB, on a
server with a little more than that for the operating system.
In this guide's verification run (a 2-vCPU, 8 GB test box, vanilla server, a
2 GB heap, a freshly generated world and no players connected),
docker stats showed the server container at about 1.5 GB (1.46 GiB
against its 3 GB limit), with the image taking about 1.3 GB on disk and the new
world about 130 MB. An idle empty world is the best case:
memory grows with connected players, loaded chunks, view distance and mods, so
treat the idle number as a floor, not a plan. A 4 GB VPS is the practical
minimum for a small friends-only server; a busy or modded server wants more.
Disk is modest to start and grows with the world as players explore. Size it for the world you expect, plus a couple of backups.
Prepare the server
Update the system and install the few tools this guide uses:
sudo apt-get update && sudo apt-get -y upgrade
sudo apt-get install -y curl openssl ufw
If you're logged in as root, create a non-root sudo user and continue as that user:
adduser deploy
usermod -aG sudo deploy
Then open SSH and the Minecraft port:
sudo ufw allow OpenSSH
sudo ufw allow 25565/tcp
sudo ufw --force enable
Now the important caveat. Docker publishes container ports by editing the
firewall rules itself, ahead of the rules ufw manages, so a port published
by Compose is reachable from the internet whether or not ufw has a rule for
it; on the test box for this guide, the port still answered from outside
after the ufw rule was deleted, with ufw active and denying incoming
traffic by default. The allow 25565/tcp line above documents your intent and is correct if
you ever run the server outside Docker, but it is not what is exposing the
port, and ufw deny would not close it. If you need to restrict who can
connect, use the whitelist (below), your provider's network firewall, or
Docker-aware rules in the DOCKER-USER chain. Never publish the RCON port
(25575); this guide leaves it unpublished.
Paid link — we earn a commission if you shop through it.
Install Docker
If Docker isn't on the server yet, the convenience script installs Docker Engine and the Compose plugin (the Docker and Compose on Ubuntu guide covers the details):
command -v docker >/dev/null || curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker "$USER"
sudo docker compose version
The group change applies on your next login; until then the commands below use
sudo docker.
Install the server
Create a project directory and a .env file for the two values Compose reads.
Each variable is added only if it is missing, so re-running the block never
changes an existing value, and the file is restricted to its owner because it
holds the RCON password:
mkdir -p ~/minecraft && cd ~/minecraft
touch .env && chmod 600 .env
grep -q '^RCON_PASSWORD=' .env || echo "RCON_PASSWORD=$(openssl rand -hex 16)" >> .env
grep -q '^EULA=' .env || echo "EULA=FALSE" >> .env
grep -c -E '^(RCON_PASSWORD|EULA)=' .env
The Compose file pins the image tag (so an upgrade is a deliberate edit) and the
Minecraft version, sets a 2 GB heap inside a 3 GB container limit, keeps the
world in a named volume, and publishes only the game port. The tty and
stdin_open lines let you attach to the server console later. The whitelist is
switched on from the start, which is the single best protection for a small
private server:
cd ~/minecraft
cat > docker-compose.yml <<'EOF'
services:
mc:
image: itzg/minecraft-server:2026.10.0-java25
restart: unless-stopped
tty: true
stdin_open: true
mem_limit: 3g
ports: ["25565:25565"]
environment:
EULA: ${EULA}
VERSION: "26.3"
MEMORY: 2G
MOTD: "My VPS server"
DIFFICULTY: normal
MAX_PLAYERS: "10"
ENABLE_WHITELIST: "TRUE"
ENFORCE_WHITELIST: "TRUE"
RCON_PASSWORD: ${RCON_PASSWORD}
volumes: ["mcdata:/data"]
volumes:
mcdata:
EOF
Accept the EULA yourself
The EULA value is deliberately left at FALSE, and the server will not start
until you change it. Mojang's end-user license agreement is a legal agreement
between you and Mojang, and the image requires it to be accepted explicitly
rather than assuming it. Read it at
minecraft.net/eula. If you accept, you
set the value yourself:
cd ~/minecraft
sed -i 's/^EULA=.*/EULA=TRUE/' .env
Then start the server and wait for it to finish generating the world. The
first start downloads the server and builds the world, which takes a minute or
two, so the loop is bounded rather than open-ended. The image ships a health
check built on mc-monitor, so waiting for healthy means the server is
answering the Minecraft protocol, not just that the container is up:
cd ~/minecraft
sudo docker compose up -d
status=starting
for i in $(seq 1 60); do
status=$(sudo docker inspect -f '{{.State.Health.Status}}' minecraft-mc-1)
[ "$status" = "healthy" ] && break
sleep 5
done
echo "Server health: $status"
sudo docker compose logs mc 2>&1 | grep -E 'Done \(' | tail -1
You should see Server health: healthy and a Done (…s)! For help, type "help" line from the server log. If the loop times out, jump to
Troubleshooting.
Check that it answers
Ask the server for its status the way a game client's server list would. This
uses mc-monitor, which the image already contains:
cd ~/minecraft
sudo docker compose exec -T mc mc-monitor status --host localhost --port 25565
It prints the server's address, version, player count and message of the day. From your own computer, you can also test that the port is reachable across the network, which is the check that your provider's firewall is open too:
nc -vz SERVER_IP 25565
Then add the server in Minecraft: Multiplayer, Add Server, and enter the
server's IP or domain (no port is needed for 25565). A domain is optional; a
plain A record pointing a name such as mc.example.com at the server gives
friends something easier to type. Minecraft does not use HTTPS, so there is
no certificate to set up.
Whitelist and operators
With ENABLE_WHITELIST on, only listed players can join. The image gives you
two ways to manage the list. For a fixed group, list names in the Compose
environment (use Java Edition profile names, not Xbox gamertags); the same
works for OPS, which grants administrator rights:
WHITELIST: |
FirstPlayer
SecondPlayer
OPS: |
FirstPlayer
For changes while the server is running, run commands through the image's
rcon-cli, which talks to the server over RCON from inside the container, so
the RCON port never has to be published:
cd ~/minecraft
sudo docker compose exec -T mc rcon-cli whitelist list
sudo docker compose exec -T mc rcon-cli list
To add a player, run whitelist add with their Java Edition name. It needs a
real account name, so substitute your own:
cd ~/minecraft
sudo docker compose exec -T mc rcon-cli whitelist add PLAYER_NAME
Keep the server's online-mode at its default of on. Turning it off lets anyone
connect under any name, which defeats the whitelist.
Backups
The world lives in the mcdata volume, which also holds server.properties,
and that file contains the RCON password. A world copied while the server is
writing can be corrupt, so the backup first tells the server to stop saving,
flushes everything to disk, archives the volume, then turns saving back on. The
volume name is the project directory name plus the volume name, here
minecraft_mcdata. The archive is restricted to its owner because of the
password:
cd ~/minecraft
sudo docker compose exec -T mc rcon-cli save-off
sudo docker compose exec -T mc rcon-cli save-all flush
sudo docker run --rm -v minecraft_mcdata:/data -v "$HOME":/backup alpine \
tar czf "/backup/minecraft-world-$(date +%F).tar.gz" -C /data .
sudo docker compose exec -T mc rcon-cli save-on
sudo tar czf ~/minecraft-config-$(date +%F).tar.gz docker-compose.yml .env
sudo chmod 600 ~/minecraft-world-*.tar.gz ~/minecraft-config-*.tar.gz
ls -lh ~/minecraft-world-*.tar.gz ~/minecraft-config-*.tar.gz
Copy these files off the server, to object storage or another machine,
anywhere that survives this VPS dying, and run the set from cron. If a
save-off ever fails midway, run save-on by hand, or the server stops saving
the world. A backup you have never restored is a guess, so check at least that
the archive holds the world:
sudo tar tzf ~/minecraft-world-$(date +%F).tar.gz | grep -E '(^|/)level\.dat$'
(sudo is needed because the archive is restricted to its owner, which is
root here.) That shows a level.dat file, which means the archive contains a world. It does
not prove a server boots from the copy. For a real restore onto a new server,
install Docker, unpack the config archive into ~/minecraft, create the
volume, unpack the world archive into it, then start the stack. Do that once on
a scratch box so you know it works.
Upgrades
Because the image tag and the Minecraft version are both pinned, an upgrade is
an edit. Take a backup first, because Minecraft world upgrades are one-way: a
world opened by a newer server cannot go back to an older one. Read the release
notes for the new version, change VERSION (and the image tag if you want a
newer image build) in docker-compose.yml, then pull and recreate:
cd ~/minecraft
sudo docker compose pull
sudo docker compose up -d
sudo docker compose logs --tail 20 mc
Players need a matching client version, so tell them before you upgrade. If you add mods or plugins, check that each supports the new version before you move. Keep the image tag's Java version in line with what the Minecraft version requires; the image's documentation lists which Java each tag provides.
Troubleshooting
The container exits immediately. Read sudo docker compose logs mc. The
usual cause is the EULA: EULA is still FALSE. Set it as described above
and run sudo docker compose up -d.
The container is killed or restarts, with exit code 137. The kernel or
Docker killed it for exceeding the memory limit. The heap is smaller than the
container limit, but the JVM also uses non-heap memory. Raise mem_limit, lower
MEMORY, or move to a larger server.
Players can't connect but the container is healthy. Check the layers in
order: the server answers locally (mc-monitor above), your provider's
network-level firewall allows inbound TCP 25565, and the player's client
version matches the server. Remember that ufw is not the control that matters
for a Docker-published port.
Players are refused with "You are not white-listed". Add them with
rcon-cli whitelist add NAME, using their Java Edition profile name.
The server lags ("Can't keep up!"). The server is CPU-bound, usually on a
single core. Lower VIEW_DISTANCE and SIMULATION_DISTANCE, remove heavy
plugins, or move to a plan with faster cores.
The world is gone after recreating the stack. The volume was removed
(docker compose down -v). Plain docker compose down keeps it; the -v flag
deletes the world.
Verification + next steps
Not exercised in our test run: the new-user step and the DNS check against your real domain (we ran on a throwaway box with a temporary hostname); joining from a real Minecraft client, so the client steps below are described, not observed.
You're done when mc-monitor reports your server, a whitelisted friend can
join from the public internet, docker compose ps shows the server healthy
after a reboot (restart: unless-stopped brings it back), and you have
checked a backup archive at least once.
From there, set the options you care about (game mode, seed, view distance) through environment variables, move to Paper or Fabric if you want plugins or mods, and automate the backup with cron. The image's documentation covers every variable, and Best VPS for Minecraft lists the server tiers we would start with.