#!/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"