# Migrating the stack to another host Move the Ulicraft stack from one machine to another **keeping the same domain**, carrying the **Drasl users + skins** (and monitors + file-share index), but **not the Minecraft world**. Written for the cochi → oxps move, but the scripts take `--from` / paths so it generalises. Three scripts do the mechanical work; the rest is deploying the distribution repo, TLS, DNS, and firewall — the same one-time setup any fresh host needs. | script | runs on | does | |---|---|---| | `tooling/migrate-check.sh` | target (or `--host`) | validate the machine meets the requirements | | `tooling/migrate-export.sh` | **your workstation** | pull state from the source host into one bundle | | `tooling/migrate-import.sh` | target | restore that bundle into this host's stack | ## What actually moves **In the bundle** (`migrate-export.sh`): - `.env` — all secrets (RCON, `FILES_*` hashes, `DRASL_ADMIN_*`, OVH DNS API). - `certs/` — the Let's Encrypt cert+key for every name (opt out `--without-certs`). - volume `drasl_state` — **accounts, passwords, skins, capes, signing key**. 144K. This is the whole point of the migration. - volume `kuma_data` — Uptime Kuma monitors + history (~38M). - volume `filestash_state` — filestash sqlite + search index (~700K). **You provide on the target** (not in the bundle — too big, or you rebuild it): - The **distribution repo** checkout at `DISTRIBUTION_WEB_ROOT` (~3.3G). The `minecraft` service bind-mounts kubejs source dirs from it and **will not boot** until it exists. You deploy this yourself (Gitea deploy key is per-repo, per host). - `generators-data/` (~109M, needs WAN) via `tooling/sync-generators-data.sh`. - `runtime/authlib-injector.jar`, `launcher/` via their `fetch-*.sh` scripts. **Deliberately dropped:** the world (`mc_data`) and `backups/` — you said the world isn't needed. (Carry the world anyway with `migrate-export.sh --with-world`.) `www/`, `server-mods/`, and rendered configs are regenerated by `update-all.sh`. ## Requirements on the target `migrate-check.sh` splits them: - **`[restore]` (hard)** — docker daemon, **Compose v2** (`docker compose`, not the legacy `docker-compose` v1 binary), git, tar, gzip, ssh. Import refuses without these. - **`[deploy]` (warn)** — envsubst, rsync, node+pnpm, python ≥ 3.11, nginx, acme.sh, ≥12G RAM (`MAX_MEMORY: 10G`), ~10G free disk, free ports. You can restore without these, but need them before `./update-all.sh --hard`. Installing Compose v2 by hand (if the host only has v1): ```bash mkdir -p ~/.docker/cli-plugins curl -SL https://github.com/docker/compose/releases/latest/download/docker-compose-linux-$(uname -m) \ -o ~/.docker/cli-plugins/docker-compose chmod +x ~/.docker/cli-plugins/docker-compose docker compose version # confirm v2+ ``` ## Step by step (cochi → oxps) ### 0. Check the target ```bash tooling/migrate-check.sh --host oxps ``` Fix every `✗`. Warnings are fine to defer to step 5. ### 1. Export (on your workstation) ```bash tooling/migrate-export.sh --from cochi --out ~/bundles # → ~/bundles/ulicraft-bundle-cochi-YYYYMMDD-HHMMSS.tar.gz (mode 0600) ``` Reads the source read-only; writes nothing there. The bundle holds private keys and `.env` secrets — it is created `0600`, keep it that way. ### 2. Get the repo + bundle onto the target ```bash # clone the stack repo on oxps (add oxps's Gitea deploy key first) git clone ~/mc/ulicraft-server && cd ~/mc/ulicraft-server # copy the bundle over scp ~/bundles/ulicraft-bundle-cochi-*.tar.gz oxps:~/mc/ulicraft-server/ ``` The checkout **directory name is the Compose project name** (here `ulicraft-server`). Volumes restore as `_drasl_state`, and `docker compose` later looks for exactly that — so whatever you name the dir, be consistent. (Pin `COMPOSE_PROJECT_NAME` in `.env` if you want it fixed.) ### 3. Import (on the target) ```bash cd ~/mc/ulicraft-server tooling/migrate-import.sh ulicraft-bundle-cochi-*.tar.gz \ --path-prefix /home/ubuntu/mc=/home/$USER/mc ``` `--path-prefix OLD=NEW` rewrites the source-absolute host paths in `.env` (`DISTRIBUTION_WEB_ROOT`, `GENERATORS_REPO`, `GENERATORS_DATA_DIR`, `FILES_DATA_DIR`) to where they live on oxps. Import then checks each one exists and **warns loudly** about any that don't — `DISTRIBUTION_WEB_ROOT` especially, since the server won't boot without it. Guards: it refuses to overwrite volumes that already exist (pass `--force` to replace them — a clean wipe, not a merge), backs up any existing `.env`, and verifies the bundle's sha256 checksums before touching anything. ### 4. Deploy the distribution repo Clone/publish the distribution repo to the `DISTRIBUTION_WEB_ROOT` path from step 3. Two checkouts drift — publish deliberately, don't blind-rsync from cochi. Add oxps's own Gitea deploy key for that repo. ### 5. Finish the host setup ```bash tooling/fetch-authlib.sh tooling/fetch-launcher.sh tooling/sync-generators-data.sh # needs WAN tooling/render-nginx.sh --install # host nginx vhost → caddy tooling/issue-letsencrypt.sh # seed acme.sh + its renewal cron # firewall: tcp 25565, 80, 443, CADDY_HTTP_PORT ; udp 24454 ``` > **Bundle certs are a bridge, not a renewal.** The certs from the bundle let > nginx start immediately, but nothing renews them — acme.sh's state lived on > cochi. Run `issue-letsencrypt.sh` on oxps once so renewals happen here. It uses > the OVH **DNS-01** challenge, so it works before DNS points at oxps and needs > no inbound ports. ### 6. Cut over ```bash ./update-all.sh --hard # render → sync → build → up docker compose ps # all services healthy ``` Point DNS `${BASE_DOMAIN}` + subdomains at oxps. Once a player has joined: ```bash docker compose exec minecraft rcon-cli op # OPS env breaks on Drasl UUIDs ``` ## Rollback window — one way, mind the clock Same domain ⇒ DNS points at **one** host. cochi stays as a fallback, but the moment a player **registers or changes a skin on oxps**, `drasl.db` diverges from cochi's copy. Rolling back to cochi then **silently loses** those new accounts/skins. Safe for hours as a fallback; not for weeks. If you must roll back after real use, re-export from oxps and re-import onto cochi first. ## Re-running / testing Everything is idempotent-ish and safe to rehearse: - `migrate-export.sh` never writes on the source; run it as often as you like. - `migrate-import.sh` refuses to clobber existing volumes without `--force`, so a second run on a populated host stops instead of nuking data. To rehearse a full restore on a throwaway host, import, inspect, `docker compose down -v`, repeat.