feat(compose): mount mods, config and kubejs writable

Drop :ro from ./server-mods:/mods and the four kubejs source subdirs, and
add a new ./server-config:/data/config bind — mod config used to live only
inside the mc_data volume, unreachable from the host without root.

The config bind needs seeding: a bind mount shadows what is under it, so an
empty ./server-config hides the generated config and every mod falls back to
defaults. tooling/export-server-config.sh copies /data/config out of the
volume first, halting if the target is non-empty, if the container is still
running, or if the mc_data volume name is ambiguous.

Writable mounts also need the server's uid (itzg runs as 1000) to have write
access on the host dir; oxps' host user is 1002, hence MC_UID/MC_GID.

The kubejs ROOT still must not be mounted — BaseProperties.save() NPEs and
kubejs never initialises. That is about mounting the root at all, not about
the mode, so dropping :ro does not change it. It does mean kubejs writes now
land in $DISTRIBUTION_WEB_ROOT as uncommitted changes in that host's checkout.

Also documents how to reach the mc_data volume (exec/cp, resolved host path,
stop-before-write rule) in plan/05-minecraft.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
oierandClaude Opus 5 committed 2026-07-26 00:46:30 +02:00
1 parent dfb8719074
commit 92c6f5fc8f
7 files changed
+252 -8

No files matched your search

+19 -8
View File
@@ -100,17 +100,28 @@ services:
volumes:
- mc_data:/data
- ./runtime:/extras:ro # authlib-injector.jar lives here
- ./server-mods:/mods:ro # itzg auto-syncs /mods → /data/mods
# Writable (no :ro) so edits round-trip: the container can write back into
# the host dir. All of these must be writable by the server's uid (itzg runs
# as 1000) — see plan/05-minecraft.md "Writable bind mounts".
- ./server-mods:/mods # itzg auto-syncs /mods → /data/mods
# Mod configs. NOT in mc_data anymore — bind-mounted so they are editable
# from the host without root. Seed the dir from the volume BEFORE the first
# boot with this mount (tooling/export-server-config.sh), otherwise an empty
# host dir shadows the existing /data/config and every mod regenerates
# defaults.
- ./server-config:/data/config
# Distribution client-override files the server also needs, from the live
# distribution (DISTRIBUTION_WEB_ROOT), same source as caddy. Only the kubejs
# *source* subdirs are bind-mounted ro — kubejs itself writes config/, cache/,
# *source* subdirs are bind-mounted — kubejs itself writes config/, cache/,
# generated/, logs/ at boot, so the kubejs ROOT must stay writable (lives in
# mc_data). Mounting the whole kubejs dir ro makes BaseProperties.save() NPE
# and kubejs never initialises. CustomSkinLoader is ro (read-only json).
- ${DISTRIBUTION_WEB_ROOT:?DISTRIBUTION_WEB_ROOT unset in .env}/servers/ulicraft-1.21.1/files/kubejs/server_scripts:/data/kubejs/server_scripts:ro
- ${DISTRIBUTION_WEB_ROOT}/servers/ulicraft-1.21.1/files/kubejs/startup_scripts:/data/kubejs/startup_scripts:ro
- ${DISTRIBUTION_WEB_ROOT}/servers/ulicraft-1.21.1/files/kubejs/data:/data/kubejs/data:ro
- ${DISTRIBUTION_WEB_ROOT}/servers/ulicraft-1.21.1/files/kubejs/assets:/data/kubejs/assets:ro
# mc_data). Mounting the whole kubejs dir makes BaseProperties.save() NPE
# and kubejs never initialises.
# These write back into the distribution checkout — an in-game /kubejs
# reload + edit lands in DISTRIBUTION_WEB_ROOT, i.e. in git. Intentional.
- ${DISTRIBUTION_WEB_ROOT:?DISTRIBUTION_WEB_ROOT unset in .env}/servers/ulicraft-1.21.1/files/kubejs/server_scripts:/data/kubejs/server_scripts
- ${DISTRIBUTION_WEB_ROOT}/servers/ulicraft-1.21.1/files/kubejs/startup_scripts:/data/kubejs/startup_scripts
- ${DISTRIBUTION_WEB_ROOT}/servers/ulicraft-1.21.1/files/kubejs/data:/data/kubejs/data
- ${DISTRIBUTION_WEB_ROOT}/servers/ulicraft-1.21.1/files/kubejs/assets:/data/kubejs/assets
- ${DISTRIBUTION_WEB_ROOT}/servers/ulicraft-1.21.1/files/CustomSkinLoader:/data/CustomSkinLoader:ro
depends_on:
- drasl # authlib (auth.) must resolve at boot