feat(tooling): host migration scripts (export/import/check)
Move the stack to a new host keeping the same domain, carrying Drasl users + skins (+ kuma monitors, filestash index) but not the world. - migrate-check.sh: validate the target meets requirements. [restore] hard-fails (docker, compose v2, git, tar, ssh); [deploy] warns (node/pnpm, py>=3.11, nginx, acme.sh, RAM, disk, ports). --host runs it remotely by streaming itself over ssh. - migrate-export.sh: workstation-run, pulls the source read-only over ssh into one 0600 bundle (.env, certs/, drasl_state/kuma_data/ filestash_state volumes, checksummed MANIFEST). Certs in by default (--without-certs); world opt-in (--with-world). - migrate-import.sh: target-run, sha256-verify, refuse existing volumes (--force), backup+restore .env with --path-prefix rewrite, check host paths resolve, restore certs+volumes, print the manual tail. - plan/22-migration.md: human walkthrough (cochi -> oxps), rollback window warning, rehearsal notes. - .gitignore: ignore ulicraft-bundle-*.tar.gz (holds secrets + LE keys). Assumes Compose v2 on the target (no update-all.sh changes). Distribution repo, TLS issuance, DNS and firewall stay manual. Round-trip tested against prod: 9.2M bundle, drasl.db restored byte-exact, clobber guard verified. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
00e9815307
commit
b9bcc13b73
5 files changed
+625
No files matched your search
@@ -0,0 +1,163 @@
|
||||
# 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 <repo> ~/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 `<dirname>_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 <name> # 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.
|
||||
Reference in new issue
Block a user