Zudoku
Features

ROMs

InterfacesAPICLISDKs

Unikraft Cloud supports the ability to attach Read-Only Memory (ROM) blobs to instances. It allows you to create a general-purpose base image and then customize individual instances by attaching code or data as separate ROM blobs. This enables quick deployment of custom functionality to preexisting language environments without rebuilding the entire image.

Overview

With ROMs, you can:

  • Deploy variations of an app from a single base image.
  • Update app code without rebuilding the base image.
  • Reduce image size and deployment time.

The ROM workflow consists of two main components:

  1. Base Image: A general-purpose image containing the runtime environment (for example, Python interpreter, Node.js, etc.).
  2. ROM Blobs: Separate, lightweight images containing your app code or any other data you want to customize.

This separation allows you to maintain one base image while deploying many different instances with different data.

By default, the app running inside the base image needs to mount the attached ROM device. ROM blobs appear as block devices at paths like /dev/ukp_rom_<rom_name>, where <rom_name> is the ROM name you specify when creating the instance.

For convenience, this guide uses automounting by setting the at field on a ROM entry. When at is present, the platform automatically mounts the ROM at the given path on boot, so your app can read files from it directly without any manual mount step.

Setup

This example shows how to deploy Python functions using ROMs on Unikraft Cloud. Ensure you have the CLI installed and configured with your Unikraft Cloud account. Make sure to log into Unikraft Cloud and pick a metro close to you. This guide uses fra (Frankfurt, 🇩🇪):

TerminalCode
unikraft login

Base image

First, create a base image with a Python HTTP server that loads and executes custom Python programs from a ROM.

Dockerfile
FROM python:3.12 AS build RUN set -xe; \ /usr/sbin/ldconfig /usr/local/lib FROM scratch # Python binary COPY --from=build /usr/local/bin/python3 /usr/bin/python3 # System binaries COPY --from=build /usr/bin/mount /usr/bin/mount COPY --from=build /usr/bin/sh /usr/bin/sh COPY --from=build /usr/bin/mkdir /usr/bin/mkdir # System Libraries COPY --from=build /lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/libc.so.6 COPY --from=build /lib/x86_64-linux-gnu/libm.so.6 /lib/x86_64-linux-gnu/libm.so.6 COPY --from=build /lib/x86_64-linux-gnu/libmount.so.1 /lib/x86_64-linux-gnu/libmount.so.1 COPY --from=build /lib/x86_64-linux-gnu/libselinux.so.1 /lib/x86_64-linux-gnu/libselinux.so.1 COPY --from=build /lib/x86_64-linux-gnu/libblkid.so.1 /lib/x86_64-linux-gnu/libblkid.so.1 COPY --from=build /lib/x86_64-linux-gnu/libpcre2-8.so.0 /lib/x86_64-linux-gnu/libpcre2-8.so.0 COPY --from=build /usr/lib/x86_64-linux-gnu/libz.so.1 /usr/lib/x86_64-linux-gnu/libz.so.1 COPY --from=build /usr/lib/x86_64-linux-gnu/libcrypto.so.3 /usr/lib/x86_64-linux-gnu/libcrypto.so.3 # Dynamic linker / loader COPY --from=build /lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2 COPY --from=build /etc/ld.so.cache /etc/ld.so.cache # Python (and other) libraries COPY --from=build /usr/local/lib /usr/local/lib # Python app COPY ./wrapper.sh /wrapper.sh COPY ./server.py /src/server.py

The commands below will use the at field to automount the ROM. That's why the Kraftfile omits the wrapper.sh script and the app can read the ROM file directly from /rom/rom.py.

This base image builds for x86_64 only, and its Dockerfile copies libraries from architecture-specific paths such as /lib/x86_64-linux-gnu. To run the same base image on ARM hosts, add kraftcloud/arm64 to targets, and copy from the matching aarch64-linux-gnu paths. See architectures for more detail.

Package and push the base image:

TerminalCode
unikraft build . --output <my-org>/http-python:latest

Wait a few seconds for propagation and check that the image is present:

TerminalCode
unikraft images list

ROM files

To showcase the benefits of using ROMs, create two files, each containing a Python function that the base image will load and execute. Create a separate directory for each of the ROMs:

Terminal
mkdir -p rom1/fs rom2/fs
def function(): return "Hi from 1st ROM!\n"

ROMs are architecture-specific. A ROM Kraftfile has no runtime to take architectures from, so it names them itself under targets, or you pass --arch on the command line. A ROM build that does neither fails. Listing more than one architecture builds the ROM for each of them in one invocation, and each instance needs the ROM that matches the architecture of its base image.

Package and push the ROMs:

TerminalCode
unikraft build rom1/ --output <my-org>/my-rom1:latest unikraft build rom2/ --output <my-org>/my-rom2:latest

Wait a few seconds for upload and check that the ROMs are present:

TerminalCode
unikraft images list

Instances

Create two instances with the same base image and attach different ROMs:

unikraft run --metro fra \ --name test-http-python-rom1 \ -m 512M \ -p 443:8080/tls+http \ --domain test-http-python-rom1 \ --scale-to-zero policy=on,cooldown-time=1000,stateful=false \ --rom name=python_function,image=<my-org>/my-rom1:latest,at=/rom \ --image <my-org>/http-python:latest unikraft run --metro fra \ --name test-http-python-rom2 \ -m 512M \ -p 443:8080/tls+http \ --domain test-http-python-rom2 \ --scale-to-zero policy=on,cooldown-time=1000,stateful=false \ --rom name=python_function,image=<my-org>/my-rom2:latest,at=/rom \ --image <my-org>/http-python:latest

The API requires a name on each ROM entry, while the unikraft CLI derives one from at when you omit name=. See ROM names and mount points for the rules that both fields follow.

Check that the instances are up:

TerminalCode
unikraft instances get test-http-python-rom1 unikraft instances get test-http-python-rom2

Note the roms array in the instances configurations. Each ROM is available as a readable device at /dev/ukp_rom_python_function, in the form of an EROFS filesystem. In this example, the platform mounts it automatically at /rom, and the app reads /rom/rom.py from that mount point.

Testing

Query the instances to call the Python function from the ROM. You should see different responses:

TerminalCode
curl https://test-http-python-rom1.fra.unikraft.app curl https://test-http-python-rom2.fra.unikraft.app
Code
Hi from 1st ROM! Hi from 2nd ROM!

Cleanup

When done, remove the instances:

TerminalCode
unikraft instances delete test-http-python-rom1 test-http-python-rom2

Inline ROMs

Instead of pre-packaging and pushing a ROM image to a registry, you can provide file contents directly in the instance creation request. This is useful when you create instances from a template where the snapshot locks ENV and ARGS, but you still need to inject instance-specific data.

An inline ROM is ephemeral: it's automatically deleted once the last reference to it is closed.

To use inline ROMs with unikraft, specify a local directory as source:

TerminalCode
unikraft run --metro fra \ --name test-http-python-rom-inline \ -m 512M \ -p 443:8080/tls+http \ --domain test-http-python-rom-inline \ --scale-to-zero policy=on,cooldown-time=1000,stateful=false \ --rom dir=./rom1/fs,at=/rom \ --image <my-org>/http-python:latest

API format

The API allows you to specify the contents of an inline ROM directly in the JSON request body when creating an instance. Add a files array to a ROM entry instead of an image field:

TerminalPOST /instances
curl -X POST \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Content-Type: application/json" \ "https://api.fra.unikraft.cloud/v1/instances" \ -d '{ "image": "<my-org>/my-base:latest", "roms": [ { "name": "my-rom", "files": [ { "path": "config/settings.json", "encoding": "text", "data": "{\"debug\": false, \"port\": 8080}" }, { "path": "certs/client.pem", "encoding": "base64", "data": "LS0tLS1CRUdJTi..." } ], "at": "/rom/my-rom" } ] }'

Each file in the files array has three fields:

FieldDescription
pathFile path inside the ROM filesystem. The platform creates directories automatically.
encodingEither text (UTF-8 string) or base64 (base64-encoded binary data).
dataThe file contents in the specified encoding.

The platform packages the provided files into an EROFS filesystem and attaches it as a ROM device, the same way image-based ROMs work. The at field is optional and works the same as for image-based ROMs—when present, the platform mounts the ROM at the given path automatically.

Combining with image-based ROMs

You can mix inline ROMs and image-based ROMs in the same request:

TerminalPOST /instances
curl -X POST \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Content-Type: application/json" \ "https://api.fra.unikraft.cloud/v1/instances" \ -d '{ "image": "<my-org>/my-base:latest", "roms": [ { "name": "app-code", "image": "<my-org>/my-rom:latest", "at": "/app" }, { "name": "instance-config", "files": [ { "path": "config.json", "encoding": "text", "data": "{\"instance_id\": \"abc-123\"}" } ], "at": "/config" } ] }'

Limits

The platform enforces the following default limits on inline ROMs:

LimitDefault
ROMs per instance8 (inline and image-based combined)
Files per inline ROM16
Total size per inline ROM1 MB (across all files)
Max API request body8 MB

No per-file size limit exists. The total size limit applies to the combined size of all files in a single inline ROM. Since inline ROM data is base64-encoded in the JSON request body, the 8 MB request body limit is the practical upper bound on total data per API call.

ROM names and mount points

The guest derives each ROM's device node, /dev/ukp_rom_<name>, from the ROM's name. A ROM name has a maximum length of 63 characters and contains only these characters, the same as a plugin name:

  • Lowercase and uppercase letters (a–z, A–Z)
  • Digits (0–9)
  • Hyphen (-) and underscore (_)

When a --rom flag carries no name=, the unikraft CLI builds the name from at. It drops the leading /, turns every other / into -, and removes every character other than a letter, a digit, or -, so at=/rom/base gives the name rom-base.

A ROM can't share its name with a plugin of the same instance, and that includes a name the CLI derives from at. For example, a ROM at /sandbox gets the name sandbox, which collides with the sandbox plugin.

The at field takes an absolute path without . or .. components and without colons (:). The platform reserves /uk and every path below it for its own use in the guest, where it mounts plugins, so a ROM can't mount there. The same rule applies to the mount points of volumes. Two ROMs of the same instance also can't mount at the same path.

Error messageCause
ROM name must be between 1 and 63 characters longThe name is empty or longer than 63 characters.
ROM name may only contain the characters 'A'-'Z', 'a'-'z', '0'-'9', '-' and '_'The name holds another character, such as the . of a file extension.
The mountpoint must not be '/uk' or a path below itThe at path is /uk or a path below it.
Failed to configure roms: EINVAL or Failed to configure plugins: EINVALA ROM and a plugin share a name, or two ROMs share a mount point.
Failed to update roms: EINVAL or Failed to update plugins: EINVALA PATCH request that the platform applies right away breaks one of these two rules.

When the platform can't apply a PATCH request right away, for example on a running instance, it queues the change. It then checks these two rules only when it applies the change. A queued change that breaks one of them shows up with "status": "failed" in the updates array of the instance status.

Learn more

Use the --help option for detailed information on using Unikraft Cloud:

TerminalCode
unikraft --help

Or visit the CLI Reference. For more information on the features used in this use case and how to use them, check out the following documentation:

Last modified on