# 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
