Captures live config state of nullstone Purpur 1.21.11 server: - docker-compose.yml (itzg/minecraft-server image, MODRINTH_PROJECTS + PLUGINS lists) - All plugin configs under live-server/plugins/ (no DBs, no jars, no world data) - Server core: bukkit.yml, spigot.yml, purpur.yml, paper-global.yml, paper-world-defaults.yml, server.properties Excluded via .gitignore: - World data (world/, world_nether/, world_the_end/, auth_limbo/) - Sensitive: AuthMe DB (password hashes), Lands DB, CoreProtect DB, Essentials userdata - Jars (auto-fetched), logs, caches, .paper-remapped
6.9 KiB
Roadmap — plugin acquisition overhaul
Goal: replace runtime env-driven plugin downloads with a reproducible, source-of-truth-first acquisition pipeline. Make the server fully open source, fully auditable, fully reproducible.
Problem (current state)
Plugins are pulled at every container boot via MODRINTH_PROJECTS + SPIGET_RESOURCES env vars in docker-compose.yml. Pain points hit during 2026-04-27 deploy:
- Slug ≠ name —
vault,protocollibnot on Modrinth at expected slug. Three boot loops to discover. - Channel mismatch — GrimAC alpha-only, WorldEdit beta-only on bleeding-edge MC versions. Default
releasefilter silently rejected. Two more boot loops. - Wrong env var —
MODRINTH_DEFAULT_VERSION_TYPEis for modpack flow;MODRINTH_PROJECTSflow needsMODRINTH_PROJECTS_DEFAULT_VERSION_TYPE. One more boot loop. - VERSION=LATEST + Purpur — Purpur 26.x versioning scheme confused itzg, sent "26.1.2" as MC version to Modrinth API; EssentialsX query returned no files.
- No lockfile —
latestdrifts daily. No checksum, no audit trail. - No pre-flight — every typo is a 30s container restart cycle.
- License opacity — no automated check that plugins are FOSS-compatible before adding.
REMOVE_OLD_MODS=*.jarwipes manually-placed jars on every boot, hostile to manual-only plugins (LoginSecurity, MarriageMaster, etc).
Acquisition order — proposed
- GitHub Releases (primary)
- Hangar (PaperMC official)
- Modrinth
- Spiget / SpigotMC
- Manual jar (last resort, premium/dead)
Why GitHub first
- Source-truth: jar built from tag, signed commit, reproducible.
- License visible — repo
LICENSEfile. FOSS audit trivial. - Stable URL pattern:
github.com/<owner>/<repo>/releases/download/<tag>/<asset>.jar. - API:
api.github.com/repos/<owner>/<repo>/releases/latest— JSON, version + asset URLs + checksums. - No platform lock-in (Modrinth/Hangar can delist; GH source survives).
- Most Bukkit plugins ARE on GitHub — Modrinth/Hangar often just mirror.
Design
plugins.yml (manifest, committed)
plugins:
- name: LuckPerms
sources:
- github: { owner: LuckPerms, repo: LuckPerms, asset_pattern: "LuckPerms-Bukkit-*.jar" }
- modrinth: luckperms
pin: latest # or "5.5.20" or sha256:abc...
- name: ProtocolLib
sources:
- github: { owner: dmulloy2, repo: ProtocolLib, asset_pattern: "ProtocolLib.jar" }
- spiget: 1997
- name: Vault
sources:
- github: { owner: MilkBowl, repo: Vault, asset_pattern: "Vault.jar" }
- name: WorldEdit
sources:
- github: { owner: EngineHub, repo: WorldEdit, asset_pattern: "worldedit-bukkit-*.jar" }
- hangar: { author: EngineHub, project: WorldEdit }
- modrinth: worldedit
channel: beta
- name: LandClaimPlugin
sources:
- modrinth: landclaimplugin
- name: LoginSecurity
sources:
- manual: ./manual-jars/LoginSecurity-3.3.1.jar
license: GPL-3.0
upstream_url: https://www.spigotmc.org/resources/loginsecurity.19362/
plugins.lock (generated, committed)
LuckPerms-Bukkit-5.5.20.jar sha256:abc... github:LuckPerms/LuckPerms@v5.5.20
ProtocolLib.jar sha256:def... github:dmulloy2/ProtocolLib@5.4.0
...
scripts/fetch-plugins.sh (resolver)
Runs before docker compose up. Pseudo:
for plugin in plugins.yml; do
for src in plugin.sources; do # fallback chain — first hit wins
case src.type in
github) asset=$(gh-api releases/latest); curl -L -o $asset ;;
hangar) curl hangar.papermc.io/api/v1/projects/... ;;
modrinth) curl api.modrinth.com/v2/project/$slug/version ;;
spiget) curl api.spiget.org/v2/resources/$id/download ;;
manual) cp $path ;;
esac
[ $? -eq 0 ] && break
done
sha256sum $jar >> plugins.lock
done
Output: plugins/*.jar directory ready to bind-mount, plus plugins.lock for diff/audit.
Compose changes
volumes:
- /opt/docker/minecraft:/data
environment:
REMOVE_OLD_MODS: "false" # plugins/ pre-populated, don't wipe
# delete: MODRINTH_PROJECTS, SPIGET_RESOURCES, MODRINTH_PROJECTS_DEFAULT_VERSION_TYPE
itzg image becomes pure runtime. Plugin acquisition is a separate, testable, reproducible build step.
Phased rollout
Phase 1 — pin everything (1 hour)
Keep itzg env-driven. Replace slug with slug:VERSION_ID in MODRINTH_PROJECTS. Use id:VERSION in SPIGET_RESOURCES. No more latest drift.
Acceptance: docker compose up -d produces identical plugin set on any host, any day.
Phase 2 — fetch script + manifest (1 day)
- Write
plugins.ymlw/ all current plugins + sources. - Write
scripts/fetch-plugins.sh(bash, jq, curl, gh CLI). - Write
plugins.lockfirst run, commit it. - Strip
MODRINTH_PROJECTS/SPIGET_RESOURCESfrom compose;REMOVE_OLD_MODS: false. - Document new deploy:
./scripts/fetch-plugins.sh && docker compose up -d.
Acceptance: plugins/ populated from GH-first, lock committed, deploy reproducible.
Phase 3 — CI automation (1 day)
- GH Action daily:
fetch-plugins.sh --check-updates→ open PR per update, body has changelog link. - GH Action per-PR: license audit (
/repos/{owner}/{repo}/license→ SPDX id →LICENSES.md). - Renovate-style auto-merge for patch updates (config-gated).
Acceptance: plugin updates land via PR, license audit in CI, no manual fetches.
Tooling to evaluate first
| Tool | Status | Verdict |
|---|---|---|
mcpkg |
exists, immature | reuse if active |
packwiz |
mod-focused, Modrinth/Curse | adapt? |
paper-plugin-manager |
Hangar client | use as Hangar source |
| Custom bash + jq + gh + curl | trivial to build | likely fastest |
Probably 200 lines of bash beats adopting an unmaintained tool.
Side benefits
- License audit — generated
LICENSES.mdproves the stack is FOSS. - Pre-flight —
fetch-plugins.sh --checkvalidates manifest in CI before merge, no boot-time surprise. - Offline deploy — pre-baked
plugins/dir + tarball = air-gap deploy possible. - Forks — easy to swap
LandClaimPluginupstream → your own fork by changing one line inplugins.yml.
Open questions
- License whitelist policy: GPL-3, MIT, Apache-2 OK? AGPL? Proprietary?
- Update cadence: daily auto-PR, weekly, manual?
- Pin granularity: per-plugin tag, sha256 hash, or commit SHA?
- Failure mode if a source delists a pinned version: pin migration script?
- Manual-jar storage: in-repo
manual-jars/(license risk) or separate private repo?
Status
- 2026-04-27 — roadmap drafted post-deploy painshare. Not started.
- 2026-04-28 — Phase 1 (pin versions) still pending.
REMOVE_OLD_MODSbug discovered: itzg disables it whenPLUGINSenv set, so manual jars are safe. Phase 2 design finalized here. No code yet.