#!/usr/bin/env bash # migrate-import.sh — restore a bundle made by migrate-export.sh onto THIS host. # # Run it from inside the target repo checkout (the compose project directory). # It restores .env, certs/, and the docker volumes into THIS host's compose # project, then prints the manual steps that finish the cutover. # # It assumes the machine already meets the requirements — run # tooling/migrate-check.sh # first. It does NOT deploy the distribution repo, issue certs, or start the # stack; those stay in your hands (see the printed tail + plan/22-migration.md). # # Usage: # tooling/migrate-import.sh BUNDLE.tar.gz # tooling/migrate-import.sh BUNDLE.tar.gz --path-prefix /home/ubuntu/mc=/home/you/mc # tooling/migrate-import.sh BUNDLE.tar.gz --without-certs # tooling/migrate-import.sh BUNDLE.tar.gz --force # overwrite existing volumes # # --path-prefix OLD=NEW rewrites the source-absolute paths in .env # (DISTRIBUTION_WEB_ROOT, GENERATORS_REPO, GENERATORS_DATA_DIR, FILES_DATA_DIR) # to where they live on this host. Repeatable. set -euo pipefail cd "$(dirname "$0")/.." # target repo root log() { printf '\n\033[1;32m==>\033[0m %s\n' "$*"; } warn() { printf '\033[1;33m!!\033[0m %s\n' "$*" >&2; } die() { printf '\033[1;31mxx\033[0m %s\n' "$*" >&2; exit 1; } # ── args ──────────────────────────────────────────────────────────── BUNDLE="" WITH_CERTS=1 FORCE=0 declare -a PREFIXES=() while [ $# -gt 0 ]; do case "$1" in --without-certs) WITH_CERTS=0; shift ;; --force) FORCE=1; shift ;; --path-prefix) PREFIXES+=("${2:?}"); shift 2 ;; -h|--help) sed -n '2,26p' "$0"; exit 0 ;; -*) die "unknown flag: $1 (see --help)" ;; *) [ -z "$BUNDLE" ] || die "extra arg: $1"; BUNDLE="$1"; shift ;; esac done [ -n "$BUNDLE" ] || die "usage: $0 BUNDLE.tar.gz [--path-prefix OLD=NEW] [--without-certs] [--force]" [ -f "$BUNDLE" ] || die "bundle not found: $BUNDLE" BUNDLE="$(cd "$(dirname "$BUNDLE")" && pwd)/$(basename "$BUNDLE")" # absolutize command -v docker >/dev/null || die "docker required" docker info >/dev/null 2>&1 || die "cannot reach the docker daemon" docker compose version >/dev/null 2>&1 || die "docker compose v2 plugin required (see migrate-check.sh)" [ -f docker-compose.yml ] || die "run this from inside the target repo checkout (no docker-compose.yml here)" # ── target compose project name (default: this dir's basename) ────── PROJ="${COMPOSE_PROJECT_NAME:-$(basename "$PWD")}" log "target compose project: $PROJ (in $PWD)" warn "volumes will be restored as ${PROJ}_ — this MUST match what 'docker compose' uses here." warn "if that looks wrong, set COMPOSE_PROJECT_NAME in .env and re-run." # ── unpack the bundle to a temp staging dir ───────────────────────── STAGE="$(mktemp -d "${TMPDIR:-/tmp}/ulicraft-import.XXXXXX")" trap 'rm -rf "$STAGE"' EXIT log "unpacking $(basename "$BUNDLE")" tar -C "$STAGE" -xzf "$BUNDLE" [ -f "$STAGE/MANIFEST.txt" ] || die "no MANIFEST.txt in bundle — is this a ulicraft migration bundle?" log "manifest:" sed 's/^/ /' "$STAGE/MANIFEST.txt" # ── verify checksums ──────────────────────────────────────────────── log "verifying sha256 checksums" ( cd "$STAGE" && awk '/^[0-9a-f]{64} /{print}' MANIFEST.txt | sha256sum -c - ) \ || die "checksum mismatch — bundle is corrupt or truncated" # volumes present in the bundle mapfile -t VOL_FILES < <(cd "$STAGE" && ls vol-*.tar.gz 2>/dev/null || true) [ "${#VOL_FILES[@]}" -gt 0 ] || die "no volume archives in bundle" # ── refuse to clobber existing volumes unless --force ─────────────── CLASH=0 for f in "${VOL_FILES[@]}"; do name="${f#vol-}"; name="${name%.tar.gz}" if docker volume inspect "${PROJ}_${name}" >/dev/null 2>&1; then warn "volume ${PROJ}_${name} already exists on this host" CLASH=1 fi done if [ "$CLASH" -eq 1 ] && [ "$FORCE" -eq 0 ]; then die "existing volumes would be overwritten. Re-run with --force to replace them (their current contents are discarded). If this is the WRONG host, stop now." fi # ── restore .env (backup any existing one first) ──────────────────── if [ -f .env ]; then BK=".env.bak.pre-migrate.$(date +%Y%m%d-%H%M%S)" warn "existing .env → $BK" cp -a .env "$BK" fi log "restoring .env" cp "$STAGE/.env" .env chmod 600 .env # apply --path-prefix rewrites for p in "${PREFIXES[@]}"; do old="${p%%=*}"; new="${p#*=}" [ -n "$old" ] && [ "$old" != "$p" ] || die "bad --path-prefix '$p' (want OLD=NEW)" log "rewriting .env paths: $old -> $new" # only touch the RHS of KEY=... lines; escape / for sed esc_old="$(printf '%s' "$old" | sed 's/[.[\*^$/]/\\&/g')" esc_new="$(printf '%s' "$new" | sed 's/[&/]/\\&/g')" sed -i "s/${esc_old}/${esc_new}/g" .env done # ── sanity-check the host-path vars in .env resolve on THIS host ───── log "checking host paths referenced by .env" set -a; . ./.env; set +a MISSING=0 for var in DISTRIBUTION_WEB_ROOT GENERATORS_REPO GENERATORS_DATA_DIR FILES_DATA_DIR; do val="${!var:-}" [ -n "$val" ] || { warn "$var unset in .env"; continue; } if [ -e "$val" ]; then printf ' \033[1;32m✓\033[0m %s = %s\n' "$var" "$val" else printf ' \033[1;33m!\033[0m %s = %s (does NOT exist here)\n' "$var" "$val" MISSING=$((MISSING+1)) fi done if [ "$MISSING" -gt 0 ]; then warn "$MISSING path(s) above don't exist on this host yet." warn "DISTRIBUTION_WEB_ROOT especially: the minecraft service bind-mounts kubejs" warn "from it and WON'T BOOT until it's populated. Deploy the distribution repo" warn "(and re-run with --path-prefix if the paths belong elsewhere) before starting." fi # ── restore certs/ ────────────────────────────────────────────────── if [ "$WITH_CERTS" -eq 1 ] && [ -f "$STAGE/certs.tar.gz" ]; then log "restoring certs/" [ -d certs ] && warn "existing certs/ will be merged/overwritten from the bundle" tar -C . -xzf "$STAGE/certs.tar.gz" elif [ "$WITH_CERTS" -eq 0 ]; then warn "--without-certs — not restoring certs; run tooling/issue-letsencrypt.sh here" else warn "bundle has no certs.tar.gz — run tooling/issue-letsencrypt.sh here" fi # ── restore the docker volumes ────────────────────────────────────── for f in "${VOL_FILES[@]}"; do name="${f#vol-}"; name="${name%.tar.gz}" vol="${PROJ}_${name}" log "restoring volume $vol" docker volume create "$vol" >/dev/null if [ "$FORCE" -eq 1 ]; then # wipe first so a --force restore is a clean replace, not a merge docker run --rm -v "$vol":/v alpine sh -c 'rm -rf /v/* /v/..?* /v/.[!.]* 2>/dev/null || true' fi docker run --rm -i -v "$vol":/v alpine tar -C /v -xzf - < "$STAGE/$f" done # ── done: print the manual tail ───────────────────────────────────── cat <\033[0m volumes + .env + certs restored.') Remaining manual steps (see plan/22-migration.md for detail): 1. Deploy the distribution repo so DISTRIBUTION_WEB_ROOT is populated (the minecraft service bind-mounts kubejs from it; it won't boot empty). Also add this host's Gitea deploy key for that repo. 2. Fetch vendored bits: tooling/fetch-authlib.sh tooling/fetch-launcher.sh and (needs WAN) tooling/sync-generators-data.sh 3. Host nginx vhost: tooling/render-nginx.sh --install 4. Certificates: tooling/issue-letsencrypt.sh (bundle certs boot nginx now, but nothing renews them until you run this) 5. Open the firewall: tcp 25565, 80, 443, ${CADDY_HTTP_PORT:-8880} ; udp 24454 6. Point DNS ${BASE_DOMAIN:-\$BASE_DOMAIN} + subdomains at THIS host. 7. Bring it up: ./update-all.sh --hard 8. Re-op players once they've joined: docker compose exec minecraft rcon-cli op Fresh-world gotcha: if minecraft crash-loops on a fresh mc_data with "AccessDeniedException: /data/kubejs/config" (ConsoleJS.STARTUP null), Docker created /data/kubejs as root while the container runs as uid 1000. Fix once: docker compose stop minecraft docker run --rm -v ${PROJ}_mc_data:/data alpine chown 1000:1000 /data/kubejs docker compose up -d minecraft Details: plan/22-migration.md (Troubleshooting). Rollback note: same domain means DNS points at ONE host. Once a player registers or changes a skin here, this drasl.db diverges from cochi — rolling back to cochi silently loses those changes. Keep the fallback window short. EOF