setupmc.com

Secure a Minecraft Server with Docker Compose: Practical Hardening

Harden an itzg/minecraft-server deployment with online mode, a whitelist, file-based RCON secrets, a non-root container, reduced capabilities, limited ports, and trusted plugins.

Security
setupmc.com Team

Need a cleaner Java baseline?

Generate a Java Compose setup before you keep patching by hand

If you are still refining the Java server baseline, use the Java configurator and then return to the guides for the next issue.

Open Java configurator

Define what needs protection

A public Minecraft port exposes the server application to the internet. Plugins and mods execute code inside that application. Docker and SSH administer the host beneath it. Treat these as separate boundaries:

  1. player authentication and allow-listing
  2. Minecraft/RCON network exposure
  3. plugin and mod supply chain
  4. container privileges and host mounts
  5. host administration, patching, and recovery

No single Compose flag covers all five.

A hardened Compose baseline

This example assumes ./data is owned by UID/GID 1000:1000:

services:
  mc:
    image: itzg/minecraft-server:java25
    restart: unless-stopped
    user: "1000:1000"
    read_only: true
    tmpfs:
      - /tmp:size=256m
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true
    ports:
      - "25565:25565"
    environment:
      EULA: "TRUE"
      TYPE: PAPER
      VERSION: "26.1.2"
      ONLINE_MODE: "TRUE"
      ENABLE_WHITELIST: "TRUE"
      RCON_PASSWORD_FILE: /run/secrets/rcon_password
    volumes:
      - ./data:/data
    secrets:
      - rcon_password

secrets:
  rcon_password:
    file: ./rcon_password.secret

Create a long random RCON password in rcon_password.secret, restrict that host file, and exclude it from Git. Compose mounts it read-only at /run/secrets/rcon_password.

The writable server state stays under /data; the image root filesystem is read-only and /tmp is temporary. The explicit non-root user lets the container drop all Linux capabilities without relying on root during startup.

Validate the security controls

Before starting, inspect the fully resolved Compose file. Docker treats Compose as trusted host instructions, including nested includes and bind mounts:

docker compose config
docker compose up -d mc
docker compose exec mc id
docker port "$(docker compose ps -q mc)"
docker inspect "$(docker compose ps -q mc)" --format 'readonly={{.HostConfig.ReadonlyRootfs}} user={{json .Config.User}} caps={{json .HostConfig.CapDrop}} security={{json .HostConfig.SecurityOpt}}'

Expected results:

  • the process reports UID/GID 1000
  • only Minecraft port 25565 is published
  • RCON 25575 is not mapped to the host
  • root filesystem is read-only
  • all capabilities are dropped and no-new-privileges is active

Then verify the application still works:

docker compose exec mc rcon-cli version
docker compose exec mc rcon-cli whitelist add your-minecraft-name
docker compose exec mc rcon-cli whitelist list
docker compose exec mc rcon-cli save-all flush

Replace your-minecraft-name with the real username before running the second command.

Keep authentication enabled

Use ONLINE_MODE: "TRUE" for a directly exposed Java server so Mojang/Microsoft account authentication remains active. A whitelist reduces who can join but does not replace online-mode authentication.

Offline mode belongs only behind a correctly secured proxy design that performs authentication and prevents direct backend access. It is not a shortcut for public servers.

Publish only required ports

For a normal Java server, publish TCP 25565. Do not publish these merely because they exist inside the container:

  • RCON 25575
  • JMX 7091
  • arbitrary query or plugin ports
  • the Docker socket

Run administrative commands with docker compose exec mc rcon-cli .... Apply both the host firewall and, on Hetzner, the Cloud Firewall as described in the port 25565 guide.

Treat plugins, mods, and Compose files as code

Paper warns that plugins receive unrestricted access to the server and machine from the process perspective. Install only signed or checksummed releases from trusted project pages, pin versions, and keep an inventory.

Review third-party Compose files before running them. Be especially suspicious of:

  • privileged: true
  • network_mode: host or pid: host
  • cap_add, especially SYS_ADMIN
  • host devices and broad bind mounts
  • /var/run/docker.sock
  • remote include or extends content you did not inspect

Use the Paper plugin runbook for a controlled lifecycle.

Secure the host and recovery path

Container hardening cannot repair an exposed SSH account or an unpatched Docker daemon:

  • use SSH keys and a non-root administrative account
  • install OS and Docker security updates
  • restrict who belongs to the docker group; Docker access is effectively host-root access
  • keep the host firewall narrow
  • send backups off-host and test restores
  • keep secrets out of Git, shell history, and screenshots

Backups writable from the same compromised host can be deleted too. Maintain a second failure domain with automatic backups and off-host retention.

Update without losing control

Pull image security fixes regularly, but do not combine an unattended Minecraft version change with the image update. Pin VERSION, back up, pull the Java image, and follow the safe update and rollback runbook.

If you adopt Docker rootless mode, follow Docker's official prerequisites and re-test port publishing, bind-mount ownership, cgroups, and service startup. Rootless reduces daemon/runtime privilege but is a host-level migration, not a single Compose option.

Troubleshooting

SymptomLikely causeFix
/data permission deniedHost directory not owned by UID/GID 1000Correct ownership; do not add capabilities
Startup cannot write /tmpMissing tmpfs with read-only rootAdd the /tmp tmpfs shown above
RCON authentication failsSecret missing, unreadable, or changedGrant the secret to mc and recreate the service
Players cannot joinWhitelist is empty or firewall is closedAdd real usernames and verify only port 25565
Plugin asks for broad host accessUnsafe design or untrusted instructionsDo not add mounts/privileges until independently justified

Next steps

Frequently asked questions

Short answers to the questions that usually come up while working through this topic.