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>
164 lines
6.6 KiB
Markdown
164 lines
6.6 KiB
Markdown
# 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.
|