Files
craftdock/README.md
T
Lucas RayanandClaude Sonnet 5.5 a4a2584f37 #14: Gitea CI, registry composes, Node 22 image and changelog
- Gitea Actions: Trivy, Vitest in a Dockerfile test stage, arm64 image build, image
  scan and smoke test; releases reuse the image by content hash.
- docker-compose.<edition>.registry.yaml run the stack from the published image.
- Dockerfile: Node 22, a test stage, and no npm in the runtime image.
- README, CLAUDE.md and CHANGELOG describe the CI, the image and the release flow.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-01 22:54:00 -03:00

287 lines
17 KiB
Markdown

# CraftDock
Lightweight, self-hosted web panel to manage a **single** Minecraft server (Java *or* Bedrock) running as a Docker container — an Aternos-style dashboard you host yourself. Built for a Docker/Coolify deployment.
The panel never modifies the Minecraft core. It controls the game container over the Docker socket (start/stop/restart/kill, stats, logs) and sends commands through **RCON** (Java) or **container stdin** (Bedrock).
---
## Features
| Tab | What it does |
|-----|--------------|
| **Início / Status** | Start / Stop / Restart / Kill, live CPU / RAM / player count, edition badge |
| **Console** | Real-time log stream over WebSocket + command input (RCON / stdin) |
| **Jogadores** | Persistent world player directory (paginated, searchable), per-player **inspector modal** (position, health, food, inventory, XP, gamemode — read straight from the world save), whitelist / ban / op management, kick & teleport — controls adapt to what the edition supports |
| **Opções** | Friendly editor for `server.properties` (difficulty, PvP, max-players, gamemode, …) — keys missing from the file are pre-filled with their vanilla defaults |
| **Mundo** | Download the world as `.zip`, upload/replace a world, regenerate a new world |
| **Mapa** | Shows the world seed and embeds a seed map (mcseedmap.net) rendered for that seed |
---
## Editions
CraftDock supports both Minecraft editions through an adapter layer, but they are **not** equivalent:
- **Java** — commands go over **RCON**. All player controls work: whitelist, ban/pardon, op/deop, give, gamemode, kick, teleport.
- **Bedrock** — commands go over the container's **stdin** (`docker attach`). `whitelist` becomes `allowlist`, and **ban / pardon have no native command** — those buttons are hidden in the UI.
The edition is detected from the container's `TYPE` env var when `MC_EDITION=auto`, or forced with `MC_EDITION=java|bedrock`.
---
## Player inspector (world-save reads)
The Players tab keeps a persistent directory of everyone who ever spawned in the world and can open a per-player detail modal with position, health, food, inventory, XP and gamemode. All of it is **read directly from the world save, read-only** — never via console commands:
- **Java** — `world/playerdata/<uuid>.dat` (names seeded from `usercache.json`).
- **Bedrock** — the world **LevelDB**. Bedrock never stores gamertags in the DB, so CraftDock builds the identity chain itself: XUID+name from `Player Spawned` log lines (`craftdock_players.json`), then — the first time you open an **online** player — `querytarget` yields the player's `uniqueId` UUID, which keys the DB's `player_<uuid>` mapping record pointing to the real `player_server_<uuid>` data record. The binding is persisted (`craftdock_bedrock_ids.json`), so offline reads work forever after.
While the server is running, reads happen inside a save snapshot (`save hold`/`save query`/`save resume` on Bedrock, `save-all flush` on Java) so files are consistent.
---
## Live map & seed
The Map tab shows the world seed and embeds [mcseedmap.net](https://mcseedmap.net) pointed at it (a real biome/structure map that allows iframe embedding). The seed is resolved automatically:
1. `level-seed` from `server.properties` (when the operator set a fixed seed), else
2. **Bedrock** — read from `worlds/<level-name>/level.dat` (the `RandomSeed` NBT long). Bedrock has **no** `seed` console command, so this is the only reliable source.
3. **Java** — the `seed` command over RCON.
The mcseedmap version segment defaults per edition; override it with `MAP_VERSION` (e.g. `1.21-Java`, `26.30.0-Bedrock`) if the biomes don't match your server version.
---
## Logging
The panel emits structured JSON logs (one record per line: `ts`, `level`, `component`, `msg`, optional `meta`) to stdout. Level is controlled by `LOG_LEVEL` (`error` < `warn` < `info` < `debug`; default `info`). Command channels (RCON / stdin), seed resolution, HTTP requests, and unhandled errors are all logged — run with `LOG_LEVEL=debug` to see every command sent to the server and its response.
```bash
docker compose -f docker-compose.bedrock.yaml logs -f # follow panel + server logs
```
---
## Requirements
- **Node.js ≥ 18** (dev; the image uses Node 22) or Docker (deploy).
- A Docker host — the panel mounts `/var/run/docker.sock` to control the game container.
---
## Quick start (local development)
```bash
git clone <repo> && cd CraftDock
npm install
# 1. Generate the admin password hash (prints a raw and a base64 line):
npm run hash -- <your-password>
# 2. Create .env from the template and fill it in:
cp .env.example .env
# - ADMIN_PASSWORD_HASH = the raw hash line from step 1 (best for `npm run dev`)
# - SESSION_SECRET = any long random string
# - RCON_PASSWORD = your server's RCON password (leave empty for Bedrock)
# 3. Run the tests and start the dev server:
npm test
npm run dev # loads .env automatically, hot-reloads on change
```
Open <http://localhost:3000> and log in with your password.
> `npm run dev` uses `node --env-file=.env`, so your `.env` is loaded automatically.
> `npm start` (production) does **not** load `.env` — it reads real environment
> variables provided by the container/host.
---
## Running with Docker
The panel image is built from the included `Dockerfile`. Two ways to run each stack:
- **Build it yourself** (`docker-compose.<edition>.yaml`, `--build`): works anywhere, no registry needed.
- **Pull the published image** (`docker-compose.<edition>.registry.yaml`): `git.lucasrguerra.dev.br/lucasrguerra/craftdock` (linux/arm64), built, scanned and smoke-tested by CI. Tags: `latest`, `X.Y.Z`, `X.Y` and `sha-<commit>`; pin one with `IMAGE_TAG`. This is what the reference Coolify deployment runs.
To build it:
Two reference stacks are provided:
### Java (`docker-compose.java.yaml`)
```bash
# secrets are read from .env by docker compose (${ADMIN_PASSWORD_HASH}, etc.)
docker compose -f docker-compose.java.yaml up -d --build
docker compose -f docker-compose.java.yaml logs -f
```
### Bedrock (`docker-compose.bedrock.yaml`)
```bash
docker compose -f docker-compose.bedrock.yaml up -d --build
docker compose -f docker-compose.bedrock.yaml logs -f
```
Both stacks share the **same** panel env block (only the edition, world folder and
Minecraft image differ) and read their values from `.env` — see [`.env.example`](.env.example)
for the canonical list.
> **The panel only `expose`s port 3000** (reachable over the Docker network) —
> it is **not** published on the host by default, because the composes target a
> reverse proxy (Coolify/Traefik terminates TLS on 443 and forwards to 3000).
> In Coolify set the service domain **without a port** (`https://your.domain`,
> not `:3000`) and make sure the service's **target port is 3000** — a mismatch
> (e.g. Traefik still forwarding to an old port) makes plain 443 return
> *"no available server"*.
> For **local** testing without a proxy, comment out `expose` and uncomment the
> `ports:` line in the panel service, set `NODE_ENV=development` in `.env` (so
> the secure cookie isn't dropped over plain HTTP), then open
> <http://localhost:3000>.
> **Rebuild after changing panel code:** `--build` is required the first time and
> whenever you change the panel source. A runtime-only change (env var) just needs
> `up -d --force-recreate`.
> **First Bedrock boot is slow** — `itzg/minecraft-bedrock-server` downloads the
> latest version. Watch the logs until you see the server report it has started.
---
## Deployment prerequisites (Coolify / production)
The panel controls an existing, **fixed** container — it never recreates it or changes `TYPE`/`VERSION`. For everything to work, the Minecraft service must be configured as follows:
1. **`stdin_open: true` and `tty: true`** on the Minecraft service. Without a TTY, Docker returns a *multiplexed* log stream (8-byte frame headers) that garbles the Console tab, and stdin (the Bedrock command channel) never reaches the server process.
2. **`OVERRIDE_SERVER_PROPERTIES=false`**, otherwise the `itzg` image regenerates `server.properties` from env vars on every start and the Options tab won't persist.
3. **Java only:** `ENABLE_RCON=true` + a shared `RCON_PASSWORD`.
4. Panel and Minecraft server on the **same Docker network**.
5. The `mc-data` volume **shared** between the panel and the Minecraft server (the panel reads/writes world files directly).
6. Set `ADMIN_PASSWORD_HASH_B64` (the base64 line from `npm run hash`) and a strong `SESSION_SECRET`. Prefer the base64 form here — a raw bcrypt hash's `$` chars get mangled by docker compose / Coolify interpolation.
7. Run the panel with **`NODE_ENV=production`** so the session cookie is marked `secure` — this requires serving the panel over **HTTPS** (Coolify/Traefik terminate TLS on 443). The app already calls `trust proxy`, so the secure cookie works correctly behind the reverse proxy.
8. *(Optional)* Set `MAP_VERSION` if the Map tab's mcseedmap version doesn't match your server.
---
## Environment variables
| Variable | Required | Default | Description |
|----------|:--------:|---------|-------------|
| `ADMIN_PASSWORD_HASH` | ✅¹ | — | Raw bcrypt hash (`npm run hash -- <pw>`). Best for `npm run dev` |
| `ADMIN_PASSWORD_HASH_B64` | ✅¹ | — | Base64 of the bcrypt hash. Best for docker compose / Coolify (no `$` escaping) |
| `SESSION_SECRET` | ✅ | — | Long random string signing the session cookie |
| `MC_CONTAINER_NAME` | | `craftdock-mc-server` | Minecraft container name (matched first) |
| `MC_SERVICE_NAME` | | `minecraft-server` | Compose service name — fallback match when the container name is auto-generated (e.g. Coolify) |
| `MC_DATA_PATH` | | `/minecraft/data` | Path (inside the panel) to the shared MC data volume |
| `MC_EDITION` | | `auto` | `auto` \| `java` \| `bedrock` |
| `MC_WORLD_NAME` | | `world` | World folder under `MC_DATA_PATH` (Bedrock: `worlds`) |
| `RCON_HOST` | | `craftdock-mc-server` | RCON host (Java) |
| `RCON_PORT` | | `25575` | RCON port (Java) |
| `RCON_PASSWORD` | | *(empty)* | RCON password — **required for Java**, leave empty for Bedrock |
| `MAP_VERSION` | | *(per edition)* | mcseedmap version segment override (e.g. `1.21-Java`, `26.30.0-Bedrock`) |
| `PORT` | | `3000` | Port the panel listens on (also the exposed/target port in the composes) |
| `NODE_ENV` | | `development` | `production` marks the session cookie `secure` (needs HTTPS) |
| `LOG_LEVEL` | | `info` | `error` \| `warn` \| `info` \| `debug` |
| `MAX_UPLOAD_MB` | | `1024` | Max world upload size (MB) |
¹ Provide **one** of `ADMIN_PASSWORD_HASH` or `ADMIN_PASSWORD_HASH_B64`. `npm run hash -- <pw>` prints both; the raw hash wins if both are set.
---
## Security notes
- The panel mounts the Docker socket, which is **equivalent to root on the host**. Never expose it unauthenticated. Access is gated by a single admin password (bcrypt) with a signed session, enforced on every `/api/*` route **and** the Socket.io handshake.
- Always serve the panel over **HTTPS** in production (secure session cookie).
- Keep `SESSION_SECRET` secret and unique per deployment.
---
## Troubleshooting
**`pull access denied for craftdock-panel` on `docker compose up`**
The panel image is built locally, not pulled. Add `--build`: `docker compose up -d --build`.
**`failed to bind host port 0.0.0.0:3000: address already in use`**
Something else is on port 3000 — usually a leftover `npm run dev`. Find and stop it:
```powershell
Get-NetTCPConnection -LocalPort 3000 -State Listen # find the PID
Stop-Process -Id <PID> -Force # stop it
```
Then bring the stack up again.
**`ERR_CONNECTION_REFUSED` even though the panel logs "listening on :3000"**
The container is listening internally but the host port wasn't published — typically a container that was *created* during a failed port-bind and then only restarted. Recreate it:
```bash
docker compose -f <compose-file> down
docker compose -f <compose-file> up -d --force-recreate
```
Confirm the host is listening: `Get-NetTCPConnection -LocalPort 3000 -State Listen`.
**Login succeeds (no error) but the page stays on the login screen**
The session cookie isn't being stored. It's marked `secure` (from `NODE_ENV=production`) while you're on plain `http://localhost` — browsers drop secure cookies over HTTP. For local HTTP testing set `NODE_ENV=development`; in production serve over HTTPS. (Behind a reverse proxy, the app already trusts `X-Forwarded-Proto` via `trust proxy`.)
**Console tab shows garbled/binary characters**
The Minecraft container is missing `stdin_open: true` + `tty: true`. Add both and recreate it.
**Options tab changes don't persist after restart**
The Minecraft service is missing `OVERRIDE_SERVER_PROPERTIES=false`.
**Coolify: `Bind for 0.0.0.0:3000 failed: port is already allocated`**
Don't publish the panel's port on the host under Coolify — its Traefik reaches the container over the network. The composes already ship with `expose: ["3000"]` and the `ports:` line commented out (this is the default); keep it that way and set the service **domain** in Coolify. Keep `ports:` only on the Minecraft service (the raw game port).
**Coolify: `no available server` when opening `https://your.domain/`**
Traefik has a route for the domain but is forwarding to the wrong container port, so the backend looks empty. Two things to check: (1) the Coolify domain must be set **without a port** (`https://your.domain`, not `:3000`); (2) the service's **target port must be 3000** — the panel listens on 3000. A common trap is an old target port left over from a previous setup (e.g. Traefik still pointing at a port the app no longer listens on): align it to 3000 and redeploy.
**Coolify / docker compose: login fails, or warning `The "…" variable is not set. Defaulting to a blank string.`**
The `…` is the tail of your bcrypt hash — bcrypt hashes contain `$`, which docker compose interprets as variable interpolation, corrupting the hash. **Use `ADMIN_PASSWORD_HASH_B64`** (the base64 line from `npm run hash`) instead of the raw `ADMIN_PASSWORD_HASH` — base64 has no `$`, so nothing gets mangled. (Alternatively, keep the raw hash but escape every `$` as `$$`.)
**Status shows `not_found` on Coolify (panel can't see the server)**
Coolify ignores `container_name` and auto-generates names like `minecraft-server-<project>-<hash>`, so matching by `MC_CONTAINER_NAME` fails. The panel falls back to the compose **service** name — make sure the Minecraft service is named `minecraft-server` (or set `MC_SERVICE_NAME` to match) and that both services are in the **same** Coolify resource (project). The panel also needs the Docker socket mounted (`/var/run/docker.sock`).
**Map tab shows "waiting for seed" / no map**
The seed is resolved only while the server is **running**. For Bedrock it comes from `worlds/<level-name>/level.dat`, so the `mc-data` volume must be shared with the panel.
---
## Development
```bash
npm test # run the full Vitest suite
npm run test:watch
npm run dev # start with --env-file=.env and --watch
```
### Project layout
```
src/
├── server.js # Express + Socket.io composition
├── main.js # composition root (wires real dependencies)
├── config.js # env var loading + validation
├── logger.js # structured JSON logger
├── healthcheck.js # container healthcheck script
├── appState.js # cached edition/adapter resolution
├── middleware/ # auth (HTTP + socket handshake)
├── services/ # docker, rcon, stdin, properties, world, auth, seed,
│ # playerDirectory, playerDetailService, bedrockIdentityBridge
├── adapters/ # ServerAdapter: java (RCON) / bedrock (stdin)
│ └── playerFile/ # read-only world-save readers (Java NBT / Bedrock LevelDB)
├── routes/ # auth, status, players, properties, world
├── sockets/ # logs stream + status push namespaces
└── public/ # vanilla-JS tabbed SPA (Tailwind via CDN)
```
CraftDock also keeps two metadata files at the data root (bundled into world backups): `craftdock_players.json` (persistent XUID→name player directory) and `craftdock_bedrock_ids.json` (Bedrock identity bridge). See [CLAUDE.md](CLAUDE.md) for the full architecture and contribution conventions (numbered feature branches, `#N:` commit prefix).
---
## Continuous integration and releases
CI runs on the Gitea runners (`.gitea/workflows`):
- **`ci.yml`** (branches and every Monday): Trivy on the repository (vulnerabilities, secrets, misconfiguration; the accepted `DS-0002` is documented in [`.trivyignore`](.trivyignore)), the Vitest suite inside the Dockerfile `test` stage, the arm64 image build, Trivy on the image and a smoke test (`scripts/smoke-imagem.sh`: UI served, API refuses unauthenticated requests, item icons generated, LevelDB addon loads, image healthcheck turns healthy).
- **`release.yml`** (`main` and `vX.Y.Z` tags): if the registry already holds the image for the same content (`inputs-<hash>`), nothing is rebuilt — only the new tags are applied. On a tag it creates the Gitea release with the [CHANGELOG](CHANGELOG.md) notes.
- To release: update the CHANGELOG and `package.json`, commit, then `git tag vX.Y.Z && git push gitea vX.Y.Z`.