Recognizing the problem
This guide is for the classic Docker bind-mount failure:
- the container starts, but cannot write under
/data - plugins, configs, or world files fail to update
- logs mention
Permission denied
With itzg/minecraft-server, this usually comes down to one mismatch:
the host filesystem ownership does not match the user that runs the Minecraft process inside the container.
Why this happens
By default, the image switches to UID 1000 and GID 1000. If your bind-mounted host directory belongs to a different user or group, writes can fail.
That is common when:
- the directory was created by
root - the server was migrated from another host
- the host user has a different UID than 1000
Quick fix
If you intentionally want to keep the default container user, fix the ownership on the host:
sudo chown -R 1000:1000 ./data
Then recreate or restart the stack:
docker compose up -d
Option 1: Keep the default container user and fix the host
This is the simplest setup and the one I recommend for most single-server deployments.
Compose:
services:
mc:
image: itzg/minecraft-server:latest
volumes:
- ./data:/data
Host fix:
sudo chown -R 1000:1000 ./data
find ./data -maxdepth 2 -ls | head
This keeps the image on its documented default.
Option 2: Match the container user to your host user
If your host directory should stay owned by a different user, set UID and GID explicitly:
services:
mc:
image: itzg/minecraft-server:latest
environment:
UID: "1001"
GID: "1001"
volumes:
- ./data:/data
This is useful when you already standardized file ownership on the host and want the container to follow it.
Option 3: Use Compose user only when you mean it
Docker also supports a Compose-level user: setting:
services:
mc:
user: "1001:1001"
That works, but it changes the container-level user behavior more broadly.
If your goal is simply to align with the image's supported ownership model, UID and GID are usually clearer.
How to validate the fix
After restarting the stack:
docker compose logs -f
You want to see normal startup without repeated permission failures. Then test a write path, for example:
- change
server.properties - add a plugin
- trigger
save-allthrough RCON
If the server can write under /data, the fix is complete.
Common anti-patterns
chmod -R 777
This is the usual panic move and almost never the right long-term answer. It weakens the host filesystem permissions and makes future ownership debugging harder.
Mixing ownership models casually
Do not switch randomly between:
- default
1000:1000 - custom
UID/GID - Compose
user
Pick one model and keep it consistent.
Forgetting SELinux or rootless specifics
If you use Podman or an SELinux/AppArmor-heavy setup, the bind mount may need extra labeling such as :Z.
That is a different class of failure than plain UID/GID mismatch.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
Permission denied on /data | Host ownership mismatch | Align ownership with container UID/GID |
Files were created as root | Earlier commands used sudo or root container | Correct ownership recursively |
| Config still will not change | Container recreated with old mount or old env | Re-run docker compose up -d and inspect the effective config |
| Admin commands work, file writes fail | RCON path is fine, filesystem is not | Focus on bind mount ownership, not networking |
FAQ
Which option should I choose?
For most setups: keep the default 1000:1000 and make the host directory match it.
Do named volumes avoid some of these issues?
Yes. Bind mounts expose host ownership problems more directly. Named volumes can be simpler when you do not need direct host-side editing.
Next steps
- Once writes work reliably, set up automatic backups with docker-mc-backup.
- If you mainly need admin commands, use RCON and console access.
- If you are still on the first deployment, revisit the Hetzner Docker setup guide.