Self-hosting Minecraft with Docker and optional web RCON
Minecraft is straightforward to self-host with itzg's Minecraft server image. This setup runs a private Java Edition, Forge 1.20.1 server on mc.example.com:65535, with persistent world data and an optional browser console behind Traefik.
“Private” here means authenticated, allowlisted players. The game port is still reachable wherever you expose the host; bind it to a LAN/VPN address if you want network-only access as well. This is not a Bedrock server.
Prepare the host
Use a Linux host with Docker Engine and the Compose plugin. This example gives Java a 10 GB heap and the container a 12 GB limit; leave additional RAM for the OS, Docker, and other services. These are starting allocations, not a player-capacity guarantee.
sudo install -d -o "$(id -u)" -g "$(id -g)" /srv/mc.example.com
cd /srv/mc.example.com
mkdir -p data mods-forge-lite backupsCreate .env in this directory, replacing every TODO_ value. Use Java Edition profile names, and grant operator privileges only to players who need them.
MC_PLAYERS=TODO_PLAYER1,TODO_PLAYER2
MC_OPS=TODO_PLAYER1
MC_RCON_PASSWORD=TODO_REPLACE_WITH_A_LONG_RANDOM_SECRETGenerate a password with openssl rand -hex 32, paste it into .env, and protect the file with chmod 600 .env. Keep .env, data/, backups/, and rcon-db/ out of Git. The env file avoids embedding credentials in the Compose document; users with Docker/host administrative access can still inspect container settings.
docker-compose.yaml
Save this as /srv/mc.example.com/docker-compose.yaml:
name: mc-example-com
services:
mc:
image: itzg/minecraft-server:java17
restart: unless-stopped
stop_grace_period: 2m
tty: true
stdin_open: true
environment:
# Set TRUE only after you have accepted the Minecraft EULA.
EULA: "TRUE"
TYPE: "FORGE"
VERSION: "1.20.1"
# For a modpack, replace recommended with its exact Forge build.
FORGE_VERSION: "${MC_FORGE_VERSION:-recommended}"
MEMORY: "10G"
ONLINE_MODE: "TRUE"
ENABLE_WHITELIST: "TRUE"
ENFORCE_WHITELIST: "TRUE"
WHITELIST: "${MC_PLAYERS:?Set MC_PLAYERS in .env}"
OPS: "${MC_OPS:?Set MC_OPS in .env}"
EXISTING_WHITELIST_FILE: "SYNCHRONIZE"
EXISTING_OPS_FILE: "SYNCHRONIZE"
MAX_PLAYERS: "10"
VIEW_DISTANCE: "8"
SIMULATION_DISTANCE: "6"
SEED: "65434353559200"
ENABLE_RCON: "TRUE"
RCON_PASSWORD: "${MC_RCON_PASSWORD:?Set MC_RCON_PASSWORD in .env}"
REMOVE_OLD_MODS: "TRUE"
ports:
- "65535:25565/tcp"
# No host mapping for RCON port 25575.
volumes:
- ./data:/data
- ./mods-forge-lite:/mods:ro
mem_limit: 12G
labels:
- com.centurylinklabs.watchtower.enable=false
- traefik.enable=false
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
networks:
default:
name: mc-example-comJava 17 is deliberate for this Forge generation. An untagged image follows latest, whose Java version can change. Once the pack works, set MC_FORGE_VERSION to the exact build it uses and pin the container image to a tested release tag or digest. itzg Java tags and Forge version selection document those controls.
Keep ONLINE_MODE enabled so a player cannot simply claim an operator's username. The allowlist controls admission; OPS controls administration. The synchronization settings make .env authoritative on startup, so also update it when changing membership through commands. RCON remains available inside Docker for administration and graceful shutdown. Server-property reference.
The seed is for world creation; changing it is not a way to regenerate an existing world. Keep this world's version and modpack together when upgrading.
Start and connect
# Validates interpolation and structure without printing resolved passwords.
docker compose config --quiet
docker compose up -d mc
docker compose logs -f --tail=100 mcThe first start downloads and installs the server. Wait for the ready message before connecting. In the Java client, install the matching Forge version and required client mods, then add mc.example.com:65535 under Multiplayer.
Point mc.example.com at the host's reachable address. For remote players behind a home router, forward TCP 65535 to this host and allow it in the relevant firewall. This game connection does not use Traefik's HTTP routers or HTTPS certificates. Do not forward 25575, 4326, or 4327.
For LAN/VPN-only access, replace the port mapping with your host's actual interface address, for example "192.168.1.50:65535:25565/tcp", and do not add an internet port-forward. A nonstandard port alone does not restrict access.
Mods and performance
Put Forge 1.20.1 server-compatible .jar files and their dependencies into mods-forge-lite/. The image copies /mods into its writable data directory. With REMOVE_OLD_MODS, that source folder is authoritative: manually added jars in data/mods can disappear on startup. Keep your complete desired set in the mounted folder. Do not put client-only rendering/UI mods on the server or mix Fabric, NeoForge, and Forge builds. itzg mod handling.
Two optional additions to investigate before adding a larger gameplay pack:
| Mod | Purpose | Selection |
|---|---|---|
| spark | Diagnose tick time and lag before guessing at fixes | Choose a release for Forge 1.20.1 |
| FerriteCore | Reduce memory use; its author recommends installing on both sides | Choose a Forge 1.20.1 file, not the newest NeoForge download |
Add one change at a time, back up first, and check its dependencies and client requirements. If using a curated modpack, follow its server-pack instructions rather than mixing in arbitrary versions. A normal Forge server does not load Bukkit/Paper plugins.
# After changing only jars in the mounted source folder:
docker compose restart mc
# After changing .env or Compose settings, recreate with the new configuration:
docker compose up -d mcStart with the conservative view and simulation distances above. If the server lags, inspect it with spark and docker stats; raising the heap cannot fix every CPU or world-generation bottleneck. The generic SIMD flag from the original configuration is omitted because this setup has no identified workload that needs it.
Optional web RCON behind Traefik
RCON Web Admin's Docker image provides a browser console. The name is RCON, not “rcron.” Its web UI listens on 4326, and browser WebSocket traffic uses 4327; neither needs publishing on the host. Its database is persisted below.
This uses the https entrypoint and lets-encrypt resolver from the vault's Traefik guide. That guide runs Traefik on the same Linux host with host networking, allowing it to reach these container addresses. If your Traefik uses bridge networking instead, attach it to the external mc-example-com network after the Minecraft stack creates it. A Traefik instance on another machine needs a different routing arrangement.
Add these values to .env, using a different generated password for the web login:
RCON_WEB_HOST=mc-rcon.example.com
RCON_WEB_USERNAME=admin
RCON_WEB_PASSWORD=TODO_REPLACE_WITH_A_DIFFERENT_RANDOM_SECRETCreate DNS for that hostname pointing to Traefik. Save this separate file as docker-compose.rcon.yaml beside the base file:
services:
mc-rcon:
image: itzg/rcon:latest
restart: unless-stopped
depends_on:
- mc
environment:
RWA_ENV: "TRUE"
RWA_USERNAME: "${RCON_WEB_USERNAME:?Set RCON_WEB_USERNAME in .env}"
RWA_PASSWORD: "${RCON_WEB_PASSWORD:?Set RCON_WEB_PASSWORD in .env}"
RWA_ADMIN: "TRUE"
RWA_GAME: "minecraft"
RWA_WEB_RCON: "FALSE"
RWA_RCON_HOST: "mc"
RWA_RCON_PORT: "25575"
RWA_RCON_PASSWORD: "${MC_RCON_PASSWORD:?Set MC_RCON_PASSWORD in .env}"
RWA_WEBSOCKET_URL_SSL: "wss://${RCON_WEB_HOST:?Set RCON_WEB_HOST in .env}/ws"
volumes:
- ./rcon-db:/opt/rcon-web-admin/db
labels:
- com.centurylinklabs.watchtower.enable=false
- traefik.enable=true
- traefik.docker.network=mc-example-com
- traefik.http.routers.mc-rcon.rule=Host(`${RCON_WEB_HOST}`)
- traefik.http.routers.mc-rcon.entrypoints=https
- traefik.http.routers.mc-rcon.priority=10
- traefik.http.routers.mc-rcon.tls=true
- traefik.http.routers.mc-rcon.tls.certresolver=lets-encrypt
- traefik.http.routers.mc-rcon.service=mc-rcon
- traefik.http.services.mc-rcon.loadbalancer.server.port=4326
- traefik.http.routers.mc-rcon-ws.rule=Host(`${RCON_WEB_HOST}`) && PathPrefix(`/ws`)
- traefik.http.routers.mc-rcon-ws.entrypoints=https
- traefik.http.routers.mc-rcon-ws.priority=100
- traefik.http.routers.mc-rcon-ws.tls=true
- traefik.http.routers.mc-rcon-ws.tls.certresolver=lets-encrypt
- traefik.http.routers.mc-rcon-ws.service=mc-rcon-ws
- traefik.http.services.mc-rcon-ws.loadbalancer.server.port=4327RWA_WEB_RCON stays false because Minecraft uses ordinary RCON. The browser still uses a WebSocket to talk to the admin app; those are separate connections. RWA_* credentials and server settings initialize the persisted database, so verify existing users/server entries in the UI when rotating credentials. RCON Web Admin configuration.
docker compose -f docker-compose.yaml -f docker-compose.rcon.yaml config --quiet
docker compose -f docker-compose.yaml -f docker-compose.rcon.yaml up -dOpen https://mc-rcon.example.com, sign in, and try list in the console. The /ws route has higher priority so its traffic reaches the WebSocket port. For an additional Traefik access policy, apply the same policy to both routers; protecting only the HTML page leaves a separate route unchecked. Treat this console as privileged administration and prefer access through your VPN when practical.
The web image tag is floating in this example. Pin a tested digest for repeatable deployments. To disable the optional console without touching the world:
docker compose -f docker-compose.yaml -f docker-compose.rcon.yaml stop mc-rconAdministration without the web UI
docker compose exec mc rcon-cli list
docker compose exec mc rcon-cli say 'Server maintenance in five minutes.'
docker compose exec mc rcon-cli save-all flush
docker compose logs --tail=100 mcThis needs no public RCON port. The image includes the Docker command interface.
Backups, restore, and upgrades
For a small private server, a short scheduled downtime is a simple way to take a consistent backup. Run this from /srv/mc.example.com when no one is playing:
(
set -e
mkdir -p backups
docker compose stop mc
trap 'docker compose up -d mc' EXIT
sudo tar -czf "backups/minecraft-$(date +%F-%H%M%S).tgz" data mods-forge-lite
)The subshell restarts the server on exit, including if archiving fails; check the command output and archive before treating the backup as successful. Copy backups off the host and occasionally test a restore. Save Compose files, the chosen image/Forge versions, and an encrypted copy of .env separately. The optional rcon-db also contains sensitive configuration; stop mc-rcon before taking a separate backup of it.
To restore, stop mc, move the existing data and mods-forge-lite directories aside, extract a trusted archive into the project directory, and start with the versions that produced it. Avoid extracting over a newer world's files or downgrading a world in place.
Unlike stateless services, a modded Minecraft upgrade changes both software and saved state. Automatic Watchtower updates are disabled here. Back up, test the new image/mod set on a separate copy with a different port, then apply the tested versions during a maintenance window. Review your updater's configuration to ensure it respects these exclusion labels.
Troubleshooting
| Symptom | First check |
|---|---|
| Players cannot connect | Server ready in logs, matching client pack, TCP 65535, DNS/NAT/firewall, and allowlist profile names |
| Starts without expected mods | Correct Forge/version jars in mods-forge-lite, required dependencies, startup logs |
| Container exits under load | docker stats, out-of-memory events, heap versus container limit, and mod errors |
| Web page works but console does not | /ws router priority, WebSocket port 4327, and browser network errors |
| Console cannot reach Minecraft | mc service on the same network, server ready, matching RCON password, and persisted server entry |
| Changed env settings do nothing | Use docker compose up -d; restart alone does not recreate container configuration |
Related
Linked from
- 🌿 Self-hosting Xonotic with Docker for LAN games
I remember playing Xonotic over LAN at college. Hosting it again is a way to bring those…
- 🌿 Tools
Practical guides for the software I reach for when something breaks the battletested, often…