Creating a Custom Recipe
This tutorial builds a small Lumi Bedrock core recipe. It demonstrates the manifest contract and test workflow; the source selector, image, memory, and ownership list are examples derived from the repository's reference fixtures, not guaranteed production recommendations. Review upstream support, licenses, EULA terms, ports, and resource needs for your deployment.
1. Create the recipe directory
nesk init --dir ./my-lumi-core --name my-lumi-core
init creates a valid-shaped starter whose artifact URL is intentionally
non-working. Replace that manifest with the example below:
my-lumi-core/
└── luminesk.toml
The finished manifest is shown below, followed by an explanation of each design decision.
manifest_version = 1
[package]
name = "my-lumi-core"
version = "0.1.0"
display_name = "My Lumi Core"
kind = "core"
game = "minecraft"
edition = "bedrock"
summary = "A Lumi Bedrock server maintained by Example Org"
keywords = ["lumi", "bedrock", "plugins"]
license = "MIT"
authors = ["Example Org"]
[package.repository]
url = "https://github.com/example/my-lumi-core"
[inputs.memory]
type = "string"
default = "4g"
prompt = "Java heap and container memory limit"
pattern = "^[1-9][0-9]*[mMgG]$"
[inputs.port]
type = "integer"
default = 19132
min = 1
max = 65535
[[sources]]
id = "core"
type = "maven"
target = "server.jar"
max_size = 536870912
[sources.options]
repository = "https://repo.lumi.su/releases"
group = "com.koshakmine"
artifact = "Lumi"
version = "latest"
channel = "stable"
[[files]]
source = "data/worlds"
target = "worlds"
mode = "data"
[[files]]
source = "data/players"
target = "players"
mode = "data"
[[files]]
source = "data/behavior_packs"
target = "behavior_packs"
mode = "data"
[[files]]
source = "data/plugins"
target = "plugins"
mode = "data"
[[files]]
source = "data/resource_packs"
target = "resource_packs"
mode = "data"
[[files]]
source = "data/tmp"
target = ".tmp"
mode = "data"
[ownership]
preserve = ["settings.yml"]
data = [".tmp", "worlds", "plugins", "players", "behavior_packs", "resource_packs"]
[runtime]
image = "eclipse-temurin:21-jre"
command = ["java", "-Xms${input.memory}", "-Xmx${input.memory}", "-jar", "server.jar"]
workdir = "/server"
memory = "${input.memory}"
stop_signal = "SIGINT"
stop_timeout = 30
restart = "unless-stopped"
read_only_root = true
[[runtime.mounts]]
source = "."
target = "/server"
mode = "rw"
[[runtime.mounts]]
source = ".tmp"
target = "/tmp"
mode = "rw"
[[runtime.ports]]
name = "game"
host = "${input.port}"
container = 19132
protocol = "udp"
[[checks]]
id = "core-present"
phase = "post-build"
kind = "file"
path = "server.jar"
[[checks]]
id = "ready"
phase = "readiness"
kind = "log-regex"
pattern = "Done .* For help, type"
timeout = 120
required = true
[update]
strategy = "transactional"
backup = ["worlds", "plugins", "players", "behavior_packs", "resource_packs", "settings.yml"]
retain_backups = 3
rollback_on_failure = true
2. Describe identity and inputs
package.version versions your recipe. Bump it whenever published recipe
content changes so operators can distinguish revisions. It is separate from
the Lumi artifact selected by the core source.
The inputs have usable defaults and validation. The port input selects the
host-side Docker UDP port; Lumi listens on container port 19132. The memory
input is used both in Java's argv and Docker's memory limit. Lumi serves Bedrock
clients even though its runtime is Java.
This recipe has no EULA input or template. There is intentionally no
settings.yml template: Lumi creates its complete configuration on first
start. A partial template would replace that whole file, not merge individual
settings into an upstream default.
3. Resolve the server binary
The maven provider resolves com.koshakmine:Lumi from
https://repo.lumi.su/releases, as in the
official Lumi recipe.
version = "latest" with channel = "stable" selects the highest matching
stable version at lock time. The lock records the exact version, URL, size,
and SHA-256. When the repository supplies a SHA-256 sidecar, Luminesk verifies
it; otherwise, it hashes the downloaded bytes. To select a release deliberately,
replace latest with an exact published version.
The source target server.jar is a package-relative destination. It is also
the path used by the Java argv and the post-build file check.
4. Define templates and user data
The ownership declaration reserves settings.yml for the server and operator.
Because the recipe does not package that path, Lumi can create the complete
file on first start and later package updates leave it alone.
Each mode = "data" file declaration above names a source directory that does
not need to exist. For data mode only, an absent source creates an empty target
directory. Lumi can then populate worlds and plugins without making their
contents Luminesk-managed. If you want seed data, create the corresponding
directory in the recipe and put regular files in it.
If you add a top-level template tree, its output is generated by default.
Every .tmpl file must be UTF-8; Luminesk renders ${input.NAME} placeholders
and removes the suffix. Template rendering always produces a complete file; it
never merges individual properties into a server-owned file. Prefer omitting
editable server configs from the package and declaring them preserve. If a recipe genuinely needs to
seed one, ship a complete upstream-compatible configuration, mark it
preserve, and document that future recipe defaults will not replace an
existing operator copy. Use generated only when replacing the entire
untouched file is the intended behavior, and managed for immutable recipe
content.
5. Define Docker runtime and readiness
The image tag is convenient in source; nesk lock resolves it to a repository
digest. Runtime commands are argv, never shell text. The explicit root mount
makes instance files visible at /server; the root filesystem outside that
mount remains read-only. The instance-contained .tmp directory provides a
writable /tmp for the Java runtime.
The readiness regex matches Lumi's normal startup log. Validate it against the exact server release you deploy. A required timeout removes a newly started container. During update, Luminesk then restores and restarts the previous instance.
post-build checks package staging. post-install checks the applied instance.
Both phases currently support only file checks.
6. Validate, lock, and plan
Static validation does not contact providers:
nesk validate --dir ./my-lumi-core --static
Resolve providers and the Docker image, then create the lock:
nesk validate --dir ./my-lumi-core --resolve
nesk lock --dir ./my-lumi-core
Review luminesk.lock, but do not edit it. Validate package assembly, then
preview the plan with a memory override:
nesk validate --dir ./my-lumi-core --build
nesk plan --dir ./my-lumi-core \
--set memory=2g
For a recipe with secret inputs, use --set-file NAME=PATH just as you would for
an install. Validation reads the secret for temporary rendering without persisting
it.
7. Test a local install
Install into a separate empty directory so the recipe source remains clean:
nesk install ./my-lumi-core \
--dir ./test-lumi \
--set memory=2g \
--dry-run
nesk install ./my-lumi-core \
--dir ./test-lumi \
--set memory=2g \
--yes
Then exercise the lifecycle:
nesk validate --dir ./test-lumi --instance
nesk start --dir ./test-lumi
nesk status --dir ./test-lumi
nesk logs --dir ./test-lumi
nesk validate --dir ./test-lumi --readiness
nesk stop --dir ./test-lumi
# Edit the complete settings.yml generated by Lumi.
nesk start --dir ./test-lumi
Inspect the complete server-generated configuration, ownership behavior, port, memory usage, graceful shutdown, and startup logs. Test with disposable worlds before treating the recipe as production-ready.
8. Test a real recipe update
A local recipe install stores an immutable untracked snapshot; nesk update
does not follow later edits in the original local directory. To test the remote
update path, publish the recipe to a review branch and install that branch:
nesk install example/my-lumi-core@main \
--dir ./tracked-lumi \
--yes
Change the recipe, bump package.version, validate it, and push the branch.
Then review and apply from the tracked test instance:
nesk outdated --dir ./tracked-lumi
nesk diff --dir ./tracked-lumi
nesk update --dir ./tracked-lumi --dry-run
nesk update --dir ./tracked-lumi --yes
nesk status --dir ./tracked-lumi
nesk logs --dir ./tracked-lumi
Verify that edited settings.yml, worlds, and plugins were preserved;
untouched managed files changed as planned; readiness passed; and a deliberate
failed readiness condition restores the previous package/runtime. Keep an
independent backup while testing rollback.
9. Optional Dockerfile build
Use [build] only when a provider artifact and templates cannot produce the
package directly:
[build]
file = ".luminesk/Dockerfile"
output = "/out"
timeout = 1200
cpu = 2
memory = "2g"
network = false
There is no [permissions] table. Declaring [build] enables the isolated
Docker build; network = true explicitly allows its default Docker network.
Review every Dockerfile and keep networking disabled unless the build truly
needs it.
Use the luminesk.toml Reference when extending
the recipe with another provider, platform-specific artifacts, checks, or
mounts.