What a restart loop actually means
With restart: unless-stopped, Docker starts the same container again after its main process terminates. The restart policy is exposing a failure; it is not the cause.
An unhealthy status is different. The itzg image checks the Minecraft server with mc-health, but a normal Docker restart policy does not restart a container merely because its healthcheck fails. If that happens, look for an auto-heal container, Portainer rule, or orchestrator.
Stop the loop without deleting evidence
Do not begin with docker compose down, volume deletion, or a fresh /data. Disable the runtime restart policy on the existing container and let its current failing start end naturally:
docker compose ps -a
mc_container_id="$(docker compose ps -a -q mc)"
docker update --restart=no "$mc_container_id"
docker wait "$mc_container_id"
docker wait prints the exit code. This avoids sending a new stop signal that could replace the failure state you are trying to inspect. The production policy remains in Compose and returns when the service is recreated.
Capture exit state and the first error
Inspect the preserved last state:
docker inspect "$mc_container_id" --format 'exit={{.State.ExitCode}} oom={{.State.OOMKilled}} error={{json .State.Error}} restarts={{.RestartCount}}'
docker compose logs --tail=250 --timestamps mc
Read from the top of the failing startup. The final lines often contain only shutdown noise caused by the earlier error.
Useful interpretations:
| Result | Meaning | Next check |
|---|---|---|
| Exit 1 | Initialization or application error | First ERROR, Exception, or [init] failure |
| Exit 137 | Process received SIGKILL | Check OOMKilled; use the Exit 137 runbook |
| Exit 143 | Process received SIGTERM | Look for an operator, deployment, or stop timeout |
| Running but unhealthy | Process still exists, health probe fails | Inspect health output and server readiness |
Inspect the healthcheck history when the container did not exit:
docker inspect "$mc_container_id" --format '{{json .State.Health}}'
docker compose logs --tail=250 mc
Validate the effective Compose configuration
Many loops come from editing a file that Compose is not actually using:
docker compose config
docker compose images
docker image inspect "$(docker compose images -q mc)" --format '{{json .Config.Labels}}'
Confirm the effective image, TYPE, VERSION, Java tag, volumes, and memory settings. docker compose restart does not apply changed environment variables; recreation does.
Match the first error to the cause
| First useful error | Likely cause | Fix |
|---|---|---|
| EULA is not accepted | EULA missing or not TRUE | Correct Compose and recreate the service |
| Unsupported class-file or Java error | Wrong Java image for Minecraft/modpack | Select the required Java tag |
Permission denied under /data | Host ownership differs from container UID/GID | Follow the permissions runbook |
| No matching server/modpack version | Invalid TYPE, version, project, or file ID | Pin a compatible release from the primary source |
| Plugin/mod loading exception | Incompatible or duplicated artifact | Restore the last working set while stopped |
| Cannot reserve memory | Heap exceeds available container/host memory | Reduce heap or increase verified capacity |
| Port already allocated | Another container/process owns the host port | Identify the owner; do not change ports blindly |
For noisy image initialization, temporarily add:
environment:
DEBUG: "TRUE"
DEBUG_EXEC: "TRUE"
Use DEBUG_MEMORY: "TRUE" only for JVM allocation failures. Remove debug flags after diagnosis because they make logs much larger and can expose more configuration detail.
Apply and verify the correction
After editing Compose, recreate rather than restart:
docker compose config
docker compose up -d --force-recreate mc
docker compose logs -f mc
Wait for the normal ready message, then verify stability instead of declaring success after one minute:
docker compose exec mc rcon-cli version
docker compose exec mc rcon-cli save-all flush
docker inspect "$(docker compose ps -q mc)" --format 'restarts={{.RestartCount}} health={{.State.Health.Status}}'
Repeat the last command after several minutes. The restart count must stay unchanged.
Do not hide the failure
Avoid these non-fixes:
- removing the restart policy and assuming the server is repaired
- deleting
/databefore preserving the failing state - increasing every timeout without reading the first error
- disabling the image healthcheck to conceal a server that never becomes ready
- repeatedly pulling
latestand changing the failure surface
Next steps
- If the exit code is 137, continue with OOMKilled and memory diagnosis.
- For modpack initialization failures, use the CurseForge Docker runbook.
- Once stable, apply the Minecraft Docker security baseline.