Docker Guide
Run Minecraft services as Docker containers instead of bare Java processes — opt-in per group, hard resource limits, clean cleanup.
The Docker module (shipped with Nimbus 0.10.0+) lets services run inside Docker containers instead of as bare java processes. It is opt-in per group — groups without [group.docker] enabled = true keep running exactly as before.
Why use it
Bare JVM processes are faster to start, lighter, and have no external dependencies. They're the right default for the majority of Nimbus deployments. Docker wins when you need any of:
- Hard memory/CPU caps. The kernel enforces limits, not just
-Xmx. A runaway plugin can't eat all the host's RAM - Clean cleanup. Container stop = everything gone. No leftover temp files, no zombie processes, no stale
session.lock - Isolation. A misbehaving service can't interact with other services' files or sockets
- Mixed Java versions. Java 17 and Java 21 services side-by-side without host PATH gymnastics — each container carries its own JRE
- Reproducibility. Same image = same behaviour on every node in the cluster
It is not for: faster startup (Docker adds ~200ms), lower memory usage (kernel overhead), or orchestration (that's Nimbus's job — don't layer Kubernetes on top)
Prerequisites
- Docker Engine 20.10+ or Docker Desktop (with WSL2 integration on Windows)
- The Nimbus user in the
dockergroup (sudo usermod -aG docker $USER, then re-login) so Nimbus can reach the socket withoutsudo - On Linux, the socket lives at
/var/run/docker.sockby default - On Windows without Docker Desktop's WSL2 integration, expose the daemon via TCP (Docker Desktop → Settings → General → "Expose daemon on tcp://localhost:2375") — the module supports
socket = "tcp://localhost:2375"
Podman is Docker-API-compatible and should work with the module pointed at the Podman socket, but is not officially tested. If you hit an issue, open a GitHub issue with your podman version.
Installing the module
- Run the setup wizard (or re-run it:
./nimbus --reinstall) and select Docker from the module list. The module is not installed by default - Nimbus writes
config/modules/docker/docker.tomlwith sensible defaults - Start the controller — the module probes the daemon and logs
Docker daemon reachable at /var/run/docker.sock
If the daemon is unreachable at startup, the module stays loaded but inactive. Services that opted into Docker transparently fall back to bare processes with a warning in the log.
Enabling Docker for a group
Edit the group's TOML — or open the group in the dashboard and flip the Docker switch under Edit:
[group]
name = "BedWars"
software = "PAPER"
# ...
[group.docker]
enabled = trueThat's the minimum. On the next scale-up of the group:
- Nimbus picks a Java image matching the server's required Java version (17 or 21 based on MC version)
- Pulls the image if not cached locally
- Creates a container named
nimbus-bedwars-1(or whatever the service slot is) - Binds the service work dir as
/serverinside the container - Maps the allocated host port directly to the same container port
- Passes the JVM command line from
ServiceFactory(same command that would run as a bare process, minus the hostjavabinary) - Attaches to stdin+stdout so
sendCommand("stop")and the live console still work - Applies resource limits from the config
Per-group overrides
[group.docker]
enabled = true
memory_limit = "4G" # hard cgroup memory cap
cpu_limit = 3.0 # CPU quota in cores (2.0 = 2 full cores)
java_image = "eclipse-temurin:21-jre" # override auto-selection
network = "nimbus-bedwars" # override default networkEmpty strings / 0.0 mean "inherit the module default" from docker.toml.
Dedicated services
Dedicated services get the same [dedicated.docker] block:
[dedicated]
name = "SMP"
port = 25567
# ...
[dedicated.docker]
enabled = true
memory_limit = "8G"
cpu_limit = 4.0Console & dashboard
Console
docker status # Daemon version + Nimbus container count
docker ps # List Nimbus-managed containers
docker inspect <name> # Container details with live stats
docker prune # Remove stopped nimbus.managed=true containersDashboard
/modules/docker renders:
- Status cards: module enabled, daemon reachable, daemon version, container counters
- Container table: every running Nimbus container with its service/group labels, state, image, ports, and short ID
- Prune button that calls
POST /api/docker/prune
The services list (/services) shows a Docker badge next to each containerised service. The group edit dialog has a Docker section: enabled switch + memory + CPU + image.
REST API
All endpoints require an admin token:
| Endpoint | What it does |
|---|---|
GET /api/docker/status | Daemon reachability + version + container counters |
GET /api/docker/containers | Every Nimbus-managed container |
GET /api/docker/containers/{name} | Single container with live cgroup stats |
POST /api/docker/prune | Remove every stopped nimbus.managed=true container |
How recovery works
When the controller restarts, it enumerates every container labelled nimbus.managed=true and re-attaches to the running ones — no restart of the Minecraft services. The attach stream picks up fresh stdout, the exit watcher resumes, and the handle gets wired into ServiceManager exactly as if it had just been created. Stopped containers from previous runs show up in docker ps -a until you docker prune.
How memory accounting works
When Docker is active for a service, ServiceMemoryResolver prefers the cgroup memory reading from docker stats over /proc/<pid>/status. cgroup memory catches the whole container (any sidecar helper processes, forked JVMs during plugin load, etc.), not just the main java PID. This is why dashboard memory numbers for Docker-backed services tend to match docker stats exactly.
How the container is built
The module creates containers with:
Tty: true+OpenStdin: trueso Nimbus can writestop\nto stdin and read stdout from a single hijacked attach connectionRestartPolicy: no— Nimbus controls restarts, not Docker- A bridge network named
nimbus(auto-created on first start) - Labels:
nimbus.managed=true,nimbus.service=<name>,nimbus.group=<group>,nimbus.port=<port> - Port mapping
host:N → container:N(same port, the server binds what's inserver.properties/velocity.toml) - The entire work dir bind-mounted at
/server, so templates, plugins, JARs, worlds, and configs come from the host filesystem — no custom image build required
Known limitations (Phase 1)
These are on the roadmap but not in this release:
- Agent-node Docker support. The cluster protocol needs a tweak so agents can report container runtime info in heartbeats
- Pre-built images. Every container uses
eclipse-temurin:21-jre+ bind-mount. Baking the server JAR into an image is faster to start but needs a build pipeline - Windows named pipes. TCP via
tcp://localhost:2375works today; direct\\.\pipe\docker_enginesupport is not wired up - Image pull progress. First start on a new host pulls ~500 MB silently; a progress indicator in the dashboard would help
Troubleshooting
"Daemon unreachable" in /api/docker/status
Run docker ps from the Nimbus user's shell. If it fails with "permission denied on /var/run/docker.sock", you're not in the docker group — sudo usermod -aG docker $USER and re-login. If the daemon isn't running, sudo service docker start.
Services silently stay on the process path
The module falls back to a bare process if the daemon isn't available at service-start time. Check /api/doctor — the Docker section surfaces the exact reason. You can also tail the controller log for Docker daemon not reachable.
Container pulled the wrong Java image
Set java_image explicitly in the group's [group.docker] block. Auto-selection looks at the detected system Java binary name for a (jdk|temurin|jre|openjdk)NN pattern and falls back to the module default otherwise.
Backup Guide
Scheduled tar+zstd snapshots of services, templates, controller config, the state-sync store, and the database — with GFS retention, integrity verification, and one-command restore.
Doctor
Built-in diagnostic command and dashboard page that flags environment, configuration, storage, database, service, and cluster issues with actionable hints.