Skip to content

How to Deploy a Minecraft Server on a VPS

Updated Oct 2026

verified on Ubuntu 26.04 · Oct 2026
We earn commissions when you shop through the links below. Full disclosure →

Run 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.

Before you start
  • 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
Need a box for this guide? Kamatera's free tier lets you spin one up now.Start free on Kamatera → (opens in new tab)

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.

Where to host itaffiliate disclosure
Hetzner Cloudrun it on
2 vCPU · 4 GB RAM · 80 GB SSD · $23.59/mo
Get Hetzner Cloud (opens in new tab)
Kamaterafree trial
1 vCPU · 1 GB RAM · 20 GB SSD · $4.00/mo
Start free on Kamatera → (opens in new tab)
DigitalOceanalso works on
1 vCPU · 1 GB RAM · 25 GB SSD · $6.00/mo
Deploy on DigitalOcean → (opens in new tab)

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.

Next steps

Automatic HTTPS with Caddy →Run Claude Code with Ollama on Your Own VPS →Deploy Coolify on a VPS →How to Deploy Actual Budget on a VPS →How to Deploy AFFiNE on a VPS →How to Deploy AnythingLLM on a VPS →How to Deploy Appwrite on a VPS →How to Deploy Audiobookshelf on a VPS →How to Deploy Authelia on a VPS →How to Deploy authentik on a VPS →How to Deploy Baserow on a VPS →How to Deploy Beszel on a VPS →How to Deploy Bitwarden on a VPS →How to Deploy BookStack on a VPS →How to Deploy CapRover on a VPS →How to Deploy Checkmate on a VPS →How to Deploy copyparty on a VPS →How to Deploy Directus on a VPS →How to Deploy Discourse on a VPS →How to Deploy docker-mailserver on a VPS →How to Deploy Docmost on a VPS →How to Deploy Dokku on a VPS →How to Deploy Dokploy on a VPS →How to Deploy Firefly III on a VPS →How to Deploy Flarum on a VPS →How to Deploy Forgejo on a VPS →How to Deploy Gatus on a VPS →How to Deploy Ghost on a VPS →How to Deploy Ghostfolio on a VPS →How to Deploy Gitea on a VPS →How to Deploy a GitHub Actions Self-Hosted Runner on a VPS →How to Deploy GitLab on a VPS →How to Deploy GlitchTip on a VPS →How to Deploy Grafana on a VPS →How to Deploy Graylog on a VPS →How to Deploy Headscale on a VPS →How to Deploy Healthchecks on a VPS →How to Deploy Home Assistant on a VPS →How to Deploy Immich on a VPS →How to Deploy Infisical on a VPS →How to Deploy Invoice Ninja on a VPS →How to Deploy Jan on a VPS →How to Deploy Jellyfin on a VPS →How to Deploy Kanboard on a VPS →How to Deploy Karakeep on a VPS →How to Deploy Keycloak on a VPS →How to Deploy Leantime on a VPS →How to Deploy LibreChat on a VPS →How to Deploy Linkwarden on a VPS →How to Deploy LocalAI on a VPS →How to Deploy Mailcow on a VPS →How to Deploy Mailu on a VPS →How to Deploy Matomo on a VPS →How to Deploy Mattermost on a VPS →How to Deploy Meilisearch on a VPS →How to Deploy Memos on a VPS →How to Deploy n8n on a VPS →How to Deploy Navidrome on a VPS →How to Deploy NetBird on a VPS →How to Deploy Netdata on a VPS →How to Deploy Nextcloud on a VPS →How to Deploy Next.js to a VPS →How to Deploy Nginx Proxy Manager on a VPS →How to Deploy NocoDB on a VPS →How to Deploy ntfy on a VPS →How to Deploy Odoo on a VPS →How to Deploy Ollama on a VPS →How to Deploy ONLYOFFICE Docs on a VPS →How to Deploy Open WebUI on a VPS →How to Deploy OpenHands on a VPS →How to Deploy OpenObserve on a VPS →How to Deploy OpenProject on a VPS →How to Deploy Outline on a VPS →How to Deploy Pangolin on a VPS →How to Deploy Paperless-ngx on a VPS →How to Deploy Passbolt on a VPS →How to Deploy Plane on a VPS →How to Deploy Plausible Analytics on a VPS →How to Deploy Plex Media Server on a VPS →How to Deploy Pocket ID on a VPS →How to Deploy PocketBase on a VPS →How to Deploy Prometheus on a VPS →How to Deploy Psono on a VPS →How to Deploy Radarr on a VPS →How to Deploy Rocket.Chat on a VPS →How to Deploy SigNoz on a VPS →How to Deploy Sonarr on a VPS →How to Deploy Stalwart on a VPS →How to Deploy Stirling-PDF on a VPS →How to Deploy Supabase on a VPS →How to Deploy Synapse on a VPS →How to Deploy Taiga on a VPS →How to Deploy TeamPass on a VPS →How to Deploy Tinyauth on a VPS →How to Deploy Traefik on a VPS →How to Deploy Trilium on a VPS →How to Deploy Twenty CRM on a VPS →How to Deploy Umami on a VPS →How to Deploy Uptime Kuma on a VPS →How to Deploy Vaultwarden on a VPS →How to Deploy Vikunja on a VPS →How to Deploy wg-easy on a VPS →How to Deploy Wiki.js on a VPS →How to Deploy WordPress on a VPS →How to Deploy Zabbix on a VPS →How to Deploy Zitadel on a VPS →How to Deploy Zulip on a VPS →Docker & Compose on Ubuntu 26.04 →Building AI Workflows with n8n →Install Open WebUI with Ollama →Adding AI-Powered Insights to Plausible Analytics →Building AI-Powered Apps with Supabase and pgvector →

We use analytics cookies (Google Analytics, PostHog) to see which guides are useful. No ad networks, no cross-site tracking. See our privacy policy.