# itzg/minecraft-server — Custom Plugin Jar Persistence **Date:** 2026-05-07 **Context:** `ChatChat-1.0.0-SNAPSHOT-racked-1.jar` was dropped manually into `/data/plugins/` and disappeared after the next container restart. --- ## 1. Why the ChatChat jar disappeared itzg's entrypoint runs this sequence on every start (when `REMOVE_OLD_MODS=TRUE`): 1. **Wipe** — every file in `/data/plugins/` matching `REMOVE_OLD_MODS_INCLUDE` and not matching `REMOVE_OLD_MODS_EXCLUDE` is deleted. 2. **Download** — every URL in `PLUGINS` and every Modrinth project in `MODRINTH_PROJECTS` is fetched into `/data/plugins/`. 3. **Copy** — anything listed in `COPY_PLUGINS_SRC` (or container paths inside `PLUGINS`) is rsynced in. Our current compose has: ```yaml REMOVE_OLD_MODS: "true" REMOVE_OLD_MODS_INCLUDE: "*.jar" REMOVE_OLD_MODS_EXCLUDE: "AuthLimbo*.jar" ``` So step 1 deletes **every** `*.jar` except `AuthLimbo*.jar`. The hand-placed ChatChat jar was not in any download list and not in the exclude glob, so it was nuked and never re-downloaded. AuthLimbo survives only because we explicitly excluded it. This is documented behaviour, not a bug — itzg's design assumes plugins are declarative, sourced from URLs/Modrinth/`COPY_PLUGINS_SRC`, never hand-dropped. ## 2. Three mechanisms to make custom jars persist | # | Mechanism | How | |---|-----------|-----| | A | **`REMOVE_OLD_MODS_EXCLUDE` glob** | Add `ChatChat*.jar` to the exclude list. Quick but fragile — depends on filename and only protects already-present files; doesn't handle re-deploy on a fresh volume. | | B | **`COPY_PLUGINS_SRC` bind-mount** | Mount a host dir of custom jars read-only at e.g. `/plugins-custom`, set `COPY_PLUGINS_SRC=/plugins-custom`. Entrypoint copies them in after the wipe. Survives wipes, version-controllable, declarative. | | C | **`PLUGINS` URL → Forgejo Release** | Upload the jar as a Forgejo Release asset, add the download URL to the existing `PLUGINS` list. Same flow as EssentialsX/spark already use. | Note: `PLUGINS` also accepts container paths directly (e.g. `PLUGINS=/plugins-custom/ChatChat.jar`), so mechanism B can collapse into the existing `PLUGINS` env if preferred. ## 3. Recommended path for racked.ru — Mechanism B `COPY_PLUGINS_SRC` is the cleanest fit: - Custom jars live in the repo (or `/opt/docker/minecraft-custom-plugins/`), so they're under version control / backup. - No external host dependency (Forgejo could be down — bind mount can't be). - Build artefacts from `staging/chatchat/` drop straight into the mounted dir. ### docker-compose.yml diff ```diff MODRINTH_LOADER: paper SPIGET_RESOURCES: "" REMOVE_OLD_MODS: "true" REMOVE_OLD_MODS_INCLUDE: "*.jar" - REMOVE_OLD_MODS_EXCLUDE: "AuthLimbo*.jar" + REMOVE_OLD_MODS_EXCLUDE: "AuthLimbo*.jar,ChatChat*.jar" + COPY_PLUGINS_SRC: "/plugins-custom" volumes: - /opt/docker/minecraft:/data + - /opt/docker/minecraft-custom-plugins:/plugins-custom:ro ``` Then on the host: ```bash sudo mkdir -p /opt/docker/minecraft-custom-plugins sudo cp staging/chatchat/ChatChat-1.0.0-SNAPSHOT-racked-1.jar \ /opt/docker/minecraft-custom-plugins/ sudo cp /opt/docker/minecraft/plugins/AuthLimbo-*.jar \ /opt/docker/minecraft-custom-plugins/ # optional: source-of-truth sudo chown -R 1000:1000 /opt/docker/minecraft-custom-plugins docker compose up -d --force-recreate mc ``` The `EXCLUDE` line still lists `ChatChat*.jar` so that if the bind mount ever vanishes, an existing copy in `/data/plugins/` isn't wiped — belt and braces. ## 4. Bonus — Forgejo Release upload procedure (mechanism C) If you'd rather host the jar at `git.s8n.ru` (e.g. for cobblestone or a friend's box without the bind mount): ```bash # 1. Tag and push cd staging/chatchat git tag -a chatchat-racked-1 -m "ChatChat racked build 1" git push origin chatchat-racked-1 # 2. Create release + upload asset (uses Forgejo PAT from ~/.config/veilor-forgejo) TOKEN=$(cat ~/.config/veilor-forgejo/pat) curl -X POST \ -H "Authorization: token $TOKEN" \ -H "Content-Type: application/json" \ https://git.s8n.ru/api/v1/repos/s8n/minecraft-server/releases \ -d '{"tag_name":"chatchat-racked-1","name":"ChatChat racked-1","draft":false}' RELEASE_ID=$(curl -s -H "Authorization: token $TOKEN" \ https://git.s8n.ru/api/v1/repos/s8n/minecraft-server/releases/tags/chatchat-racked-1 \ | jq -r .id) curl -X POST \ -H "Authorization: token $TOKEN" \ -H "Content-Type: application/java-archive" \ --data-binary @ChatChat-1.0.0-SNAPSHOT-racked-1.jar \ "https://git.s8n.ru/api/v1/repos/s8n/minecraft-server/releases/$RELEASE_ID/assets?name=ChatChat-1.0.0-SNAPSHOT-racked-1.jar" ``` Then add to compose: ```yaml PLUGINS: | ...existing... https://git.s8n.ru/s8n/minecraft-server/releases/download/chatchat-racked-1/ChatChat-1.0.0-SNAPSHOT-racked-1.jar ``` ## References - itzg docs: https://docker-minecraft-server.readthedocs.io/en/latest/mods-and-plugins/ - Source: https://github.com/itzg/docker-minecraft-server/blob/master/docs/mods-and-plugins/index.md - Issue #310 (COPY_PLUGINS_SRC behaviour): https://github.com/itzg/docker-minecraft-server/issues/310