universe docs source
browse docs
docs /quickstart

Quickstart

Build the fat JAR, write your first node config and instance configuration, create a template, and deploy a running instance via the REST API or console.

Universe ships as a single fat JAR produced by the loader module. The loader bootstraps a custom classloader, extracts the embedded app.jarinjar, resolves and downloads the runtime dependencies listed in dependencies.txt, then invokes the orchestrator. Everything (Master API, Wrapper runtime, and console) lives in that one file, so getting a node running takes only a few minutes once the JAR is built.

Prerequisites

  • JDK 25+ and Gradle 9.5+ to build from source.
  • Linux or macOS is recommended for the built-in screen and tmux runtimes.
  • Docker (optional) if you prefer the container quick-start.

  1. Build the JAR

    Clone the repository and run the Gradle build. Compiled artifacts are copied to .built/ automatically.

    ./gradlew build

    After a successful build you will find:

    .built/
      universe-loader-0.0.1.jar   ← the only file you need to run Universe
      minecraft-modern-*.jar
      minecraft-velocity-*.jar
      ...
    tip

    You can copy universe-loader-0.0.1.jar to any remote node. It is entirely self-contained.

  2. Run the JAR for the first time

    Navigate to the directory where you want Universe to store its data, then launch the loader:

    java -jar .built/universe-loader-0.0.1.jar

    On first run, Universe detects missing configuration and creates this layout:

    ./config.json          ← node identity and cluster settings
    ./database.json        ← database provider settings
    ./configuration/       ← instance configuration files (.json)
    ./templates/           ← template file trees (<group>/<name>/)
    ./running/             ← active instance working directories
    ./extensions/          ← extension JARs and their configs
    ./logs/universe.log    ← SLF4J/Logback log file

    The node starts in Master mode by default (isMasterNode: true). Stop the process (Ctrl+C or type stop) and edit the generated files before running again.

  3. Edit ./config.json

    The node configuration controls identity, cluster membership, and which role the node assumes. Open ./config.json and review every field:

    {
      "address": "127.0.0.1",
      "port": 6000,
      "apiPort": 7000,
      "nodeId": "node-1",
      "clusterName": "universe-cluster",
      "isMasterNode": true,
      "masterAddress": "127.0.0.1",
      "masterPort": 6000,
      "masterApiPort": 7000,
      "debug": false
    }
    FieldDescription
    addressIP address this node advertises to the Hazelcast cluster.
    portHazelcast cluster port (default 6000).
    apiPortKtor REST API port, only used when isMasterNode: true (default 7000).
    nodeIdUnique identifier for this node, used in instance assignment and template sync.
    clusterNameHazelcast group name, every node in a cluster must share this value.
    isMasterNodetrue starts the REST API and InstanceCountEnforcer; false joins as a Wrapper.
    masterAddressIP or hostname of the Master (used by Wrappers to join the cluster).
    masterPortHazelcast port on the Master.
    masterApiPortREST API port on the Master (used by Minecraft plugins and tooling).
    debugtrue enables DEBUG console and INFO framework logging; otherwise WARN+ only.
    i
    note

    A Master node can also run instances locally. You do not need a separate Wrapper for a single-machine deployment.

  4. Create your first template directory

    Templates are file trees under ./templates/<group>/<name>/. Universe copies the resolved tree into ./running/<instance-id>/ before launching, replacing variables in files listed under fileModifications.

    mkdir -p ./templates/server/base
    # Add any files your process needs, for example:
    cp /path/to/server.jar ./templates/server/base/server.jar

    A minimal server.properties using built-in variables:

    server-port=%PORT%
    server-ip=%HOST_ADDRESS%

    Built-in template variables replaced at deploy time:

    VariableValue
    %PORT%Allocated instance port.
    %INSTANCE_ID%6-character alphanumeric instance ID.
    %MASTER_IP% / %MASTER_ADDRESS%Master node address.
    %MASTER_PORT%Master Hazelcast port.
    %MASTER_API_PORT%Master REST API port.
    %NODE_ID%Local node ID.
    %HOST_ADDRESS%Local host address (or runtime-specific override).
    %CONFIGURATION_NAME%Configuration name.
  5. Create your first instance configuration

    Instance configurations live in ./configuration/ as JSON files. Create ./configuration/default.json:

    {
      "name": "default",
      "runtime": "screen",
      "command": "java -jar server.jar",
      "static": false,
      "instanceGroups": [],
      "nodes": ["node-1"],
      "hostAddress": "127.0.0.1",
      "availablePorts": { "min": 25565, "max": 25570 },
      "minimumServiceCount": 1,
      "environmentVariables": {},
      "templateInstallationConfig": {
        "allOf": [{ "name": "base", "group": "server", "storage": "local", "priority": 0 }],
        "allInGroups": [],
        "oneOf": [],
        "oneInGroups": [],
        "onTemplatePasteOverridePresentFiles": false
      },
      "fileModifications": ["server.properties"],
      "properties": {}
    }
    FieldDescription
    runtimeRuntime key: screen, tmux, process, docker, k8s.
    commandShell command executed inside the instance working directory.
    statictrue preserves the working directory between restarts.
    nodesList of nodeId values eligible to host this configuration.
    availablePortsPort range Universe scans when allocating the instance port.
    minimumServiceCountInstanceCountEnforcer keeps at least this many instances running.
    templateInstallationConfig.allOfTemplates always copied, in priority order (ascending).
    fileModificationsFiles scanned for %VARIABLE% replacement after template copy.
    propertiesCustom key→value pairs exposed as %KEY% template variables.
  6. Deploy an instance

    Start the node:

    java -jar .built/universe-loader-0.0.1.jar

    You can create an instance through the REST API or the console.

    # Create a new instance from the "default" configuration
    curl -X POST http://localhost:7000/api/instances \
      -H "Content-Type: application/json" \
      -d '{"configurationName": "default"}'

    The response contains the new InstanceInfo object including the assigned id, allocatedPort, and an initial state of CREATING.

    # Stop an instance by ID
    curl -X DELETE http://localhost:7000/api/instances/<id>
  7. Verify the running instance

    List all instances to confirm the new one is ONLINE:

    curl http://localhost:7000/api/instances

    Example response:

    [
      {
        "id": "a1b2c3",
        "configurationName": "default",
        "wrapperNodeId": "node-1",
        "hostAddress": "127.0.0.1",
        "allocatedPort": 25565,
        "state": "ONLINE",
        "lastHeartbeat": 1718000000000,
        "processPid": 12345,
        "runtime": "screen"
      }
    ]

    Other useful endpoints:

    # Health check
    curl http://localhost:7000/api/ping
    
    # Node info (version, uptime, resources)
    curl http://localhost:7000/api/node
    
    # Cluster node list
    curl http://localhost:7000/api/cluster/nodes
    
    # Tail last 100 log lines for an instance
    curl "http://localhost:7000/api/instances/a1b2c3/logs?lines=100"

Docker Compose quick-start

i
note

Docker Compose is the fastest way to get a Master running without building from source. The image lives at git.lunarlabs.dev/scala/universe:latest.

Create a docker-compose.yml in your working directory:

services:
  universe-master:
    image: git.lunarlabs.dev/scala/universe:latest
    container_name: universe-master
    stdin_open: true
    tty: true
    ports:
      - "7000:7000"   # REST API
      - "6000:6000"   # Hazelcast
    volumes:
      - ./data:/data

  # Optional: add a dedicated Wrapper node
  universe-wrapper:
    image: git.lunarlabs.dev/scala/universe:latest
    depends_on:
      - universe-master
    volumes:
      - ./wrapper-data:/data

For the Wrapper’s ./wrapper-data/config.json, set isMasterNode: false and point it at the Master:

{
  "isMasterNode": false,
  "masterAddress": "universe-master",
  "masterPort": 6000,
  "masterApiPort": 7000
}

Start both services:

docker compose up -d

Interact with the Master console via attach, or use the REST API:

# Interactive console attach
docker attach universe-master

# Or via REST API (recommended)
curl -X POST http://localhost:7000/api/commands/execute \
  -H "Content-Type: application/json" \
  -d '{"command": "cluster status"}'
!
warning

Instances spawned inside a Docker container inherit the container environment. Use the runtime-docker extension if you need isolated per-instance containers rather than bare processes inside the orchestrator container.