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>
88 lines
3.6 KiB
Bash
Executable File
88 lines
3.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# export-server-config.sh — seed ./server-config/ from /data/config inside the
|
|
# mc_data volume, so the compose bind mount `./server-config:/data/config` can
|
|
# take over without losing the mod configs the server already generated.
|
|
#
|
|
# WHY: a bind mount shadows whatever is under it. Mounting an empty host dir over
|
|
# /data/config hides every existing config file and each mod regenerates its
|
|
# defaults — silently reverting tuning. Run this ONCE, before the first boot with
|
|
# the mount in place.
|
|
#
|
|
# Cautious by design: refuses to run if the target already has content, if the
|
|
# volume can't be resolved unambiguously, or if the server is still running.
|
|
#
|
|
# Usage:
|
|
# tooling/export-server-config.sh # export, then chown to MC_UID:MC_GID
|
|
# MC_UID=1002 MC_GID=1002 tooling/… # oxps (host user oier = 1002)
|
|
set -euo pipefail
|
|
|
|
cd "$(dirname "$0")/.." # repo root
|
|
|
|
[ -f .env ] && { set -a; . ./.env; set +a; }
|
|
|
|
TARGET="./server-config"
|
|
MC_UID="${MC_UID:-1000}"
|
|
MC_GID="${MC_GID:-1000}"
|
|
|
|
# ── resolve the mc_data volume ──────────────────────────────────────────────
|
|
# Compose prefixes the volume with the project name (defaults to the repo dir
|
|
# name), so never hardcode it — match and require exactly one.
|
|
mapfile -t VOLS < <(docker volume ls -q | grep -E '(^|_)mc_data$' || true)
|
|
if [ "${#VOLS[@]}" -eq 0 ]; then
|
|
echo "FATAL: no mc_data volume found (docker volume ls). Nothing to export." >&2
|
|
echo " If this is a fresh host there is no config yet — create an empty" >&2
|
|
echo " $TARGET and let the server generate defaults into it." >&2
|
|
exit 1
|
|
fi
|
|
if [ "${#VOLS[@]}" -gt 1 ]; then
|
|
echo "FATAL: multiple mc_data volumes, refusing to guess:" >&2
|
|
printf ' %s\n' "${VOLS[@]}" >&2
|
|
exit 1
|
|
fi
|
|
VOL="${VOLS[0]}"
|
|
|
|
# ── refuse while the server is live (it rewrites config on shutdown) ────────
|
|
if [ -n "$(docker ps -q -f name='^minecraft$')" ]; then
|
|
echo "FATAL: the minecraft container is running. Stop it first:" >&2
|
|
echo " docker compose stop minecraft" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# ── refuse to clobber an existing export ────────────────────────────────────
|
|
if [ -d "$TARGET" ] && [ -n "$(ls -A "$TARGET" 2>/dev/null)" ]; then
|
|
echo "FATAL: $TARGET already has content — refusing to overwrite." >&2
|
|
echo " Move it aside if you really want a fresh export." >&2
|
|
exit 1
|
|
fi
|
|
mkdir -p "$TARGET"
|
|
|
|
# ── copy out of the volume ──────────────────────────────────────────────────
|
|
echo "Exporting $VOL:/data/config -> $TARGET"
|
|
docker run --rm \
|
|
-v "$VOL":/data:ro \
|
|
-v "$PWD/$TARGET":/out \
|
|
alpine sh -euc '
|
|
if [ ! -d /data/config ]; then
|
|
echo "FATAL: /data/config does not exist in the volume." >&2
|
|
exit 1
|
|
fi
|
|
cp -a /data/config/. /out/
|
|
'
|
|
|
|
# ── ownership: the server (uid 1000 under itzg) must be able to write ───────
|
|
docker run --rm -v "$PWD/$TARGET":/out \
|
|
alpine chown -R "$MC_UID:$MC_GID" /out
|
|
|
|
COUNT=$(find "$TARGET" -type f | wc -l)
|
|
echo "Exported $COUNT files, owned by $MC_UID:$MC_GID."
|
|
|
|
if [ "$(id -u)" != "$MC_UID" ]; then
|
|
echo
|
|
echo "NOTE: your host uid ($(id -u)) != $MC_UID, so $TARGET is not yours to edit." >&2
|
|
echo " Either edit via sudo, or add group write and join the group." >&2
|
|
fi
|
|
|
|
echo
|
|
echo "Next: the compose bind mount ./server-config:/data/config now takes over."
|
|
echo " docker compose up -d --force-recreate minecraft"
|