Run a Minecraft server that costs nothing while nobody plays
const server = await ix.machines.create({ name: "paper", ipv4: true })
const java = server.exec(["java", "-jar", "paper.jar", "--nogui"])
await server.stop() // nobody is online: only the world on disk bills
await server.start() // a player is back: the world is as you left itThis notebook runs a plain PaperMC Minecraft server on one ix machine. While players are on, it is a machine with a public IPv4 address. When the last one leaves you stop the machine, and the world waits on its disk until you start it again.
One limit up front: ix does not wake a stopped machine when a player connects. You, or a script of yours, start it.
Create the machine
ipv4: true asks for a public IPv4 address, which Minecraft clients need because they connect to a raw TCP port, not an HTTPS URL.
// @ix kernel
import { ix } from "@indexable/sdk";
const box = await ix.machines.create({ name: "paper", ipv4: true });
box.name"paper"Download Paper and accept the EULA
Open https://papermc.io/downloads/paper, copy the URL of the current build's jar, and put it in the PAPER_URL environment variable before you run this cell. The machine needs a Java runtime; the image here is assumed to carry one.
Writing eula=true to eula.txt below is you accepting the Minecraft EULA at https://aka.ms/MinecraftEULA. Read it first.
// @ix kernel
const paperUrl = Deno.env.get("PAPER_URL");
if (paperUrl === undefined) throw new Error("set PAPER_URL to the paper.jar URL from papermc.io/downloads/paper");
await box.shell(
`mkdir -p /srv/mc && cd /srv/mc && curl -fsSL -o paper.jar '${paperUrl}' && echo eula=true > eula.txt`,
{ check: true },
);
const { stdout } = await box.exec(["ls", "/srv/mc"]);
stdout.trim()eula.txt
paper.jarStart the server
exec returns a process at once. Keep the handle: releasing it cancels the command, and the server with it. A command that must outlive your session goes through box.spawn instead, which returns a pid and sends output to the machine logs.
The first start generates the world, so it takes longer than later ones. Paper prints Done when it accepts connections. The time in that line below is illustrative.
// @ix kernel
const server = box.exec(
["java", "-Xmx4G", "-jar", "paper.jar", "--nogui"],
{ cwd: "/srv/mc" },
);
for await (const line of server.stdout.lines()) {
console.log(line);
if (line.includes("Done (")) break;
}[12:00:03] [ServerMain/INFO]: Starting minecraft server version 1.21.4
[12:00:07] [Server thread/INFO]: Preparing level "world"
[12:00:09] [Server thread/INFO]: Done (4.2s)! For help, type "help"Join
Players add <ipv4>:25565 as a server address in the Minecraft client.
// @ix kernel
const info = await box.info();
`${info.ipv4}:25565`"203.0.113.7:25565"Stop it when nobody is online
Type list in the server console, or ask a status plugin, to see who is on. When nobody is, stop the machine. A stopped machine bills only its disk, and a removed one bills nothing (see billing). Stopping ends the java process, so the world is saved first with Paper's own stop command.
// @ix kernel
await server.stdin?.write("stop\n");
await server;
await box.stop();
(await box.info()).state"stopped"Start it again
Starting a stopped machine boots it with its disk as you left it. Processes do not come back, so run the same java command; Paper finds the world in /srv/mc and loads it, which is quicker than the first run.
// @ix kernel
await box.start();
const again = box.exec(["java", "-Xmx4G", "-jar", "paper.jar", "--nogui"], { cwd: "/srv/mc" });
for await (const line of again.stdout.lines()) {
console.log(line);
if (line.includes("Done (")) break;
}[12:41:50] [Server thread/INFO]: Preparing level "world"
[12:41:52] [Server thread/INFO]: Done (2.1s)! For help, type "help"Optional: try a plugin on a copy
Snapshot the world and restore it into a second machine named paper-test. The copy shares the snapshot's bytes with the original until either side writes. A restore takes the disk, not running processes: memory is not in a snapshot yet, so start java again on the copy. See snapshots.
The cell flushes the world to disk first, so the snapshot holds a saved world while the server keeps running.
// @ix kernel
await again.stdin?.write("save-all flush\n");
const snap = await box.snapshot();
const test = await ix.machines.create({ name: "paper-test", snapshot: snap.snapshotId, ipv4: true });
const { stdout: world } = await test.exec(["ls", "/srv/mc/world"]);
world.trim().split("\n").slice(0, 3)["data", "entities", "level.dat"]