ConduitDocs

Migrating a live network onto Conduit

How to move a running Minecraft network — CloudNet-style templates, static services, Skript systems, databases, worlds — onto a Conduit cluster without losing what makes it that network. This is the playbook that moved the Skydinse lobby and the full TimeSMP survival (spawn + three sharded regions) onto Conduit, generalized. Every trap in here was hit for real.

For porting gamemode code (rounds, maps, stats hooks), see skript/conduit-games/PORTING-GUIDE.md — this document is about servers and the network around them.

The shape of a migration#

Conduit has three layers, and a live server maps onto all three:

Live networkConduitNotes
CloudNet task/template (TMspawn, Lobby)Task on a blueprintdynamic or static; the blueprint carries software + version
Static service dir (local/services/TMspawn-1)the provisioned containerfor static live services this is the authoritative source — templates drift
Bridge plugin (CloudNet-Bridge, Skyd.Redis, HuskSync)connector (plugin/)routing, messaging, inventory share, stats — built in
Skript systemsSkript overlay, unchanged where possibleperf-critical systems live in the connector instead

Two facts shape everything below:

  • Names. Conduit servers are <task>-<vmid> (timesmp-spawn-214) and tasks themselves contain hyphens. Live code that splits a server name at - and takes the first token — CloudNet's single-token task convention — breaks three ways at once (spawn detection, {server.task}, seamless-spawn triggers). Audit every split at "-" before anything boots.
  • Sources of truth. For a static live service, pull from local/services/<name> — the running directory. Templates are where stale worlds and outdated Citizens saves live. The TimeSMP spawn's template carried an NPC set two seasons old; the static service had the real one.

Order of operations#

Per server, in this order — each step's failures are invisible until the next one runs, so skipping ahead costs a debugging round later:

  1. Blueprint + task. Create the task on the matching blueprint (paper flavor, memory, cores). Provisioning clones the golden image, installs software, writes paper-global.yml with Conduit's forwarding secret, and installs the connector. Let it finish before touching files.
  2. Deploy the content bundles (worlds, plugins, scripts) — see bundle rules below.
  3. Wire the databases. Add the server's database ids to its stats config; the connector serves credentials after it registers (see the wait pattern below).
  4. Push languages. Catalogs are group-scoped — a host resolves to its role group, and pushing regenerates the per-host data file. A <none> in a scoreboard or hologram means the catalog for that system never arrived, not that the code is broken.
  5. Routing. The proxy picks the server up from registration. If the live network routed by a group name that is not the task name (susigroups), add the alias mapping rather than renaming tasks to match.
  6. Verify — the checklist at the end. Then, and only then, point players at it.

Bundle rules#

The single most expensive class of migration bug is content that came along for the ride.

  • Never tar a server's whole Skript/scripts for a different role. The lobby's tree contains lobby/ join handlers, NPC spawners and warp definitions; deployed onto TimeSMP they overrode the SMP's own spawn placement every join (players teleported into the void one second after arriving) and spawned the lobby's NPCs in the survival world. Build per-role script sets: shared glue + shared systems + that role's own scripts, nothing else.
  • Jars and plugins flow through the panel, not hand copies. The panel's drift view shows which instance runs which jar; a hand-copied jar is invisible to it and survives no reprovision.
  • A script in the repo is not a script on the host. Verify on the running server. The /discord-style social commands were "deployed" twice before anyone checked the host and found the file absent everywhere.
  • Live templates ship their own paper-global.yml with the live network's forwarding secret inside. Deploying a template tarball over a provisioned server silently replaces Conduit's secret, and every proxy connect fails with Unable to verify player details. Re-assert the secret (or deploy configs selectively) after any template import.

Identity, secrets, credentials#

The hyphen-split trap. Three confirmed sites in the Skydinse stack, all with the same shape:

# live: TMspawn-1 → "TMspawn"; Conduit: timesmp-spawn-214 → "timesmp" (wrong)
set {_n::*} to {_server} split at "-"

Fix by comparing against the task portion (everything before the final -<vmid>) with a first-segment fallback for live compatibility. The known victims: spawn-server detection (smpIsSpawnServer), the {server.task} global, and the seamless-spawn trigger dispatch. Assume there are more in any code you port.

Connector-served credentials are NOT SET until registration. The connector registers with the panel seconds after the JVM starts — after Skript has loaded every script. Any script that builds a database connection at load time from conduit … host-style expressions races that registration, and there are two ways to lose:

  • Comparing the expression against "" — an absent value is not set, which is not equal to "", so the guard falls through and the URL is built out of <none>.
  • Building the connection handle as the probe — Skript's the database "…" opens its pool eagerly, so "try, then check" burns a dead pool every boot.

The pattern that works, everywhere:

set {_h} to conduit gamestats host
if {_h} is not set:
    set {_h} to ""
while {_h} is "":
    wait 2 seconds
    set {_h} to conduit gamestats host
    if {_h} is not set:
        set {_h} to ""
# only now build the connection

Six systems needed exactly this (EssentialsY, shop, sign, profile, susi/profile handles, the works-council election) — every one of them was a boot-race that only detonated on cold boots, because warm restarts reuse the connector's cached config. A migration is nothing but cold boots.

Hardcoded live databases are a live-fire hazard. The live templates carried the production database host and password for EssentialsY inline in settings.sk. On Conduit that host was unreachable — which was the only thing preventing the migrated servers from writing into the still-running live network's data. Grep every ported settings file for hostnames and credentials before first boot, and point them at the connector-served values.

Sharded SMPs: serverlogic vs ConduitSharding#

A live SMP split across region servers (smpcore serverlogic) can either adopt Conduit's sharding or keep its own. The answer that survived contact: keep serverlogic, feed it Conduit's pieces.

  • Run the regions as a normal multi-instance task. Disable Conduit's connector-side sharding on it (sharding.enabled: false — the baked level seed survives; only the POST /sharding endpoint regenerates worlds). Two border/transfer systems double-fire, and active connector sharding suppresses the inventory share by design.
  • Give every region the same level seed so strip terrain is continuous — set it once at provision.
  • Point serverlogic's cluster list at the Conduit server names (timesmp-region-216/…), on the regions and the spawn (that list is how /rtp finds targets). The names are per-vmid, so a reprovision means updating the list.
  • Inventory share: one Conduit InvShare group spanning spawn + regions — the equivalent of the live HuskSync cluster_id. Spawn included: on live, spawn and regions shared one inventory cluster.

Data#

  • Worlds: copy from the static service for builds that matter; regenerate from the live seed for a fresh season (the TimeSMP regions chose regeneration — identical terrain, no 375 GB transfer).
  • Citizens NPCs: saves.yml binds NPCs to a world UUID. Match it to the deployed world's uid.dat or every NPC silently fails to spawn. And Citizens writes saves.yml from memory on shutdown — replace the file only after the JVM has fully exited, or your edit is overwritten.
  • SQL: player data lives in SQL and migrates as per-database dumps. The panel re-asserts roles and grants continuously, so dumps carry data only. Any server whose portal/routing logic queries an SMP's stats database needs that database id mapped on itself — the lobby needs the SMP's db for its portal's ban check, not just the SMP servers.
  • Skript variables are not a data store. The live config's variable backend pattern excludes everything (pattern: !.*) — variables are runtime cache, reasserted from config files at load. If a ported system keeps real state only in Skript variables, that state dies on restart; move it to SQL.

The per-server verification checklist#

Every LXC instance across the cluster, with node, address, memory and uptime

A server is migrated when all of these hold, in this order:

  1. Clean parse. Zero Cannot determine which function in the boot log. Two same-named functions with overlapping signatures make Skript refuse every call site — silently, at parse time, killing whole features (spawn teleports, /rtp) while everything else works. This is a time bomb: it arms when a second definition lands on disk and detonates at the next boot, which may be weeks later. Grep the compat/stub files for every function a newly deployed module defines.
  2. Zero dead database pools. No HikariPool … Exception and no '<none>:<none>' in the boot log; every bridge logs its "connection established".
  3. Registered and ready. The connector registered with the panel; the instance shows ready; the proxy lists it.
  4. Languages resolve. No <none> in scoreboards, holograms, GUIs.
  5. The in-game walk. Join through the real portal/route, land on the real spawn, run the role's core loop (/rtp and a border crossing for an SMP; a round for a game). Server-side checks cannot see a wrong spawn platform or a dead NPC.

When it breaks anyway#

  • Unable to verify player details → forwarding secret mismatch, almost always a template's own secret deployed over Conduit's. Check paper-global.yml inside the container — June-era containers may keep local config dirs where the shared copy is a decoy.
  • Player lands at world spawn instead of the build → spawn-flow identity checks (the hyphen split), or a foreign role's join handler in the script tree.
  • Feature dead with a clean-looking log → parse-time ambiguity (checklist #1) or a stub function shadowing the real implementation.
  • Worked yesterday, dead today after nothing changed → it was never re-parsed yesterday. Anything that only runs at boot (script parses, config reads, credential waits) is only tested by a cold boot. Restart a migrated server on purpose before calling it done.
Edit on GitHubdocs/migration.md 7 min readUpdated