# Documentation > Complete documentation for Large Language Models --- ## Document: Introduction URL: /docs/introduction # Introduction Unikraft Cloud is a from-first-principles virtualization solution and cloud platform based on **MicroVMs** which provides best-in-class performance, security, and scalability. Running your app on Unikraft Cloud is like running a container but with hardware isolation, millisecond cold starts, and stateful scale-to-zero. Expect higher server density, cost-savings, I/O performance boosts, active vCPU pricing and _much more_. New: GPU support in enterprise preview - read more [here](/platform/instances#gpus). ## Quick start 1. [Create a free account](https://console.unikraft.cloud/signup). 1. Install the Unikraft CLI. ```bash title="1-liner (macOS & Linux)" curl --proto '=https' --tlsv1.2 -fsSL https://unikraft.com/cli/install.sh | sh ``` ```bash title="macOS" brew install unikraft/cli/unikraft ``` ```powershell title="Windows (PowerShell)" irm https://unikraft.com/cli/install.ps1 | iex ``` ```bash title="Debian/Ubuntu" # Update and install dependencies sudo apt update sudo apt install ca-certificates curl # Download and add the GPG key sudo install -d -m 0755 /etc/apt/keyrings sudo curl -fsSL \ -o /etc/apt/keyrings/unikraft-cli.gpg \ https://pkg.unikraft.com/debian/cli-apt/keys/cli-apt.gpg sudo tee /etc/apt/sources.list.d/unikraft-cli.sources < See [alternative installation instructions](https://github.com/unikraft-cloud/cli/?tab=readme-ov-file#installation) for other platforms. 1. Authenticate with your account. ```bash unikraft login ``` 1. Deploy your first scale-to-zero, serverless app with Unikraft Cloud. ```bash unikraft run \ --metro fra \ --publish 443:8080/http+tls \ --scale-to-zero policy=on,cooldown-time=1000 \ --image nginx:latest ``` --- ## Document: FAQ URL: /docs/faq # FAQ {/* vale off */} #### What is Unikraft? {/* vale on */} Unikraft is the company behind Unikraft Cloud, a next-generation compute platform that leverages microVM technology. It delivers millisecond cold boots and scale-to-zero autoscaling so you never pay for idle resources. {/* vale off */} #### What is a microVM? Definitionally, a microVM is any virtual machine launched with Firecracker, an open source Virtual Machine Monitor (VMM) from Amazon, originally done for AWS Lambda. {/* vale on */} The VM itself can still be chunky: as long as Firecracker launched it, it's considered a microVM. And any microVM *is* a VM, so it has the same strong, hardware-level isolation as any VM does. #### Can you do cloud-prem, or on-prem deployments? Yes. Unikraft Cloud also runs on [dedicated hosts, in your cloud account, or on your own servers](/platform/hosting-options). Please [get in touch](https://unikraft.com/contact) to discuss them. #### Aren't virtual machines heavyweight? They need not be! Unikraft microVMs are proof you can have strong, hardware-level isolation. This isolation is the workhorse of public cloud deployments. It combines with the lightweight characteristics of containers or processes, such as millisecond cold boot times. #### How much smaller are Unikraft images? The answer depends on the app, but with Unikraft most (up to ~90%) of an image's size is due to the app itself. For example, an NGINX Unikraft image is under 2MBs in size. #### Why can't you just use a container? When you deploy a container on the public cloud, it gets deployed on top of a virtual machine to have strong, hardware-level isolation. The container thus adds yet another layer of overhead between your app and the hardware. At Unikraft, `Dockerfile`s create the filesystem at build time. Then, a lean microVM provides the best efficiency at deploy time. #### Do Unikraft microVMs come with security benefits? Yes, especially stemming from the fact that they have a minimal Trusted Computing Base (TCB), and everything is off by default (services, ports, etc). #### Can a service and a database run with Unikraft Cloud? Definitely. Every instance on Unikraft Cloud has a private IP and DNS name. You can plug instances together. Follow this [guide](/platform/services) for instructions. #### With an access token and an app, what should you do? You'll need a `Dockerfile` and `Kraftfile`. See any of the apps/langs guides [here](/guides/bun) to see examples. {/* vale off */} #### What is `kraft`? {/* vale on */} `kraft` is the open source CLI tool of the Unikraft project. Use it to build Unikraft [unikernels](https://unikraft.org). Its `kraft cloud` command is the deprecated way to deploy to Unikraft Cloud. Use the [open source `unikraft` CLI tool](https://github.com/unikraft-cloud/cli) instead. {/* vale off */} #### What is `Kraftfile`? {/* vale on */} A `Kraftfile` is used by the CLI toolchain to understand how to build and deploy your instance. Typically you can use the default `Kraftfile` found in each [Unikraft Cloud example](https://github.com/unikraft-cloud/examples). Below is a sample `Kraftfile` with a brief explanation: ```yaml title="Kraftfile" spec: v0.7 runtime: python:3.12 targets: - kraftcloud/x86_64 rootfs: source: ./Dockerfile format: erofs cmd: ["/usr/bin/python3", "/src/server.py"] ``` The `runtime` specifies one of the Unikraft Cloud runtimes (microVMs) built to run different languages and apps. Here it specifies a Python runtime. The `targets` parameter specifies the platform and architecture pairs to build for. Here it targets `kraftcloud` (Unikraft Cloud) on `x86_64`, which is the architecture the hosted platform runs on. Without `targets`, a `runtime` builds for `x86_64` only, so list every architecture you need, for example `kraftcloud/arm64` alongside `kraftcloud/x86_64`. See [architectures](/platform/images#architectures) for more detail. The `rootfs` parameter tells the CLI to use a `Dockerfile` in the same directory to build the root filesystem as an [EROFS image](/tutorials/rootfs-formats). The `cmd` parameter tells the platform which command to run when deployed. #### How's millisecond scale-to-zero achieved? Magic 🪄 Kidding, it's the combination of using Unikraft microVMs to run workloads and a custom node ingress controller. This controller can be reactive in milliseconds and scale to thousands of instances. Other optimizations to the underlying host also help. When traffic dies, your app will stop, consume no resources, and get charged $0. When the next request from your user comes, the instance will wake in milliseconds and reply. Your user will be none the wiser. #### Do you have cold boots on Unikraft Cloud? By definition, meaning starting an instance up from zero, yes, cold boots exist. People often mean a *slow* cold boot: seconds or minutes. On Unikraft Cloud, cold boot times happen in milliseconds. #### How do you achieve millisecond autoscale? By leveraging fast cold boot times, and by coupling that with a reactive and scalable controller and proxy infrastructure. Autoscale like you've never seen it before. #### Millisecond snapshots? Yep. Instances on Unikraft Cloud are lean. Your app determines size, so snapshots are quick. Stateful scale-to-zero in milliseconds anyone? #### What do you need to get started? [Sign up](https://console.unikraft.cloud/signup) to get an access token. If you're interested in an enterprise solution please [write to us](https://unikraft.com/contact). #### What about usability? Unikraft puts great care and effort into providing seamless integration with major tools and frameworks like Docker, Kubernetes, Terraform, and Prometheus/Grafana. #### Why Unikraft and what's wrong with current cloud offerings? Current cloud stacks and offerings are over-bloated and over-priced. With Unikraft you can be sure that the resources you're consuming and paying for are going to your app, and your app only. When your app is idle, so is your bill. For Unikraft Cloud Enterprise, imagine running 1000s of instances with a couple of servers. #### Does Unikraft provide cost savings? Absolutely! Unikraft can provide cost savings in many ways: 1. Scale-to-Zero: don't ever pay for idle again; 2. Autoscale: don't ever pay for warm instances to cope with peaks; 3. Fewer instances: higher, more efficient I/O means fewer instances for similar workloads; and, 4. Server density: 1000s of instances on a single server mean fewer servers, and higher savings. #### What's the relationship between the Unikraft open source project and Unikraft Cloud? Unikraft OSS allows you to build and run unikernels locally via `kraft run` (and even hack the Unikraft OS itself if you like tinkering!). When you're ready to deploy to cloud, use the `unikraft` CLI. #### Are you a replacement for Docker? Definitely not! Docker is great for dev environments and for building images. In fact, Unikraft relies on `Dockerfile` to specify the filesystem of images on Unikraft Cloud. Having said that, when it's time to deploy, Unikraft places the resulting filesystem on a lean microVM. It's ready to run with hardware-level isolation and extreme efficiency. #### Are you a competing technology to WASM? No. You can read more about [how Unikraft and WASM complement each other in our dedicated blog post on the matter](https://unikraft.com/blog/unikernels-and-wasm/). In short, WASM provides language-level isolation, whereas Unikraft provides hardware-level. When you deploy WASM to the public cloud, there will be a VM underneath for isolation. Maybe even a container runtime. The most efficient way to deploy WASM workloads on the public cloud is on Unikraft Cloud. The VM (the unikernel) itself has only the minimal code needed to run the WASM runtime. #### Can you deploy within hyperscaler infra and connect to their services with Unikraft Cloud? Yes. Unikraft supports [metros](/platform/metros) (regions) within existing hyperscaler infra, and also connectivity to their services. For example, you could run an API server on Unikraft Cloud and connect to S3 as a storage back end. Contact information is available for more details. #### Does Unikraft Cloud work on Mac/Windows/Linux? Yes. The CLI runs on macOS, Linux, and Windows. {/* vale off */} #### What CPU architectures can you deploy to? {/* vale on */} Both `x86_64` and `arm64`, and the hosted platform runs on `x86_64`. The architecture of the guest image must match the host architecture, so name the architectures you deploy to in the `targets` key of your `Kraftfile` or through the `--arch` flag. A `runtime` with no `targets` builds for `x86_64` only, and a single `unikraft build` invocation produces an image for every architecture you ask for. See [architectures](/platform/images#architectures) for more detail. {/* vale off */} :::caution[**Limited Access**] `arm64` hosts are available as part of enterprise plans. To deploy to them, reach out to the [Unikraft Cloud Discord](https://unikraft.com/discord) or send an email to [support@unikraft.com](mailto:support@unikraft.com). ::: {/* vale on */} #### How can you track instances on Unikraft Cloud? Unikraft provides full `stdout` and `stderr` log support for all users. Paying customers also get access to Prometheus metrics. {/* vale off */} #### What is a unikernel? {/* vale on */} A unikernel is a specialized virtual machine. Each target app gets a distro and kernel containing only the code it needs. Everything builds at compile time, before deployment. If an app doesn't need a line of code to run, it doesn't get deployed. This packages as any other virtual machine, with strong, hardware-level isolation. Unikraft packages unikernels as OCI images. #### Does Unikraft Cloud only support unikernels? No, Unikraft Cloud also supports minimalistic, Linux-based microVMs. #### Are there other platforms supporting unikernels? Other unikernel projects exist, but most are research efforts, unmaintained, or limited to a single app or language and aren't POSIX (Portable Operating System Interface) compliant. #### Do you have a Kubernetes integration? [Yes!](/integrations/kubernetes) #### Do you have a Terraform integration? [Yes!](https://github.com/unikraft-cloud/terraform-provider-ukc) #### When should you use the CLI stable and/or staging? You should use stable versions when initially trying out the Unikraft Cloud Platform and its features. Any time you encounter a problem, it's good to first try out the latest staging version before forwarding the problem as it might have been already fixed. There's also a chance that staging contains changes available only on non-stable nodes. #### What keywords does the CLI ignore from a `Dockerfile`? This is the current list of ignored keywords: `EXPOSE`, `HEALTHCHECK`, `ONBUILD`, `SHELL`, `STOPSIGNAL`, `VOLUME`. The `USER` keyword is relevant for running subsequent `RUN` instructions in the Dockerfile, but not for the `ENTRYPOINT` and `CMD` commands. At runtime, Unikraft Cloud instances run as `root` user, unless explicitly switching to another user. #### How does the image size and memory relate to boot times? When using CPIO, there is a [strong correlation](/tutorials/rootfs-formats) between the image size and the boot time. This is because it needs to load the whole image into memory first. When using EROFS, there is no such correlation, as the operating system loads pages directly from disk, on demand. At the same time, there is a slight correlation between big memory allocations and higher boot times, though not significant. This is irrespective of the file system used. #### What's the key difference between cold boots and warm boots? End-result wise, cold and warm boots are no different, the instance still works. The server handles the request in both cases if the app is stateless. The main difference appears in [stateful](/features/snapshots) apps and when considering latency. A cold boot equates to starting the instance every time and then stopping it if no traffic hit it for a while. A warm boot resembles the suspend and resume operations in virtual machines. The platform saves the system memory to disk and loads it when a request comes. Warm boots offer clear advantages over cold boots, having lower boot times, no setup time, and are better in general, with the trade-off of using space on the disk. Thus, for most use cases, using [stateful scale-to-zero](/features/scale-to-zero#stateful-scale-to-zero) is the better alternative. Cases exist where short lived VMs are small enough to have a small boot time, but they use a lot of memory. In this case, it might be better to use cold boots to use disk efficiently. You should first experiment with this or discuss it with a Unikraft Engineer to verify it. #### What can you push as an image? An image typically bundles a kernel and a rootfs together as different layers of an OCI package. To keep things light, certain optimizations are in place to not repush existing kernels and to reuse them from your account. If these aren't automatically detected, the CLI pushes the kernel again. At the same time, things like [ROMs](/features/roms) use the same packaging systems, but push images with a missing kernel and only a rootfs. These have specific metadata that identifies them as ROMs. An image can also ship a rootfs alone and boot on the kernel installed on the node instead of one of its own, which the [platform kernel](/platform/images#platform-kernel) section covers. Finally, kernels can also run without a rootfs. They don't do anything, but support exists to run them. #### How to tell if an app can run in the Unikraft kernel or TinyX? No exact suite can determine if your app can run on one or another. TinyX is a slimmed down Linux Kernel, which means that most cloud-compatible apps work out of the box as long as it has all files available. Some virtual devices and virtual file system entries are missing, but you can contact a Unikraft staff member to look into adding them. If your app successfully runs on TinyX and Unikraft Cloud, you have the option to also try running it on the Unikraft kernel. This kernel removes even more unused features so the chance that the app isn't compatible increases, but the performance and boot times are better. For example, if your app uses extensive process cloning, it has a low chance to work on the Unikraft kernel, as it supports running a single process at a time. Other examples of unsupported formats are 32bit executables and non-PIE (Position Independent Executable) executables, as the Unikraft kernel can't load them in memory at the moment. Other small differences exist, which you should report to the Unikraft Team for fixing, as the Unikraft kernel tries to follow Linux and the POSIX format. #### How does the [rootfs format](/tutorials/rootfs-formats) and size equate to the memory used? When using the CPIO file system, the memory allocated to the instance needs to be at least twice as big as the image size. This is because the system must load and unpack the CPIO rootfs format in memory before using it. This is a bare minimum and to this you need to add also the memory your app needs to run. When using EROFS, there is no direct correlation between the image size and the memory needed. Ideally, the memory should be at least the image size to make sure the system can load all files in memory before using. The system loads files on demand and maps them directly from disk, ensuring lower memory usage and overhead. If you have files that you know are never used, it's better to remove them from the image to slim it down. Thus, in theory, EROFS uses only the allocated pages, but it's also highly dependent on the workload. An instance with a 3GB image might use 128MB of RAM at one point, whilst an instance with a 300MB image might use 1GB of RAM at some point. --- ## Document: Webhooks URL: /docs/use-cases/webhooks # Webhooks {/* vale off */} Send events to a microVM that wakes and responds in milliseconds, then shuts down until the next event. No warm instance waiting for a delivery that may not come; the event itself is what starts the handler. Between deliveries the handler isn't running. Endpoints that fire a few times a day stop being a standing bill. When a provider replays a backlog, the service group [adds instances](/features/autoscale) in milliseconds and [balances](/features/load-balancing) across them. Stripe, GitHub, your own services --- ## Under the hood The platform features this use case is built on. The VM only runs while it is actively serving a request. The service group can add instances in milliseconds to deal with event storms. Distributes incoming events across the instances in the service group. Sign up to Unikraft Cloud and follow the guide to deploy your first webhook {/* vale on */} --- ## Document: Serverless Functions URL: /docs/use-cases/serverless-functions # Serverless Functions {/* vale off */} Deploy hardware-isolated serverless functions that boot and respond in milliseconds, and cost nothing between calls. Most serverless platforms share a kernel or a language runtime between tenants. Every function here is a hardware-isolated microVM with its own kernel, safe enough to run code you didn't write. Bring your own runtime, however heavy. Snapshot it warm into a [template](/platform/instances#instance-templates): no warm pool to keep paying for. Templates alone would give you identical runtimes. The function's own code arrives as a [ROM](/features/roms) mounted at boot, so one warm template serves many different functions, and shipping a new one is a few KB, not an image build. Node, Python, Go, JVM (your Dockerfile) Runtime booted and warm, frozen mid-flight Watch the recording to see one language runtime snapshotted once, then different handlers dropped in as ROMs, each waking to serve a request in 6 ms. --- ## Under the hood The platform features this use case is built on. Snapshot bulky runtimes so they start in milliseconds, and mount the function code as a ROM so changing it doesn't require rebuilding the image. Let the platform absorb traffic spikes across instances without a separate load balancer. Sign up to Unikraft Cloud and follow the guide to deploy your first serverless function now {/* vale on */} --- ## Document: Serverless Databases URL: /docs/use-cases/serverless-databases # Serverless Databases {/* vale off */} Run whichever database you already use, and start, clone and restore it in milliseconds. [Branch](/features/branching) a running database (memory and [volume](/platform/volumes) state included) and run the risky migration against the copy. [Checkpoints](/features/checkpoints) keep a history of full instance state, so rolling back is a restore rather than a rebuild. State is [snapshotted](/features/snapshots) when the database goes quiet and restored on the next query. Per-tenant databases stop being expensive. The only thing that can reach the database Whichever database engine you run: memory and volume state, live {/* The checkpoints belong to the primary, so the band sits under it while the diagram is a column. Once the diagram is a row it wraps onto a line of its own below the whole flow, which is why it is ordered last and given a full line to sit on. */} A full clone, memory included; run the risky change here Watch the recording to see a running Postgres branched in 40 ms, written to without touching the original, and either copy woken in about 16 ms. --- ## Under the hood The platform features this use case is built on. Clones a running database instance, including memory and volume state, with at most a few milliseconds of downtime on the original. Capture the full state of an instance and keep a history, so you can restore from a known good snapshot. Snapshots memory and volume state when the database is idle and restores it in milliseconds on the next request. Pay only for what you use. The database can be hidden from the public internet entirely, reachable only by your web server. Sign up to Unikraft Cloud and follow the guide to run your first serverless database {/* vale on */} --- ## Document: Sandboxes URL: /docs/use-cases/sandboxes # Sandboxes {/* vale off */} Securely run untrusted sandboxes in milliseconds; [scale them to zero](/features/scale-to-zero#stateful-scale-to-zero), [branch](/features/branching) or [checkpoint](/features/checkpoints) them as needed. Snapshot a fully initialised environment (runtime, deps, warm caches) into a [template](/platform/instances#instance-templates) and boot each sandbox from it. Idle ones cost nothing. [Scale to zero](/features/scale-to-zero#stateful-scale-to-zero) and restore statefully in ms. Every sandbox is a hardware-isolated microVM with its own kernel, not a container sharing the host's. Its traffic leaves through a [shield](/features/network-shield) microVM that filters egress and holds your credentials, so untrusted code can use an API without ever seeing the key. One hardware-isolated microVM per task: [branch](/features/branching) a live sandbox to launch sub-agents from its exact state, or [checkpoint](/features/checkpoints) it and rewind when a path goes wrong. A (scale to zero) microVM every packet must pass through: filters traffic, injects secrets Third-party APIs, package registries Watch the video to see a sandbox boot in under 10 ms, scale to zero while idle, and resume with its state intact. :::tip[Create a sandbox from your own code] The [JavaScript SDK](/sdks/js/sandboxes) wraps the [sandbox plugin](/features/plugins#sandbox-plugin) in a `Sandbox` class, so a microVM is a few lines: ```ts import { Sandbox } from "@unikraft/cloud"; // `await using` deletes the sandbox when this scope ends, however it ends. await using sandbox = await Sandbox.create(); const { stdout } = await sandbox.exec("echo hello"); await sandbox.writeFile("/work/input.txt", "..."); ``` Commands, standard input, signals and the filesystem are all covered. The [Python](/sdks/python) and [Go](/sdks/go) SDKs drive the instances themselves. ::: --- ## Under the hood The platform features this use case is built on. A separate microVM that all network traffic is routed through. It can filter traffic and inject secrets. An authenticated channel over which command execution and filesystem access services are provided. Clones a running sandbox, memory and volume state included, in milliseconds, so a sub-agent can start from the parent's exact state instead of setting itself up again. Capture the full state of a sandbox and keep a history, so you can rewind to the last good point when a run goes wrong. Optionally terminates long-running sandboxes so a stuck or hostile workload can't run indefinitely. Give large, heavy sandbox environments millisecond startup, and cost nothing while idle. Sign up to Unikraft Cloud, then follow the guide on creating your first sandbox {/* vale on */} --- ## Document: Remote IDEs URL: /docs/use-cases/remote-ides # Remote IDEs {/* vale off */} Run full developer environments that [scale to zero](/features/scale-to-zero#stateful-scale-to-zero) when idle and start back up exactly as you left them, in milliseconds. Memory and disk are [snapshotted](/features/snapshots) when the developer stops typing, and restored on the next keystroke, processes still running. The workspace state is held in persistent [volumes](/platform/volumes), so [scale-to-zero](/features/scale-to-zero#stateful-scale-to-zero) cycles and restarts don't lose work. One environment per developer, per branch or per review, with idle ones costing nothing. Language toolchains, editor server, extensions Environment booted and warm, snapshotted once --- ## Under the hood The platform features this use case is built on. Snapshot memory and volume state after the heavy initialisation stage so the environment starts in milliseconds. Persist changes between scale-to-zero cycles, so the workspace outlives any single instance. Restrict what the environment can reach and inject the secrets it needs. Sign up to Unikraft Cloud and follow the guide to deploy your first remote IDE {/* vale on */} --- ## Document: Remote Desktops URL: /docs/use-cases/remote-desktops # Remote Desktops {/* vale off */} Provide a full cloud desktop environment that starts in milliseconds. A whole desktop session (window manager, apps, browser) [snapshotted](/features/snapshots) after login and resumed on connect. [Branch](/features/branching) a running desktop to hand someone an identical, already-configured session. One desktop per user or per contractor, isolated as a VM, costing nothing while nobody is connected. OS, desktop environment, installed apps Desktop logged in and warm, snapshotted once Watch the video to see a VNC desktop start with no perceptible lag and scale back to zero the moment you disconnect. --- ## Under the hood The platform features this use case is built on. Snapshot the desktop after its long initialisation so sessions resume in milliseconds. Filter network traffic to and from the remote desktop, and inject the secrets it needs Caps how long a session can run, useful when an app hangs and holds the instance open. Sign up to Unikraft Cloud and follow the guide to run your first remote desktop {/* vale on */} --- ## Document: What people build on Unikraft URL: /docs/use-cases/overview # What people build on Unikraft {/* vale off */} A sample of some of the common use cases Unikraft is used for in production. Follow the links below to learn more. Securely run untrusted sandboxes in milliseconds; scale them to zero, branch or checkpoint them as needed. Run MCP servers as microVMs that wake in milliseconds when an agent calls them, and cost nothing between calls. Give every session, scrape or screenshot its own disposable browser microVM (headless or with a full display) that starts in milliseconds. Give every build or test job its own microVM. Start your full toolchain from a snapshot in milliseconds, and inject that job's code at boot time. Run full developer environments that scale to zero when idle and start back up exactly as you left them, in milliseconds. Deploy hardware-isolated serverless functions that boot and respond in milliseconds, and cost nothing between calls. Deploy your API gateway on ultra low-latency microVMs that scale up when required and cost nothing when idle. Send events to a microVM that wakes and responds in milliseconds, then shuts down until the next event. Run whichever database you already use, and start, clone and restore it in milliseconds. Start heavy gaming workloads in milliseconds, and scale them to zero when the session is empty. Provide a full cloud desktop environment that starts in milliseconds. {/* vale on */} --- ## Document: MCP Servers URL: /docs/use-cases/mcp-servers # MCP Servers {/* vale off */} Run MCP servers as microVMs that wake in milliseconds when an agent calls them, and cost nothing between calls. Tools with expensive initialisation (indexes, model loads, browsers) are [snapshotted](/features/snapshots) warm and resumed on the call. Agent traffic is bursty and mostly idle. A server that isn't being called isn't running, and isn't billed. One isolated server per agent, per tool or per tenant, rather than one shared process handling everyone. Calls a tool when the agent needs it Filters traffic, injects the secrets; servers never hold them --- ## Under the hood The platform features this use case is built on. Bulky tools start and resume in milliseconds, and consume nothing between tool calls. Injects secrets so the MCP server itself never has access to them, and filters its outbound traffic. Sign up to Unikraft Cloud and follow the guide to run your first MCP server {/* vale on */} --- ## Document: Headless & Headful Browsers URL: /docs/use-cases/headless-browsers # Headless & Headful Browsers {/* vale off */} Give every session, scrape or screenshot its own disposable browser microVM (headless or with a full display) that starts in milliseconds. Bring any browser you like; the platform is browser-agnostic. Its long startup happens once: snapshot the initialised browser into a [template](/platform/instances#instance-templates) and boot every session from there. Templates alone would give you identical browsers. A [ROM](/features/roms) mounted at boot carries that session's own profile, cookies, proxy or script, so one warm template serves many distinct sessions. Run browsers with a real display and [GPU](/platform/instances#gpus)-backed rendering (for video, WebGL and canvas-heavy pages that a headless, software-rendered browser can't handle). Any browser (Chromium, Firefox, WebKit) plus your automation code Browser fully initialized, then snapshotted Watch the video to see a browser deployed with a full display, driven over VNC and screenshotted, asleep between each interaction. --- ## Under the hood The platform features this use case is built on. Snapshot the browser after its long initialisation and start instances from that state in milliseconds. Read-only blobs mounted on a VM at start. Paired with templates they customise each instance (profile, cookies, proxy, script) without rebuilding the image. Headful browsers can render on a GPU, so video playback, WebGL and canvas-heavy pages behave as they do on a real machine. Clones a running browser (memory, cookies and open tabs included), so several paths can be explored from one logged-in state. Optionally filter the browser's network traffic, and inject secrets and API keys so the browser does not need to hold them. Caps how long a session can run, useful when a page hangs and would otherwise stall the instance. Sign up to Unikraft Cloud then follow the guide to automate your first browser {/* vale on */} --- ## Document: Game Servers URL: /docs/use-cases/game-servers # Game Servers {/* vale off */} Start heavy gaming workloads in milliseconds, and [scale them to zero](/features/scale-to-zero#stateful-scale-to-zero) when the session is empty. Shut the server down when the last player leaves and bring it back in milliseconds when someone joins. Per-world config and mods are mounted onto the instance rather than baked into a new image each time. One instance per world or match, isolated as a VM; the platform is built for that scale. Game binary, assets, mod loader World loaded and warm, snapshotted once Watch the video to see ten Minecraft worlds kept ready at once, with only the world in play actually running. --- ## Under the hood The platform features this use case is built on. Snapshot memory and volume state after the heavy load stage so a world resumes in milliseconds. Persist world state between scale-to-zero cycles. Restrict what the server can reach and inject the secrets it needs. Sign up to Unikraft Cloud and follow the guide to run your first game server {/* vale on */} --- ## Document: Build and Test Environments URL: /docs/use-cases/build-test-environments # Build and Test Environments {/* vale off */} Give every build or test job its own microVM. Start your full toolchain from a [snapshot](/features/snapshots) in milliseconds, and inject that job's code at boot time. Pack the whole environment (compilers, deps, warm caches) into a [template](/platform/instances#instance-templates): a snapshot with all that state already initialized. Size stops mattering. Run thousands of instances with minimal memory footprints on a platform built for scale, all on the same server. The code under test arrives as a [ROM](/features/roms), a small read-only image mounted at start. Same environment, different payload per job, no rebuild. Your Dockerfile: toolchain, deps, caches Heavy initialisation done once, booted and frozen Watch the recording to see a build environment snapshotted into a template, then two different builds injected as ROMs with 6 ms cold starts from both. --- ## Under the hood The platform features this use case is built on. Pre-initialised snapshots let you do the heavy initialisation once, save the state, and start new instances from it, cutting startup to milliseconds. Read-only blobs mounted on a VM. Combined with templates, they carry the per-job code onto each instance created from the template. The instance shuts down when no work is running and comes back in milliseconds on the next request. You pay only for what you use. Sign up to Unikraft Cloud and follow the guide to run your first build environment {/* vale on */} --- ## Document: API Gateways URL: /docs/use-cases/api-gateways # API Gateways {/* vale off */} Deploy your API gateway on ultra low-latency microVMs that scale up when required and cost nothing when idle. [Autoscale](/features/autoscale) adds gateway instances as traffic climbs and removes them as it subsides, [down to zero](/features/scale-to-zero) when nothing is arriving. Each gateway is a hardware-isolated microVM with its own kernel, not a process sharing a host runtime. Millisecond cold starts, minimal memory footprints and transparent [load balancing](/features/load-balancing) within a service group results in ultra-low latency. Clients, partners, your own apps --- ## Under the hood The platform features this use case is built on. Starts as soon as multiple instances are attached to a service group. There's no separate load balancing instance to run. Scales up during traffic spikes in milliseconds and back down once they subside. An idle gateway isn't running, so you aren't paying for it between requests. Sign up to Unikraft Cloud and follow the guide to deploy your first API gateway {/* vale on */} --- ## Document: Scale To Zero Triggers URL: /docs/tutorials/scale-to-zero-triggers # Scale To Zero Triggers import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" Unikraft Cloud supports scaling instances to zero based on different triggers. This allows you to optimize resource usage and costs by automatically setting instances to standby when they're not needed. They will automatically resume when required with minimal delay. The platform can scale down instances automatically, or you can scale them down manually. You can read more about Scale To Zero [here](/features/scale-to-zero). ## Automatic triggers Automatic triggers are the most common and easiest way to use scale-to-zero with your instances on Unikraft Cloud. By default, instances scale down automatically when there is no traffic coming to them for a specified period. You can also configure scale-to-zero to consider idle active connections when determining whether to scale down an instance. ### No traffic When you enable this trigger, Unikraft Cloud will track incoming connections to your instance. If there is no traffic for the configured duration, the instance scales down to zero. You can use the CLI to enable this trigger: ```bash unikraft run \ --scale-to-zero policy=on,cooldown-time=5000 \ ... ``` ### Idle connections When enabling this trigger, the platform will consider all active connections to your instance, so all traffic going to them. The instance will scale down if you make no new network connections and all existing connections have been idle for the configured duration. The `idle` policy requires stateful scale-to-zero, so set `stateful=true` together with it. You can enable this trigger using the CLI as follows: ```bash unikraft run \ --scale-to-zero policy=idle,stateful=true,cooldown-time=5000 \ ... ``` :::tip If your app uses [keep-alive](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Keep-Alive) messages over this active connection, your instance won't scale down. You will have to either disable keep-alive or configure them with an interval high enough to allow scaling down. For example, with a 5 seconds cooldown, you can set keep-alive to once per minute. ::: ## Manual triggers For specific use cases, you might want to manually scale your instances to zero, or to manually disable scale-to-zero. Unikraft Cloud supports two ways to manually trigger scale-to-zero. ### API You can use the Unikraft Cloud API to manually scale down an instance. You will have to use the `/instances/suspend` endpoint to scale down an instance. This happens instantly, regardless of the configured automatic triggers. First, set the required environment variables: ```bash # Set Unikraft Cloud access token export UKC_TOKEN=token # Set metro to Frankfurt, DE export UKC_METRO=fra ``` Then invoke the following `curl` command to scale down an instance named `my-instance`: ```bash title="PUT /instances/suspend" curl -X PUT https://api.fra.unikraft.cloud/v1/instances/suspend \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Content-Type: application/json" \ -d '{ "name": "my-instance", "drain_timeout_ms": 10000 }' ``` You can do the same using the `unikraft` CLI: ```bash unikraft instance suspend my-instance --drain-timeout 10s ``` The drain timeout allows you to specify a grace period for existing connections to finish before the instance goes to standby. If set to `0`, no grace period exists and the instance goes to standby immediately, dropping all existing connections. If set to `-1` or unset, the platform will wait indefinitely for existing connections to finish before suspending the instance. ### Device file You can also manually scale down an instance by writing '+'/'-' to a special device file available inside the instance. Every instance has this file at `/uk/libukp/scale_to_zero_disable`. By writing '+' to this file, you disable automatic scale-to-zero for the instance until the number of '-' written matches the number of '+' written. For example, to disable automatic scale-to-zero, run the following command inside the instance: ```bash echo '+' > /uk/libukp/scale_to_zero_disable ``` :::tip If your app starts listening for incoming connections before being ready to serve them, you can use this mechanism to disable scale-to-zero during startup. Once your app is ready to serve requests, you can write '-' to the file to re-enable automatic scale-to-zero. ::: ## Learn more * The [Scale To Zero](/features/scale-to-zero) feature page. * The [CLI reference](/docs/cli/unikraft). --- ## Document: Rootfses, Volumes and ROMs URL: /docs/tutorials/rootfs-volumes-roms # Rootfses, Volumes and ROMs import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" Rootfses, volumes and ROMs (Read-Only Memory) are storage types that share many similarities, but have different use cases and results. Below, this tutorial discusses each one of them and how to use them, and then does a quick comparison between the three. ## Storage types ### Rootfses The rootfs, also known as the initrd, is the main component in any instance running on Unikraft Cloud, and any UNIX-based operating system. It's used for configuring its default behaviour and contains all files needed for it to boot. On Unikraft Cloud it's used to also start a default app (without any specific configuration). When booting, the system loads rootfs pages in memory, which incurs a small access time characteristic to RAM read/write operations. At the same time, because the rootfs resides in volatile memory, changes aren't persistent across reboots. Because of this it's best to put only essential information in there to keep the size down (1-2GB maximum), to save on boot times and on used RAM. For persistent storage, consider using [volumes](/platform/volumes) instead. A rootfs can be of two types: CPIO or EROFS. Each type has its own advantages and disadvantages, but all-around EROFS is better suited for Unikraft Cloud. The [rootfs formats](/tutorials/rootfs-formats) documentation page details both types. You can create a rootfs with the CLI for both types, for example: **CPIO** ```bash # make sure to specify the cpio format in Kraftfile # ... # rootfs: # source: ./Dockerfile # format: cpio # ... unikraft build . --output /node-playwright-chromium:latest unikraft run --metro fra \ -p 443:8080/http+tls \ -m 2700M \ --image /node-playwright-chromium:latest ``` **EROFS** ```bash # make sure to specify the erofs format in Kraftfile # ... # rootfs: # source: ./Dockerfile # format: erofs # ... unikraft build . --output /node-playwright-chromium:latest unikraft run --metro fra \ -p 443:8080/http+tls \ -m 900M \ --image /node-playwright-chromium:latest ``` :::note `unikraft build` keeps the file owners of a `Dockerfile`, image, or tarball source, the way Docker does, so `COPY --chown` means what it says. Files from a directory source belong to root. EROFS is a read-only file system, so you can't change owners in place. To change them at run time, have the entrypoint copy those files to a writable path (for example, a volume) and run `chown` there. ::: ### Volumes [Volumes](/platform/volumes) are a persistent, large storage component on Unikraft Cloud. You attach volumes to stopped instances or before you create them through the [API](/api/platform/v1/volumes#create-volume). They're automatically mounted in the instance at the specified path either `RW` (Read-Write) or `RO` (Read-Only). They're backed up on SSD (Solid State Drive) disks and offer redundancy without compromising on access latency. As such you can use them for persistent data, secrets, storage, logs, and more, and you can share them between instances. To create and use a volume, you can use the CLI volume commands. For example, you need to first create a volume: ```bash unikraft volumes create --metro fra \ --name my-volume \ --size 100 ``` Then you can optionally populate it with local data: ```bash unikraft volume import \ my-volume \ --source my-data/ ``` Finally, when you deploy an instance you can attach the volume to it: ```bash unikraft run --metro fra \ -m 512M \ -p 443:8080/http+tls \ -v my-volume:/mnt \ --image /my-app:latest ``` ### ROMs [ROMs](/features/roms) are an extension of the rootfs concept and a good way to customize generic rootfses for many instances. They're attached on stopped instances and Unikraft Cloud can mount them automatically in the instance. They're read-only and there is a limit in size, and as such should contain only custom rootfs code/data. They're intended to be small, fast, and flexible as opposed to volumes. For example, you can use ROMs in the context of a web server that offers different index pages based on the ROM attached for localization. By using ROMs, you can have a single slim image to which you can attach different ROMs depending on the language you want to serve. Finally, it's considered good practice to keep ROMs small and focused, and the instance shouldn't depend on them to boot correctly. You could package ROMs as an OCI image and push them to the registry, or you could create them directly from a local directory. They show up as any other image in the registry, but don't have a kernel. Because a ROM has no `runtime` to take architectures from, its `Kraftfile` must name them under `targets`, or you pass `--arch` on the command line. To package them you can use the CLI: ```bash # make sure to specify the rom path and the build targets in the Kraftfile # ... # targets: # - kraftcloud/x86_64 # roms: # - ./fs # ... unikraft build . --output /function-hello:latest ``` You can then specify ROMs when creating an instance: ```bash title="unikraft" unikraft run --metro fra \ --rom name=function-hello,image=/function-hello:latest,at=/rom \ ... ``` ```bash title="API" curl -X POST \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Content-Type: application/json" \ https://api.fra.unikraft.cloud/v1/instances \ -d '[{ "roms": [ { "name": "function-hello", "image": "/function-hello:latest", "at": "/rom" } ], ... }]' ``` ## Comparison | Type / Metric | Rootfses | Volumes | ROMs | |---------------|----------|---------|------| | I/O Speed | Fast | Decent | Fast | | I/O Latency | Small | Noticeable | Small | | Storage Medium | RAM | SSD/HDD | RAM | | Intended Size | Medium | (up to) Large | Small | | File System | CPIO/EROFS | `ext4`/`virtio-fs` | Any | | Mounting | Automatic | Automatic | Automatic/Manual | | Persistence | No | Yes | Yes (as read-only image) | | Mandatory | Yes | No | No | ## Conclusion As expected, each storage type has its own advantages and disadvantages. Rootfses are mandatory and fast, but you should keep them small and only contain essential information. Volumes are persistent and large, but have noticeable access latency. ROMs are a flexible way to customize rootfses, but you should also keep them small and not be essential for booting. ## Learn more * [Volumes](/platform/volumes) and how to use them. * [Rootfs formats](/tutorials/rootfs-formats) and how to create them. * [ROMs](/features/roms) and how to use them. * The [CLI reference](/docs/cli/unikraft). * The `unikraft build` [command reference](/docs/cli/unikraft/build) for building images. --- ## Document: Rootfs Formats URL: /docs/tutorials/rootfs-formats # Rootfs Formats Every virtual machine consists of at least two components: a kernel, and a root file system. This tutorial focuses on the latter, the **rootfs** (also known as initrd). Currently, on the Unikraft Cloud platform, two types of file system archives are usable: CPIO (Copy In, Copy Out) and EROFS (Enhanced Read-Only File System). They're packaging formats used for loading the root file system in the instance. They consist mainly of metadata and file content without any advanced formatting to keep them as simple as possible. The kernel loads and mounts the data from them at boot time. ## Getting started The main difference between the rootfs formats is performance-wise. ### CPIO CPIO is a simple format which translates in the need to copy and unpack the entire archive into guest memory when starting the instance. This incurs a double memory usage during boot time, as the archive is first loaded into memory, then unpacked into a ramfs for the kernel to load. At runtime, the guest can reclaim the initial memory allocated for the archive, and will only use the unpacked ramfs. The boot times will still proportionally increase with the size of the CPIO archive. Another consequence of using a CPIO initrd is that [snapshots](/features/snapshots) must include them, since Unikraft Cloud can't track where they're stored in guest memory. ### EROFS EROFS wins on startup efficiency by not needing to unpack the archive at all into guest memory. This means that you don't need to accommodate double the memory usage during boot time, and boot times are faster as well. :::tip Another significant benefit of EROFS (although *not available yet* on public metros) is that Unikraft Cloud can enable `DAX` (Direct Access) on instances using EROFS rootfs. This means that the host side loads the initrd, and the guest gets a map (**not a copy**) of it into its memory. Thus, boot times aren't affected by the size of the rootfs at all. Memory usage is also drastically reduced at boot, and only increases as the guest accesses files. This also means that snapshots can exclude the original initrd. Writing to the rootfs is still possible, because Unikraft Cloud uses **overlayfs** to mount the ramfs. Any file changes are present in the snapshot. Finally, `DAX` enables sharing the same rootfs image between many instances, increasing scalability. ::: ## Packaging the rootfs The CLI supports packaging both rootfs formats out of the box. With it, you can package a rootfs from these sources: - a simple file - a directory - a `Dockerfile` - a remote OCI image If provided as a file, the CLI tool passes the file as-is, without any extra packaging. Otherwise, you need to package the rootfs into a supported format (either CPIO or EROFS). Already packaged archives with external tools (for example `mkfs.erofs`, CPIO, `bsdcpio`) are also usable. It's worth mentioning that after unpacking, there is no difference in the produced file contents between the formats. They may only differ metadata-wise, depending on the implementation. You may observe that a CPIO archive is typically smaller on disk than an EROFS one. The only effect this has is on the time it takes to push the image. :::info The default format for the CLI tool is EROFS, as it provides better performance and lower memory usage at boot time. ::: You can specify the rootfs format in the `Kraftfile`: ```yaml title="EROFS" # Kraftfile rootfs: source: ./Dockerfile format: erofs ``` ```yaml title="CPIO" # Kraftfile rootfs: source: ./Dockerfile format: cpio ``` Below are two attempts at creating and starting an instance from the same [image](https://github.com/unikraft-cloud/examples/tree/main/node-playwright-chromium), packaged using both formats. A full guide on using this image is available [here](/guides/node-playwright-chromium). ### EROFS deployment ```bash # make sure to specify the erofs format in Kraftfile # ... # rootfs: # source: ./Dockerfile # format: erofs # ... unikraft build . --output /node-playwright-chromium:latest unikraft run --metro fra \ -p 443:8080/http+tls \ -m 1000M \ --image /node-playwright-chromium:latest ``` ```ansi metro: fra name: node-playwright-chromium-4rrxr uuid: aeff2d28-d718-4068-9e24-4e2bb203022c state: starting image: /node-playwright-chromium resources:  memory: 1000MiB  vcpus: 1 service:  name: dry-fire-vhmh9z8u  uuid: 28762473-9e16-4921-bee5-e57c9e5dff34  domains:  - fqdn: dry-fire-vhmh9z8u.fra.unikraft.app networks: - uuid: dd2581ff-f7e8-4aac-b7e6-73b1d64c5cb7  private-ip: 10.0.3.133  mac: 12:b0:0a:00:00:19 timestamps:  created: just now ``` :::note `unikraft build` keeps the file owners of a `Dockerfile`, image, or tarball source, the way Docker does, so `COPY --chown` means what it says. Files from a directory source belong to root. EROFS is a read-only file system, so you can't change owners in place. To change them at run time, have the entrypoint copy those files to a writable path (for example, a volume) and run `chown` there. ::: ### CPIO deployment As stated, CPIO requires more memory to boot, so you need to give more than the 1000MiB used in the EROFS case. ```bash # make sure to specify the cpio format in Kraftfile # ... # rootfs: # source: ./Dockerfile # format: cpio # ... unikraft build . --output /node-playwright-chromium:latest unikraft run --metro fra \ -p 443:8080/http+tls \ -m 2700M \ --image /node-playwright-chromium:latest ``` ```ansi metro: fra name: node-playwright-chromium-b0nyn uuid: e4d00b20-4fc9-4cba-a29d-846fa733c358 state: starting image: /node-playwright-chromium resources:  memory: 2700MiB  vcpus: 1 service:  name: fragrant-dawn-v42h579x  uuid: e9424066-5c71-4142-94ee-44acc5ed3d09  domains:  - fqdn: fragrant-dawn-v42h579x.fra.unikraft.app networks: - uuid: 762a85e2-e6ad-435a-867e-4139a5fda69f  private-ip: 10.0.3.157  mac: 12:b0:0a:00:00:9d timestamps:  created: just now ``` ## Conclusion The packaged archives have a size of around 900MiB each. This also includes the kernel, which is around 36MiB. The EROFS-packaged instance requires at least 1000MiB to work, and the CPIO one requires 2700MiB. Simple arithmetic backs this. In the CPIO case, the kernel gets a copy of the archive in memory and needs to move it into ramfs. This extraction process requires double the memory of the archive size. This means the instance needs at least an extra (900-36)x2=1728MiB of memory compared to EROFS to boot. Finally, related to boot times, both cases have similar [warm boot](/features/snapshots) times, but the extra copies mentioned increase the cold boot time in the CPIO case. Bottom-line is, using EROFS is the go-to in every case where possible, as the upsides definitely outweigh the small downsides. ## Learn more * The [CLI reference](/docs/cli/unikraft). --- ## Document: Rootfs Compression URL: /docs/tutorials/rootfs-compression # Rootfs Compression Compressing the root file system represents using an algorithm like `gzip`, `zstd`, etc. to reduce the size of the rootfs on disk. This has the added benefit of reducing the time it takes to push the image to the Unikraft Cloud platform, as you transfer less data. It also means that the size on disk on the host side reduces, which can be beneficial when dealing with many images or large images. This size can drop depending on the contents of the rootfs and the compression algorithm used. For example, text files often compress by up to 90% and more, whilst already compressed files like images or videos don't compress well at all. ## Getting started The Unikraft Platform supports compressed EROFS rootfses, but it's highly recommended to use uncompressed ones if size allows. The kernel decompresses the data at runtime, which makes file access slower across the board. That's why the first attempt at using the platform should always be with uncompressed rootfses. Of the two [rootfs formats](/tutorials/rootfs-formats), only EROFS supports compression. Unikraft Cloud doesn't support a compressed CPIO rootfs. EROFS does per-block compression, which means that the system decompresses only the blocks that it reads. EROFS supports most compression algorithms, but the recommended ones are those that have fast decompression speeds like `lz4hc`. As presented in the [rootfs formats tutorial](/tutorials/rootfs-formats) and in the [FAQ](/faq#how-does-the-image-size-and-memory-relate-to-boot-times), the performance characteristics of the two formats differ. With a compressed EROFS rootfs, boot times change less, and memory usage stays low, because the system decompresses only the blocks that it reads. Compression has a downside that Direct Access (`DAX`) isn't possible when using compression, as the host side can't map a compressed image into guest memory. This caveat results in having to load the entire compressed rootfs into guest memory, increasing memory usage by at least the size of the compressed rootfs. Still, if many files aren't accessed in boot, the cold boot time might decrease compared to an uncompressed EROFS rootfs. ## Packaging a compressed rootfs The `unikraft` CLI doesn't support compressing rootfses. This choice is deliberate, as the performance downsides aren't worth it most of the time. The `unikraft` CLI supports deploying pre-compressed EROFS rootfses. As a study case, this tutorial uses this [`node` image](https://github.com/unikraft-cloud/examples/tree/main/node-playwright-chromium), which has a big rootfs size. A full guide on using this image is available [here](/guides/node-playwright-chromium). To package a compressed EROFS rootfs, you need to use the `mkfs.erofs` tool before packaging it with the CLI. To do this you will also have to first export the image built from the `Dockerfile` into a `tar` archive: ```bash docker build -o type=tar,dest=rootfs.tar -t node-playwright-chromium -f Dockerfile . ``` You can then use the resulting `tar` archive to create a compressed EROFS rootfs using the following command. Note that you can change the compression algorithm and level by modifying the `-z` option. In this example the level is `9` (maximum) and the algorithm is `lz4hc` (high compression lz4): ```bash mkfs.erofs --all-root -b 4096 -d2 -E noinline_data -z lz4hc,9 rootfs.erofs --tar=rootfs rootfs.tar ``` Finally, you can package and deploy the compressed EROFS rootfs using the CLI: ```bash # first, modify the Kraftfile to use the compressed EROFS rootfs: # ... # rootfs: # source: ./rootfs.erofs # ... # build and deploy the image unikraft build . --output /node-playwright-chromium:erofs-compressed unikraft run --metro fra \ --scale-to-zero policy=on \ -p 443:8080/http+tls \ -m 600M \ --image /node-playwright-chromium:erofs-compressed ``` ## Performance impact You can see below the same [`node` image](https://github.com/unikraft-cloud/examples/tree/main/node-playwright-chromium) packaged and deployed three times: as an uncompressed CPIO rootfs, and as an EROFS rootfs with and without compression. You can view the results in the table below, which show the image size, boot times, and minimum memory requirements for each case. To check the image size, you can list your images: ```bash unikraft images list ``` | rootfs Type | Compression | Image Size | Boot Time | Instance Memory Required | |---------------|-------------|------------|------------|--------------------------| | CPIO | No | 916 MiB | 1152.08 ms | 2700 MiB | | EROFS | No | 924 MiB | 155.39 ms | 1000 MiB | | EROFS | Yes (lz4hc) | 542 MiB | 122.10 ms | 600 MiB | :::note The image sizes reported in the table also contain the `base-compat` kernel layer, which is around 36MiB. The instance memory required is the minimum memory required to boot the instance, and respond to a simple `curl` request. ::: EROFS performs better in both compressed and uncompressed forms, compared to CPIO. The compressed EROFS rootfs boots even faster than the uncompressed one. This is likely due to the reduced amount of data that the system reads from disk, resulting in faster access times despite the decompression overhead. This only happens in the cold boot scenario. In [warm boots](/features/snapshots), the uncompressed EROFS rootfs tops the charts as the system needs no decompression at all, and wakeups happen faster. When it comes to memory usage, you can see that the CPIO image requires **a lot** more memory to boot. This is because the system must unpack the full archive into memory before it can use it. Unikraft Cloud reclaims that memory after boot, but the initial memory overhead is still higher than with EROFS. With EROFS, you don't see this limitation. :::tip For BYOC or on-prem deployments, Unikraft Cloud offers the option to share the same read-only initrd between instances when using EROFS images. This has the added benefit that Unikraft Cloud loads the initrd into host memory only once as read-only. Instances only map the pages they need, which reduces memory usage and boot times even further. Upon writing, the system performs copy-on-write, meaning that it copies only the modified pages into the instance's memory. In this case, the boot time differences between compressed and uncompressed EROFS rootfses are negligible. There might still be a difference when handling page faults, when accessing parts of the rootfs for the first time. ::: ## Conclusion As a rule of thumb, use uncompressed rootfses whenever possible and try to always use EROFS over CPIO for better performance. If limited by size constraints, try using compressed EROFS rootfses with a fast decompression algorithm like `lz4hc`. ## Learn more * The [rootfs formats tutorial](/tutorials/rootfs-formats) for understanding the differences between CPIO and EROFS. * The [`mkfs.erofs` documentation](https://man.archlinux.org/man/extra/erofs-utils/mkfs.erofs.1.en) for more information on creating compressed EROFS file systems. * The [CLI reference](/docs/cli/unikraft). * The `Kraftfile` [reference](/kraftfile/v0.7) for specifying rootfs sources and formats. --- ## Document: KraftKit To Unikraft URL: /docs/tutorials/kraftkit-to-unikraft # KraftKit To Unikraft This tutorial shows how to migrate from the legacy `kraft cloud` CLI to the new `unikraft` CLI for Unikraft Cloud. It focuses on the differences that matter during day-to-day work: authentication, profiles, deployment flow, common commands, and flag behaviour. ## Prerequisites Make sure you have the new CLI installed. See the [unikraft CLI overview](/docs/cli/overview) for installation instructions. If you still need the legacy syntax during the migration, keep the [`kraft cloud` reference](https://unikraft.org/docs/cli/reference/kraft/cloud) nearby. ## Main mental shift The biggest change is that the legacy CLI combines packaging and deployment into a single command, while the new CLI treats them as separate operations. In the legacy flow: ```bash title="kraft" kraft cloud deploy \ -p 443:8080/http+tls \ -M 512Mi \ --rootfs-type erofs \ . ``` the CLI reads the local project, builds the root filesystem and image if needed, pushes it, and starts an instance. In the new flow, the CLI splits the same work into two clearer stages: ```bash title="unikraft" unikraft build . --output /my-app:latest unikraft run --metro fra \ -p 443:8080/http+tls \ -m 512M \ --image /my-app:latest ``` This split has two practical consequences: * `unikraft build` is where you package the image and store it in a registry. * `unikraft run` is where you create an instance from an existing image and set runtime options such as ports, memory, scale-to-zero, volumes, and environment variables. That separation makes the deployment flow more explicit. It also means you need to think about whether a flag belongs to image creation time or instance runtime. ## Login and authentication ### Public Unikraft Cloud For the public service, the new CLI uses an interactive login flow that opens a browser window to authenticate with the Unikraft Cloud dashboard. The legacy CLI relies on environment variables to store the access token and metro selection. ```bash title="unikraft" unikraft login ``` ```bash title="kraft" # Set Unikraft Cloud access token export UKC_TOKEN=token # Set metro to Frankfurt, DE export UKC_METRO=fra ``` The environment variables `UKC_TOKEN` and `UKC_METRO` are only supported by the legacy CLI. They aren't the normal way to authenticate with the new CLI. ### Profile management The new CLI uses profiles to manage authentication and configuration for different environments. The `default` profile contains all the publicly available metros: ```bash unikraft profile list ``` ```ansi title="" NAME ACTIVE METROS default true ["fra0", "dal0", "sin0", "was1", "fra", "dal", "sin", "was", "sfo"] ``` Thus, listing instances with `unikraft` is more convenient than with `kraft cloud`: ```bash title="unikraft" unikraft instances list ``` ```bash title="kraft" export UKC_METRO=fra kraft cloud instance list #------------------------ export UKC_METRO=was kraft cloud instance list ```
```ansi title="unikraft" METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra httpserver-go121-qdrlz stopped /httpserver-go121 256MiB 1 summer-star-huyfslqm.fra.unikraft.app 20 hours ago fra httpserver-go121-bhjvm stopped /httpserver-go121 256MiB 1 restless-gorilla-dalzi9bs.fra.unikraft.app 20 hours ago was httpserver-go121-xjsli standby /httpserver-go121 256MiB 1 bitter-bonobo-20plsjfu.was.unikraft.app 2 minutes ago ``` ```ansi title="kraft" NAME FQDN STATE STATUS IMAGE MEMORY VCPUS ARGS BOOT TIME httpserver-go121-qdrlz summer-star-huyfslqm.fra.unikraft.app stopped /httpserver-go121@sha256:7785ebdf6c1ffaf05c3be... 256 MiB 1 90.20 ms httpserver-go121-bhjvm restless-gorilla-dalzi9bs.fra.unikraft.app stopped /httpserver-go121@sha256:7785ebdf6c1ffaf05c3be... 256 MiB 1 89.33 ms #------------------------- httpserver-go121-xjsli bitter-bonobo-20plsjfu.was.unikraft.app standby standby /httpserver-go121@sha256:7785ebdf6c1ffaf05c3be... 256 MiB 1 165.63 ms ``` Write operations require an explicit metro selection with `--metro`: ```bash title="unikraft" unikraft run --metro fra \ -p 443:8080/http+tls \ -m 512M \ --image /my-app:latest ``` For custom or private metros, define a profile in `~/.config/unikraft/config.yaml`: ```yaml title="config.yaml" profiles: : type: cloud token: organization: metros: - name: endpoint: https://api..unikraft.cloud country: ``` Then select it explicitly: ```bash title="unikraft" unikraft profile use unikraft profile list ``` :::tip If you work against more than one metro or installation, create one profile per environment and switch with `unikraft profile use` instead of rewriting shell variables. This has the advantage of persistence through configuration files. ::: ## Translate the most common commands Use the following mappings as a quick migration reference. ### Log in ```bash title="unikraft" unikraft login ``` ```bash title="kraft" export UKC_TOKEN= export UKC_METRO= ``` ### Select a configured environment ```bash title="unikraft" unikraft profile use ``` ```bash title="kraft" export UKC_TOKEN= export UKC_METRO= ``` ### Build and push an image ```bash title="unikraft" unikraft build . --output /my-app:latest ``` ```bash title="kraft" kraft pkg \ --plat kraftcloud \ --arch x86_64 \ --name /my-app:latest \ --rootfs-type erofs \ --push \ . ``` ### Run an instance from an image ```bash title="unikraft" unikraft run --metro fra \ -p 443:8080/http+tls \ -m 512M \ --image /my-app:latest ``` ```bash title="kraft" kraft cloud vm create \ --start \ -p 443:8080/http+tls \ -M 512Mi \ /my-app:latest ``` ### List instances ```bash title="unikraft" unikraft instances list ``` ```bash title="kraft" kraft cloud instance list ``` ### Remove an instance ```bash title="unikraft" unikraft instances delete ``` ```bash title="kraft" kraft cloud instance remove ``` ### Create a volume ```bash title="unikraft" unikraft volume create \ --metro fra \ --name data \ --size 200M ``` ```bash title="kraft" kraft cloud volume create \ --name data \ --size 200Mi ``` ## Flags differences ### Memory units Memory units behave differently in the new CLI. ```bash title="unikraft" unikraft run --metro fra \ -m 512M \ --image /my-app:latest ``` ```bash title="kraft" kraft cloud deploy \ -M 512Mi \ . ``` `unikraft` only accepts units in the binary system, for example, `512M` or `512MiB` for Mebibytes. It rejects the `kraft`-style `Mi` and `Gi` suffixes, so `-m 512Mi` fails with `invalid suffix: 'mi'`. The legacy `kraft cloud` works with the decimal system as well—so `512M` would mean 512 Megabytes, which is about 488 Mebibytes, while `512Mi` would mean 512 Mebibytes, which is about 536 Megabytes. ### Scale-to-zero The new CLI folds three legacy switches into one structured flag: ```bash title="unikraft" unikraft run --metro fra \ --scale-to-zero policy=idle,cooldown-time=5000,stateful=true \ --image /my-app:latest ``` ```bash title="kraft" kraft cloud deploy \ --scale-to-zero idle \ --scale-to-zero-stateful \ --scale-to-zero-cooldown 5s \ . ``` Not specifying any scale-to-zero configuration means **don't scale-to-zero**. Note that, besides `policy`, each of the options for scale-to-zero are optional. ### Filtering and waiting The new CLI adds a rich filter language for many commands. For example: ```bash unikraft instances list --filter 'state==running,metro==fra' ``` and: ```bash unikraft instance wait my-instance --until state==running ``` These `--filter` and `--until` expressions are features of the new CLI. They aren't available in the legacy `kraft cloud` CLI. See [Fields](/docs/cli/fields#filter-expression-reference) for the full syntax. ## A side-by-side migration example Suppose your old workflow looked like this: ```bash title="kraft" # do this once in every terminal session export UKC_TOKEN=token export UKC_METRO=fra kraft cloud deploy -n my-app -p 443:8080/http+tls -M 512Mi -e APP_ENV=production . kraft cloud instance get my-app kraft cloud instance logs my-app -f kraft cloud instance remove my-app ``` The migrated workflow is: ```bash title="unikraft" unikraft build . --output /my-app:latest unikraft run --metro fra -n my-app -p 443:8080/http+tls -m 512M -e APP_ENV=production --image /my-app:latest unikraft instances get my-app unikraft instance logs my-app -f unikraft instances delete my-app ``` Note the absence of environment variable exports in the new CLI, which uses persistent profiles instead. ## Migration checklist When moving an existing guide, script, or workshop from `kraft cloud` to `unikraft`, check the following: * Replace `kraft cloud deploy` with `unikraft build` plus `unikraft run`; * Move the local path such as `.` to `unikraft build`; * Add an explicit image reference with `--output` on `build` and `--image` on `run`; * Replace legacy authentication environment variables with `unikraft login` or a configured profile; * Move run-time flags such as memory, ports, env vars, volumes, and scale-to-zero settings to `unikraft run`; * Translate legacy scale-to-zero flags into the structured `--scale-to-zero` syntax; * Update instance management commands such as list and delete to the new command names; * Take advantage of new `--filter` and `--until` flags where they simplify scripts. ## The legacy `kraft cloud` CLI The `unikraft` CLI replaces the deprecated `kraft cloud` CLI. Use it for all work on Unikraft Cloud. Unikraft no longer recommends the `kraft cloud` CLI. ## Learn more Use the `--help` option for detailed information on using Unikraft Cloud: ```bash title="unikraft" unikraft --help ``` ```bash title="kraft" kraft cloud --help ``` For more details, check the following references: * The [unikraft CLI overview](/docs/cli/overview). * The [`kraft cloud` reference](https://unikraft.org/docs/cli/reference/kraft/cloud) on unikraft.org. * The [Fields](/docs/cli/fields#filter-expression-reference) reference for the new filtering language. * The [Environment Variables](/tutorials/environment-variables) tutorial for runtime configuration patterns. --- ## Document: Environment Variables URL: /docs/tutorials/environment-variables # Environment Variables import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" Environment variables are a common way to configure applications at runtime. Unikraft Cloud supports passing environment variables to instances during deployment through many methods. This tutorial discusses each below and orders them by superseeding precedence (from most powerful to weakest). Below are all the ways you can set environment variables for your Unikraft Cloud instances. The order they're presented is from the least precedence to the highest precedence. ## Dockerfile ENV You can set environment variables in your `Dockerfile` using the `ENV` instruction in your target `FROM` stage. The CLI automatically reads them when building the image and sets them in the resulting packaged image. ```dockerfile title="Dockerfile" # ... FROM ubuntu:latest ENV MY_ENV_VAR=my_value # ... ``` ## Kraftfile env You can set environment variables in your `Kraftfile` using the `env` field. `unikraft build` reads them and sets them in the resulting packaged image. ```yaml title="Kraftfile" # ... env: MY_ENV_VAR: my_value ``` :::tip This method takes precedence over the `Dockerfile`. It will override environment variables with the same name set in the `Dockerfile`. ::: ## CLI flags When deploying an instance, you can pass environment variables using the `--env` (or `-e`) flag. You can specify many environment variables by using the flag many times. The flag works similarly to the `docker run -e` flag. ```bash unikraft run --metro fra \ -e MY_ENV_VAR=my_value \ --image /my-app:latest ``` :::tip This method takes precedence over the `Dockerfile` and `Kraftfile`. It will override environment variables with the same name set in the previous two methods. ::: ## Wrapper script Create a wrapper script that sets the environment variables before executing the main app. Check what the original entrypoint of your app is and call it after setting the variables. For example, if the original entrypoint is `/entrypoint.sh`, you can create a new script `wrapper.sh` like this: ```bash title="wrapper.sh" #!/bin/sh export MY_ENV_VAR="my_value" exec /entrypoint.sh "$@" ``` Then you change either your `Dockerfile` or `Kraftfile` to start this wrapper script. Unikraft Cloud starts the image's `CMD` (or the Kraftfile `cmd`) and ignores the Dockerfile `ENTRYPOINT`, so set the wrapper as the command. To pass arguments on to the app, list them after the wrapper, for example `CMD ["/wrapper.sh", "--port", "8080"]`: Dockerfile Kraftfile ```dockerfile title="Dockerfile" # ... COPY wrapper.sh /wrapper.sh RUN chmod +x /wrapper.sh CMD ["/wrapper.sh"] ``` ```yaml title="Kraftfile" # ... cmd: "/wrapper.sh" ``` :::tip This method has the highest precedence and will override environment variables set in the CLI, `Dockerfile`, or `Kraftfile` as it hardcodes them in the script. Thus it's recommended to do this only for variables that you set and don't change. ::: ## Learn more * The [CLI reference](/docs/cli/unikraft). * The [`Dockerfile` reference](https://docs.docker.com/reference/dockerfile/) for understanding how to set environment variables in `Dockerfile`s. * The [`Kraftfile` reference](/docs/kraftfile/v0.7) for understanding how to set environment variables in `Kraftfile`s. --- ## Document: Docker To Unikraft Cloud URL: /docs/tutorials/docker-to-ukc # Docker To Unikraft Cloud This tutorial shows how to take an existing container image and turn it into a Unikraft Cloud deployment. It starts with a safe first pass that keeps the upstream filesystem intact. It then shows how to slim the image down to a bare minimum deployment. The examples use `FROM image:latest` as a placeholder for the upstream OCI image. They follow the same general pattern used in the [Unikraft Cloud example repository](https://github.com/unikraft-cloud/examples). ## Prerequisites Make sure you have the [unikraft CLI](/docs/cli/overview) installed. You also need a container runtime such as Docker because the `Dockerfile` builds the root filesystem. Log into Unikraft Cloud before you deploy: ```bash unikraft login ``` ## Use the upstream image The fastest way to get an existing container running on Unikraft Cloud is to keep its root filesystem as-is. For that, BuildKit fetches the upstream image through a `FROM image:latest` line and the `Kraftfile` reproduces the original start command. :::tip A `rootfs` source of `type: oci` skips the `Dockerfile` entirely and keeps the image's command and environment, so the steps below only matter when you want to change them. See [root filesystem from an existing image](/platform/images#root-filesystem-from-an-existing-image). ::: ### Inspect the upstream image Before writing the `Kraftfile`, inspect the original image and record its effective startup command. In Docker terms, the final process arguments are usually `ENTRYPOINT` followed by `CMD`. ```bash docker pull image:latest docker image inspect image:latest --format '{{json .Config.Entrypoint}}' docker image inspect image:latest --format '{{json .Config.Cmd}}' docker image inspect image:latest --format '{{json .Config.WorkingDir}}' docker image inspect image:latest --format '{{json .Config.ExposedPorts}}' ``` Use the values you get as follows: * If `Entrypoint` is empty, use `Cmd` as the full `cmd` value in the `Kraftfile`. * If both are present, concatenate them in the same order and use the result as the `cmd` value. * If the image starts through a wrapper such as `docker-entrypoint.sh`, keep that wrapper in the command line. * Prefer absolute paths in `cmd` so the deployment doesn't depend on a specific working directory. ### Create the initial files Start with the smallest possible `Dockerfile`: ```dockerfile title="Dockerfile" FROM image:latest ``` Then create a matching `Kraftfile`: ```yaml title="Kraftfile" spec: v0.7 runtime: base-compat:latest targets: - kraftcloud/x86_64 rootfs: source: ./Dockerfile format: erofs cmd: ["/path/from-entrypoint", "arg1", "arg2"] ``` In this first-pass `Kraftfile`: * `runtime: base-compat:latest` selects the generic compatibility runtime. * `targets: kraftcloud/x86_64` specifies the platform and architecture pairs to build for. Without `targets`, the build produces `x86_64` only, so add `kraftcloud/arm64` to the list to also build for ARM hosts. * `rootfs: ./Dockerfile` tells the CLI to build the root filesystem from the `Dockerfile`. * `cmd: [...]` explicitly sets the process arguments that should run inside the instance. Replace the placeholder command with the exact values extracted from `docker image inspect`. For example, if the image reports `Entrypoint=["/usr/local/bin/docker-entrypoint.sh"]` and `Cmd=["postgres"]`, then use `cmd: ["/usr/local/bin/docker-entrypoint.sh", "postgres"]`. :::caution These `Dockerfile` keywords aren't deployment controls on Unikraft Cloud. In particular, `EXPOSE`, `HEALTHCHECK`, `ONBUILD`, `SHELL`, `STOPSIGNAL`, and `VOLUME` are ignored as Docker deployment semantics. Also note that instances run as `root` unless you explicitly switch users at runtime. ::: ### Deploy the first pass Run an initial deployment before optimizing anything. This confirms that the runtime, filesystem, and command line are valid. ```bash unikraft build . --output /docker-port:latest unikraft run --metro fra \ -p 443:/tls+http \ -m 512M \ --image /docker-port:latest ``` Replace [``](/platform/services) with the app's listen port. Take this value from the image documentation or from the `ExposedPorts` field you inspected earlier. Public port 443 must use the `tls` and `http` handlers (`/tls+http`): without them, the platform rejects the instance with `Port 443 must use HTTP and TLS termination`. If the instance fails to boot, inspect the logs and fix the command line first. In most broken first attempts, the issue is either a wrong executable path, a missing wrapper script, or insufficient memory. :::tip If the upstream image depends on a specific `WORKDIR`, verify whether absolute paths are enough. If not, add a small wrapper script that changes directory and then `exec`s the original command. ::: ## Slim the root filesystem Once the first pass works, the next step is to stop shipping the entire upstream image. The goal is to copy only the files needed at runtime into a minimal final stage. In practice, that often means removing: * package manager caches; * compilers and build tools; * test data and documentation; * shells you no longer need; * temporary build artifacts. ### Find the files you actually need For native or mixed-language workloads, the difficult part is identifying the runtime dependencies that must stay in the image. The exact list depends on the upstream image. Use the upstream container to inspect the binary and its dependencies: ```bash docker run --rm --entrypoint sh image:latest -lc ' command -v your-app || true ldd /path/to/executable || true ' ``` If the image doesn't contain a shell, use a temporary debug image or inspect the filesystem with `docker create` and `docker cp`. The goal is the same in both cases: copy only what the final process needs at runtime. :::tip Verify whether the upstream image is `glibc`-based or `musl`-based before copying the loader and shared libraries. The sample paths above use `musl`-flavoured locations because that's a common pattern in the examples, but your image may differ. ::: ### Strip binaries and libraries Once you know which files the app needs, the next step is to reduce the size of the native executables and shared libraries themselves. This helps most with native apps and compiled dependencies. The common tool for this is `strip` from `binutils`. It removes symbol and debug information that the app doesn't need at runtime. :::tip Use stripping only after the app already boots correctly. ::: Start with the main executable and the libraries you identified with `ldd`: ```dockerfile title="Dockerfile" FROM image:latest AS upstream RUN strip --strip-unneeded /path/to/executable && \ strip --strip-unneeded /usr/lib/libexample.so.1 ``` If the upstream image doesn't include `strip`, install the required tooling only in the build stage. Don't copy that tooling into the final `scratch` stage. For example, on Debian-based images: ```dockerfile title="Dockerfile" FROM image:latest AS upstream RUN apt-get update && \ apt-get install -y --no-install-recommends binutils RUN strip --strip-unneeded /path/to/executable && \ strip --strip-unneeded /usr/lib/libexample.so.1 ``` As a rule of thumb, strip these files only when they're part of your runtime path: * the main executable; * helper executables started by wrapper scripts; * shared libraries reported by `ldd`. :::warning Don't strip files if you will need them for debugging later. ::: ### Convert the `Dockerfile` to a multi-stage build Start from the upstream image in a build stage. Then copy only the final executable, its direct shared libraries, and the app files into a `scratch` stage. ```dockerfile title="Dockerfile" FROM image:latest AS upstream # Add framework-specific build steps here if the image needs a compile step. # Remove caches here if the upstream image leaves package manager data behind. FROM scratch # Copy the main executable or wrapper script used by the app. COPY --from=upstream /path/to/executable /path/to/executable # Copy the dynamic loader and shared libraries required by the executable. COPY --from=upstream /lib/ld-musl-x86_64.so.1 /lib/ld-musl-x86_64.so.1 COPY --from=upstream /usr/lib/libgcc_s.so.1 /usr/lib/libgcc_s.so.1 COPY --from=upstream /usr/lib/libstdc++.so.6 /usr/lib/libstdc++.so.6 # Distribution metadata is often useful for runtime compatibility checks. COPY --from=upstream /etc/os-release /etc/os-release # Copy only the app files that are needed at runtime. COPY --from=upstream /app /app ``` This is the same shape used by examples such as [`httpserver-node21-nextjs`](https://github.com/unikraft-cloud/examples/tree/main/httpserver-node21-nextjs), where the final image keeps only the Node binary, its required libraries, `/etc/os-release`, and the built app output. ## Recommended deployment settings for the slim image After reducing the filesystem, deploy it with [EROFS](/tutorials/rootfs-formats) when possible. This often gives better cold-boot behaviour and lower memory pressure than CPIO for larger images. ```bash # make sure to specify the erofs format in Kraftfile # ... # rootfs: # source: ./Dockerfile # format: erofs # ... unikraft build . --output /docker-port:latest unikraft run --metro fra \ -p 443:/tls+http \ -m 256M \ --image /docker-port:latest ``` Treat the memory value as a starting point. Tune it after the first successful deployment by looking at the actual boot behaviour and logs. ## Bare minimum checklist Before calling the image minimized, check the following: * the `cmd` line uses the final executable path; * copy only runtime files into the final stage; * no package manager cache or compiler output remains; * expose the instance port through the CLI `-p` flag; * use [EROFS](/tutorials/rootfs-formats) unless you have a reason to stay on CPIO. ## More configurations ### Environment variables If the upstream image depends on environment variables, you can optionally carry them over explicitly. The most reliable method at deployment time is to pass them through the CLI with `--env` or `-e`: ```bash unikraft build . --output /docker-port:latest unikraft run --metro fra \ -p 443:/tls+http \ -m 256M \ -e APP_ENV=production \ -e LOG_LEVEL=info \ --image /docker-port:latest ``` If the image expects a wrapper entrypoint that prepares environment variables before starting the main process, keep that wrapper in your `cmd` line or replace it with your own script. The [Environment Variables](/tutorials/environment-variables) tutorial describes the wrapper pattern in more detail. :::tip `Dockerfile` `ENV` instructions carry over into the image, but you might want to override them at deployment time for better visibility and control. ::: ### Users and permissions Container images often include a `USER` directive to avoid running as `root`. On Unikraft Cloud, that directive only affects subsequent `RUN` steps while building the root filesystem. It doesn't automatically change the user that runs `ENTRYPOINT` or `CMD` at instance boot. File owners set with `COPY --chown` or `RUN chown` survive the build. In practice, this means the deployed process runs as `root` unless you switch users explicitly inside your startup command or wrapper script. When slimming the image, check permission-sensitive paths such as: * runtime directories under `/var` or `/run`; * app state directories under `/app`, `/srv`, or `/data`; * UNIX sockets or process ID files created by the upstream entrypoint. If the original image relied on a specific unprivileged user, you may need to preserve passwd and group metadata files as well: ```dockerfile title="Dockerfile" COPY --from=upstream /etc/passwd /etc/passwd COPY --from=upstream /etc/group /etc/group ``` If the app still assumes a particular user context, add a small wrapper that prepares the filesystem and then starts the original process. ### Scale to zero After the image boots correctly and the slimmed filesystem works, scale-to-zero is a good final validation step. It tests whether the app can suspend cleanly and resume on the next request. The simplest way to test scale-to-zero is to deploy with an explicit scale-to-zero policy and a short cooldown: ```bash unikraft build . --output /docker-port:latest unikraft run --metro fra \ -p 443:/tls+http \ -m 256M \ --scale-to-zero policy=on,cooldown-time=5000,stateful=true \ --image /docker-port:latest ``` After deployment, wait a few seconds and then list the instances: ```bash unikraft instances list ``` If everything is working, the instance should transition to `standby` when idle and wake up again on the next request. Apps with long initialization phases, background work, or long-lived connections may need extra adjustments before scale-to-zero behaves well. In those cases, review the [Scale-to-Zero](/features/scale-to-zero) feature page and consider a longer cooldown, stateful mode, or [temporarily disabling scale-to-zero](/tutorials/scale-to-zero-triggers#manual-triggers) from inside the app during startup. ## Learn more * [Images](/platform/images) and how `Dockerfile`s, `Kraftfile`s, and runtimes fit together. * [Rootfs Formats](/tutorials/rootfs-formats) for understanding the CPIO and EROFS tradeoffs. * The [`Kraftfile` reference](/docs/kraftfile/v0.7) for all supported top-level fields. * The [Next.js guide](/guides/httpserver-node21-nextjs) as a concrete example of a multi-stage Docker build trimmed down for deployment. --- ## Document: Kraftfile Reference (v0.7) This document contains information about how to write a `Kraftfile` which is used to configure, build, package and deploy your application as a Unikraft unikernel. URL: /docs/kraftfile/v0.7 # Kraftfile Reference (v0.7) The `Kraftfile` is the static configuration file used to programmatically build, run, package and deploy a unikernel. This document contains information about how to set that configuration including how to program Unikraft's core build system, third-party libraries, syntax options and more. A `Kraftfile` is typically found at the top-level of a repository. ## File names The following file names are automatically recognized where `Kraftfile` is the preferred name: - `Kraftfile` - `kraft.yaml` - `kraft.yml` ## Top-level `spec` attribute All `Kraftfile`s MUST include a top-level `spec` attribute which is used to both validate as well as correctly parse the rest of the file. The spec version for this document is `v0.7`: ```yaml spec: v0.7 ``` The `spec` element can also be specified as `specification`, for example: ```yaml specification: v0.7 ``` Only one of `spec` or `specification` may be specified — not both. ## Top-level `name` attribute An application `name` CAN be specified, for example: ```yaml spec: v0.7 name: helloworld ``` When no `name` attribute is specified, the directory's base name is used. ## Top-level `cmd` attribute A `cmd` attribute CAN be specified as an array or string which can be used for setting default arguments to be used during the instantiation of a new unikernel instance. ### Specified as an in-line array ```yaml spec: v0.7 cmd: ["-c", "/nginx/conf/nginx.conf"] ``` ### Specified as a multi-line array ```yaml spec: v0.7 cmd: - -c - /nginx/conf/nginx.conf ``` ### Specified as a string When specified as a string, the value is shell-parsed into individual arguments: ```yaml spec: v0.7 cmd: "-c /nginx/conf/nginx.conf" ``` ### Specifying kernel parameters The `cmd` attribute respects the Unikraft `uklibparam` convention of separating kernel arguments from application arguments via the `--` delimiter: ```yaml spec: v0.7 cmd: # Kernel arguments - env.vars=[ "HOME=/" ] # Delimiter - -- # Application arguments - -c - /nginx/conf/nginx.conf ``` ## Top-level `env` attribute An `env` attribute CAN be provided which injects environmental variables to the application. Compared to when specifying them in the `cmd` attribute, using the `env` attribute is more context-aware. Tooling recognizes where it can be best injected, for example, embedded as static variables during build-time, as OCI environmental variables when packaging, as command-line arguments, etc. ### Specified as a list ```yaml spec: v0.7 env: - HOME=/ ``` ### Specified as a dictionary ```yaml spec: v0.7 env: HOME: / ``` ## Top-level `labels` attribute A `labels` attribute CAN be specified which injects arbitrary key-value labels set during the packaging step: ```yaml spec: v0.7 labels: key: value ``` Label keys must match the pattern `^[a-zA-Z0-9._/-]+$`. ## Top-level `volumes` attribute A `volumes` attribute CAN be specified to declare the list of runtime mounts which are provided to the unikernel machine instance. There are two forms of syntax: "short-hand" and "long-hand". When specifying a destination path, this MUST be represented as an absolute path. ### Short-hand syntax A source path on the host is mapped to a destination path in the unikernel using a colon (`:`) delimiter. An optional third segment can specify a mode (e.g. `ro`): ```yaml spec: v0.7 volumes: - ./src:/dest - ./data:/data:ro ``` When no colon is present, the entry is treated as a source mounted at `/`. ### Long-hand syntax ```yaml spec: v0.7 volumes: - source: ./src destination: /dest driver: 9pfs readonly: false ``` The long-hand syntax supports the following fields: | Field | Type | Description | | ------------- | ------- | ------------------------------- | | `source` | string | Path on the host | | `destination` | string | Mount path inside the unikernel | | `driver` | string | Filesystem driver (e.g. `9pfs`) | | `readonly` | boolean | Whether the volume is read-only | | `mode` | any | Mode string (e.g. `ro`) | ### Single volume as a string The `volumes` attribute also accepts a single string value instead of a list: ```yaml spec: v0.7 volumes: ./src:/dest ``` ## Top-level `rootfs` attribute The `rootfs` element CAN be specified to define the root filesystem. ### Short-hand syntax When specified as a string, the value is treated as the source path (`source.path`). ```yaml spec: v0.7 rootfs: ./Dockerfile ``` The provided path can be one of the following: - A path to an existing CPIO archive (initramfs file) - A path to a directory which is then dynamically serialized into a filesystem image - A path to a `Dockerfile` which will be constructed via BuildKit - A path to a tarball (`.tar` or `.tar.gz`) ### Long-hand syntax The long-hand syntax allows specifying additional attributes. `source` is an object that groups the source path, type, and optional Dockerfile: ```yaml spec: v0.7 rootfs: source: path: ./initramfs.erofs type: erofs format: erofs ``` The top-level `rootfs` fields are: | Field | Type | Description | | -------- | -------- | ------------------------------------------------------------------ | | `source` | object | **(required)** Source configuration object (see below) | | `format` | string | Output format of the filesystem image: `cpio` (default) or `erofs` | > **Deprecated:** `source` can also be written as a plain string (`source: ./initramfs.erofs`) alongside a top-level `type:` field, matching the old format. > This form is deprecated. Use the structured `source:` object instead. #### `source` object fields | Field | Type | Description | | ------------ | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `type` | string | Explicit source type (see below). Must be `"dockerfile"` if `dockerfile` is set | | `path` | string | Path or reference to the filesystem source (directory, archive, OCI image, Dockerfile context) | | `dockerfile` | string | Path to a custom Dockerfile. When set, `type` is automatically inferred as `"dockerfile"` if not explicitly provided. `path` serves as the build context | When `type: dockerfile` is used without `dockerfile:`, the Dockerfile path is left unset and resolved by the caller. #### Using a custom Dockerfile ```yaml spec: v0.7 rootfs: source: type: dockerfile path: . dockerfile: ./custom/path/Dockerfile format: erofs ``` #### Source types The `type` field can be used to explicitly declare what kind of source is being provided: | Type | Description | | ------------ | ------------------------------------------------------------------------------------------------------- | | `oci` | An OCI image reference | | `dir` | A directory containing files | | `file` | A single file | | `tarball` | A tarball archive | | `cpio` | An existing CPIO archive | | `erofs` | An existing EROFS image | | `dockerfile` | A Dockerfile to be built. When `dockerfile:` is set, `type` is automatically inferred as `"dockerfile"` | When `type` is not specified, the source type is inferred from the path. #### Filesystem formats Two output formats are supported: - **`cpio`** (default) — produces a CPIO archive (initramfs) - **`erofs`** — produces an EROFS filesystem image ## Top-level `roms` attribute The `roms` attribute CAN be specified to declare additional read-only filesystem images to be provided to the unikernel at runtime. Each entry follows the same syntax as `rootfs` (both short-hand and long-hand): ```yaml spec: v0.7 targets: - kraftcloud/x86_64 roms: - ./extra-data/ - source: path: ./assets.erofs type: erofs format: erofs ``` ROMs are architecture-specific, so a build MUST be able to resolve which architectures to build them for. A `Kraftfile` that declares `roms` and no `runtime` therefore MUST also declare `targets`, unless the architecture is supplied by the tooling. ## Top-level `unikraft` attribute The `unikraft` attribute CAN be specified and is used to define the source location of the Unikraft core which contains the main build system and core primitives for building a unikernel "from source". If no `unikraft` element is specified, one of either `template` or `runtime` MUST otherwise be specified. There are two forms of syntax: "short-hand" and "long-hand". ### Setting a specific version ```yaml spec: v0.7 # Short-hand syntax unikraft: stable # Long-hand syntax unikraft: version: stable ``` To specify a specific version of Unikraft, including a specific Git commit: ```yaml spec: v0.7 # Short-hand for a specific version unikraft: v0.14.0 # Short-hand for a specific commit unikraft: 70bc0af ``` ### Setting a specific source location A remote fork, mirror, or local path can be set as the source: ```yaml spec: v0.7 # Short-hand (uses HEAD of default branch) unikraft: https://github.com/unikraft/unikraft.git # With a specific branch or tag unikraft: https://github.com/unikraft/unikraft.git@staging # Long-hand syntax unikraft: source: https://github.com/unikraft/unikraft.git version: staging ``` SSH-authenticated repositories are also supported: ```yaml spec: v0.7 unikraft: ssh://git@github.com/unikraft/unikraft.git ``` A local path can be used: ```yaml spec: v0.7 unikraft: path/to/unikraft ``` ### Specifying KConfig configuration To declare KConfig options, use the long-hand syntax. KConfig values can be specified in list format or map format: ```yaml spec: v0.7 # Using list-style formatting unikraft: kconfig: - CONFIG_EXAMPLE=y # Using map-style formatting unikraft: kconfig: CONFIG_EXAMPLE: "y" ``` ### A more complex example All three sub-attributes — `source`, `version` and `kconfig` — can be combined: ```yaml spec: v0.7 unikraft: source: https://github.com/unikraft/unikraft.git version: stable kconfig: CONFIG_EXAMPLE: "y" ``` ## Top-level `runtime` attribute The `runtime` attribute CAN be specified and is used to access a pre-built unikernel. The value is specified as a string referencing an OCI image: ```yaml spec: v0.7 runtime: unikraft.org/python3:latest ``` If no `runtime` element is specified, one of either `template` or `unikraft` MUST otherwise be specified. The `runtime` element is useful when you do not need to build a unikernel from source — for example, when using an existing application runtime like NGINX, Redis, or a language runtime like Python3. A typical pattern combines `runtime` with a filesystem and command: ```yaml spec: v0.7 runtime: unikraft.org/python3:latest volumes: - ./src:/src cmd: ["/src/main.py"] ``` ## Top-level `template` attribute The `template` attribute CAN be specified to reference an external repository which contains an application based on another `Kraftfile`. This offers a convenient mechanism for customizing or re-using configuration across multiple applications. If no `template` element is specified, one of either `runtime` or `unikraft` MUST otherwise be specified. ### Short-hand syntax ```yaml spec: v0.7 template: https://github.com/unikraft/app-elfloader.git ``` ### Long-hand syntax ```yaml spec: v0.7 template: source: https://github.com/unikraft/app-elfloader.git version: staging ``` The long-hand syntax supports the following fields: | Field | Type | Description | | --------- | ------ | -------------------------------------- | | `source` | string | Path or URL to the template repository | | `version` | string | Branch, tag, or commit to use | ### Template overlay behavior The process of applying the template's `Kraftfile` on top of another uses an overlay mechanism. Elements in the top-level `Kraftfile` overwrite the template's values when specified. For example, given a template with: ```yaml spec: v0.7 name: template unikraft: version: stable kconfig: - CONFIG_LIBVFSCORE=y targets: - qemu/x86_64 ``` And a top-level `Kraftfile` referencing it: ```yaml spec: v0.7 template: app/template:stable unikraft: version: staging ``` The result after overlay is: ```yaml spec: v0.7 name: template unikraft: version: staging targets: - qemu/x86_64 ``` The merge behavior for each field is: - **`name`**, **`targets`**, **`cmd`**, **`runtime`**, **`rootfs`**, **`roms`**, **`volumes`**: the overlay value completely replaces the template value when specified - **`env`**, **`labels`**: maps are merged, with overlay values taking precedence for duplicate keys - **`unikraft`**: `source` and `version` are replaced individually when specified; `kconfig` entries are merged with overlay values taking precedence - **`libraries`**: merged by library name, with overlay entries replacing template entries for the same name - **`template`**: not merged (avoids recursive templates) ## Top-level `libraries` attribute Additional third-party libraries CAN be specified as part of the build and are listed in map format. Similar to the `unikraft` attribute, each library can specify a `source`, `version` and a set of `kconfig` options: ```yaml spec: v0.7 name: helloworld unikraft: stable libraries: # Short-hand syntax musl: stable # Long-hand syntax lwip: source: https://github.com/unikraft/lib-lwip.git version: stable kconfig: CONFIG_LWIP_TCP: "y" CONFIG_LWIP_SOCKET: "y" ``` ## Top-level `targets` attribute A target is defined as a specific destination that the resulting unikernel is destined for and consists at minimum of a specific platform (e.g. `qemu` or `firecracker`) and architecture (e.g. `x86_64` or `arm64`) tuple. ### Short-hand syntax ```yaml spec: v0.7 targets: - qemu/x86_64 ``` ### Long-hand syntax ```yaml spec: v0.7 targets: - plat: qemu arch: x86_64 ``` The `architecture` and `platform` field names can be used as aliases for `arch` and `plat` respectively: ```yaml spec: v0.7 targets: - platform: qemu architecture: x86_64 ``` ### Short-hand and long-hand can be mixed ```yaml spec: v0.7 targets: - qemu/x86_64 - plat: qemu arch: arm64 ``` ### Specifying KConfig per target Each target can include its own `kconfig` options: ```yaml spec: v0.7 unikraft: stable targets: - plat: qemu arch: x86_64 kconfig: CONFIG_LIBVFSCORE_AUTOMOUNT_ROOTFS: "y" CONFIG_LIBVFSCORE_ROOTFS_9PFS: "y" CONFIG_LIBVFSCORE_ROOTFS: "9pfs" CONFIG_LIBVFSCORE_ROOTDEV: "fs0" - plat: qemu arch: x86_64 kconfig: CONFIG_LIBVFSCORE_AUTOMOUNT_ROOTFS: "y" CONFIG_LIBVFSCORE_ROOTFS_INITRD: "y" CONFIG_LIBVFSCORE_ROOTFS: "initrd" ``` --- ## Document: Release 9: Leda Unikraft Cloud Release 9 (Leda) release notes. URL: /docs/releases/r9-leda # Release 9: Leda Public release notes begin with [Release 10](/releases/r10-themisto). The team shared release notes for Release 9 (Leda) and earlier directly with customers. See the [releases overview](/releases/index) for the full list of releases. --- ## Document: Release 8: Ananke Unikraft Cloud Release 8 (Ananke) release notes. URL: /docs/releases/r8-ananke # Release 8: Ananke Public release notes begin with [Release 10](/releases/r10-themisto). The team shared release notes for Release 8 (Ananke) and earlier directly with customers. See the [releases overview](/releases/index) for the full list of releases. --- ## Document: Release 7: Carme Unikraft Cloud Release 7 (Carme) release notes. URL: /docs/releases/r7-carme # Release 7: Carme Public release notes begin with [Release 10](/releases/r10-themisto). The team shared release notes for Release 7 (Carme) and earlier directly with customers. See the [releases overview](/releases/index) for the full list of releases. --- ## Document: Release 6: Lysithea Unikraft Cloud Release 6 (Lysithea) release notes. URL: /docs/releases/r6-lysithea # Release 6: Lysithea Public release notes begin with [Release 10](/releases/r10-themisto). The team shared release notes for Release 6 (Lysithea) and earlier directly with customers. See the [releases overview](/releases/index) for the full list of releases. --- ## Document: Release 5: Amalthea Unikraft Cloud Release 5 (Amalthea) release notes. URL: /docs/releases/r5-amalthea # Release 5: Amalthea Public release notes begin with [Release 10](/releases/r10-themisto). The team shared release notes for Release 5 (Amalthea) and earlier directly with customers. See the [releases overview](/releases/index) for the full list of releases. --- ## Document: Release 4: Europa Unikraft Cloud Release 4 (Europa) release notes. URL: /docs/releases/r4-europa # Release 4: Europa Public release notes begin with [Release 10](/releases/r10-themisto). The team shared release notes for Release 4 (Europa) and earlier directly with customers. See the [releases overview](/releases/index) for the full list of releases. --- ## Document: Release 3: Io Unikraft Cloud Release 3 (Io) release notes. URL: /docs/releases/r3-io # Release 3: Io Public release notes begin with [Release 10](/releases/r10-themisto). The team shared release notes for Release 3 (Io) and earlier directly with customers. See the [releases overview](/releases/index) for the full list of releases. --- ## Document: Release 2: Callisto Unikraft Cloud Release 2 (Callisto) release notes. URL: /docs/releases/r2-callisto # Release 2: Callisto Public release notes begin with [Release 10](/releases/r10-themisto). The team shared release notes for Release 2 (Callisto) and earlier directly with customers. See the [releases overview](/releases/index) for the full list of releases. --- ## Document: Release 15: Isonoe Unikraft Cloud Release 15 (Isonoe) release notes. URL: /docs/releases/r15-isonoe # Release 15: Isonoe **Released:** 2026-11-04 :::note Read the [Release 15: Isonoe announcement](https://unikraft.com/blog/isonoe-15-release) on the Unikraft blog. ::: --- Release 15 lets running instances move between nodes without a restart, and brings Windows to the platform as a VM next to your Linux microVMs. Instance templates can now take updates, root users can suspend and resume accounts, and the platform tracks CPU time and memory-time per instance and per user. Memory vertical scaling arrives on ARM64, and the platform no longer enforces a license. The CLI ships for Windows and can manage an instance from inside its own shell. The dashboard adds a built-in CLI terminal, editable environment variables, more ways to create an instance, and self-service billing. Kraftlet reacts to failed pods right away, passes pod DNS settings through to instances, and gives each sandbox its own network shield. --- ## Platform ### Live migration Instances can now move from one node to another. A running instance keeps running while it moves, so its workload doesn't restart and doesn't lose its state. Stopped and suspended instances can move too. An instance keeps its UUID, its name, and its configuration on the new node. Its block volumes and its service group move along with it. If a migration fails, the instance stays on its original node. See [live migration](/features/live-migration). **Breaking changes:** Release 15 reserves port 45235 on the public addresses of each node for migrations, so guests can no longer publish a service on that port. Some nodes need extra configuration before the controller starts. See [live migration](/features/live-migration) for the details. **Requirements and limitations:** Requires the new migration permission, which root users have. Give it only to trusted users. Both nodes need Release 15. Supports microVMs only. You can't change an instance while it migrates, and on the destination it gets network addresses from that node. Live migration doesn't yet support instances created from a template, instances with vertical scaling, instances of kernel-less images, or autoscale instances. It also doesn't yet support instances with autokill, dependencies, inline ROMs, relay interfaces, or custom TAP devices. A volume moves only if it's a block volume created with the instance and no other instance has it attached. **Why it matters:** Operators can empty a node for maintenance or move load to another node. Workloads don't restart and don't lose their state. **Enterprise only.** ### Windows VMs Until now, Unikraft has been a Linux-only outfit. That changes with Release 15. Windows now runs on the platform as a VM, right next to your Linux microVMs. A Windows instance on Unikraft can do the same as any other. It scales to zero when idle and wakes on the next request. You can capture it as a template and stamp out new instances from there, and you can suspend and snapshot it like the rest. **Breaking changes:** None. **Why it matters:** Workloads that never made the jump to Linux can now run on Unikraft too. They get the same scale-to-zero, templates, and snapshots as everything else. **Enterprise only.** ### Template updates Instance templates can now take updates. You can give a template a new image, and new images for its ROMs and plugins. The template keeps its name and its UUID. If an update fails, the template doesn't change, and the template status shows the failed update. Instances created from the template follow the update. An instance without a snapshot of its own uses the new version at its next start, including instances that use scale-to-zero in stateless mode. An instance with its own snapshot, such as a suspended instance, keeps its version while it has that snapshot. Templates created from the updated template follow too, and autoscale uses the new version for new instances. The instance status now shows which template a clone came from. See [updating a template](/platform/instances#updating-a-template). **Breaking changes:** None. **Requirements and limitations:** Works for templates created directly and for templates converted from an instance that never started. You can't update a template converted from a running instance, a template with volumes, a template without exactly one network interface, or a template exported to a shared directory. An update keeps the names, paths, and authorization of ROMs and plugins, and changes only their images and the plugin configuration. A template created from another template changes its image only together with its parent. A clone whose image, ROMs, or plugins changed after its creation keeps its version. **Why it matters:** A new version of an app, or a security fix in its image, now reaches every instance of a template. Clients and autoscale configurations keep using the same template name. ### Account suspension Root users can now suspend a user account and resume it later. A suspended user keeps read access, but can't make any other changes. The user's scheduled operations fail, autoscale of the user's service groups stops, and autokill of the user's instances pauses. Root users can also choose to bring all instances of the user to rest when suspending the account. Resuming the account restores those instances to the state they were in, and autokill counts again from the end of the suspension. The user list shows which accounts have a suspension. **Breaking changes:** None. **Requirements and limitations:** Root users themselves can't have their accounts suspended. If the suspension doesn't include the user's instances, instances in standby still start on incoming requests. **Why it matters:** Operators can block an account, for example because of an unpaid invoice or abuse, without deleting its instances, and restore the account later in the same state. ### CPU time and memory-time accounting The platform now measures the CPU time and the memory-time of each instance to the millisecond, and adds them up for each user. Memory-time follows the current size of an instance with vertical scaling. Stopped instances and instances in standby use no memory-time. The user totals include deleted instances and carry on across controller restarts. CPU time no longer loses precision on long-running instances, and full VMs now report their CPU time and memory use as well. See [metrics](/platform/metrics). **Breaking changes:** None. **Requirements and limitations:** Requires the Firecracker and QEMU packages of this release. **Why it matters:** Usage-based billing and internal chargeback need exact numbers per instance and per user. The counters keep their precision at every size and survive instance deletion. **Enterprise only.** ### NVIDIA vGPU license management The platform can now manage the NVIDIA licenses of vGPU instances. An instance holds a license only while it runs, and returns it when it stops. The instance status shows the license state of each vGPU. **Breaking changes:** None. The feature is off by default. **Requirements and limitations:** Requires an NVIDIA Delegated License Service (DLS) appliance in node-locked leasing mode. Instances don't wait for a license to start, and run unlicensed until it arrives. The guest image must include the NVIDIA licensing service. **Why it matters:** License seats go only to instances that run, so a license server covers more instances. **Enterprise only.** ### Memory vertical scaling on ARM64 Vertical scaling of memory now also works for microVMs on ARM64 hosts, with the same API as on x86-64. Vertical scaling of vCPUs stays on x86-64 only. The default guest kernel configuration now supports vertical scaling on both architectures, so you don't need a custom kernel for it. An instance whose image can't scale a resource you set a range for now fails to start with a clear error. When the guest reaches its maximum memory or its quota, it now handles running out of memory as usual instead of stalling. See [vertical scaling](/features/vertical-scaling). **Breaking changes:** On x86-64, images built with the default kernel configuration now boot with Advanced Configuration and Power Interface (ACPI) enabled. **Requirements and limitations:** Requires a guest kernel of this release. **Why it matters:** ARM64 instances also no longer have to use resources sized for their peak memory use. **Enterprise only.** ### DNS settings in the CNI network configuration The guest now applies the DNS settings from the CNI network configuration of an instance: nameservers, domain, search domains, and options. The settings apply at boot and again when the instance resumes from a snapshot. Without nameservers in the settings, the instance keeps the nameserver from the platform configuration. See [guest network configuration with CNI](/features/annotations#guest-network-configuration-with-cni). **Breaking changes:** The guest ignored DNS settings before this release. Invalid DNS settings now stop the instance at boot. **Requirements and limitations:** Supports at most 3 nameservers, 6 search domains, and 8 options, and the guest ignores further entries. If both a domain and search domains are present, the search domains apply. Requires a guest kernel of this release. **Why it matters:** An external CNI plugin or IP address management system can now set the name resolution of an instance together with its addresses and routes. **Enterprise only.** ### Faster node startup A newly booted node now gets ready to serve requests in half the time. Nodes with many instances also stay responsive during system service reloads. **Breaking changes:** Requires `ukp-networking` 1.7.0 or later. This release removes the controller option `--net-iface-preopen`, and the controller doesn't start while it's set. Remove it from your configuration before updating. **Requirements and limitations:** Managed network mode doesn't change. **Why it matters:** New capacity takes on work sooner, for example when you add nodes to a metro or replace one. **Enterprise only.** ### IPv6 router advertisements in the guest The guest accepts IPv6 router advertisements again for its default router and its Maximum Transmission Unit (MTU). It still doesn't configure addresses from them. See [writing the CNI document](/features/annotations#writing-the-document). **Breaking changes:** A router advertisement on the guest network can again set the IPv6 default router and the MTU of the guest. **Requirements and limitations:** Applies to instances with a guest kernel of this release. **Why it matters:** Some CNI setups provide the IPv6 default route only through a router advertisement, and those instances now get it. **Enterprise only.** ### Licensing becomes enrollment The platform no longer enforces a license. Every feature, including checkpointing, plugins, GPUs, and custom networking, works on every node, including nodes without an enrollment. This release removes the grace period and the `Insufficient license` errors. What used to be the node license is now called enrollment. Enrollment ties a node to an organization, shows the node's state in the dashboard, and reports its usage. The commands, options, and settings take new enrollment names, and the old license names still work. **Breaking changes:** The agent's Prometheus metrics move from `license_*` to `enrollment_*`, for example `license_valid` becomes `enrollment_valid`, with no alias. Update any dashboards and alerts that use them. **Requirements and limitations:** Nodes that install packages from the `ukp://` package source still need enrollment. **Why it matters:** Nobody wants a license check in the critical path of production. Enrollment keeps the usage overview without a node ever refusing a workload because of it. **Enterprise only.** ### Automatic node enrollment A node that has an enrollment token now enrolls itself on startup, and keeps trying until it succeeds. The node starts reporting to its organization right away, without a restart. **Breaking changes:** None. Nodes that already have an enrollment keep it. **Requirements and limitations:** Each node needs its own token, or a reusable token from the **Enrollment** page of the dashboard. Remove the token from the node configuration once the node has enrolled. **Why it matters:** Enrollment used to be a manual step after installation, which didn't work for golden images and autoscaled machines. With a reusable token in the image, every machine built from it enrolls itself on first boot. **Enterprise only.** ### Other platform changes - Images without a kernel now use the platform's guest kernel package automatically when it's installed. - The instance status now shows the API port of each plugin to users with the `net_manager` permission. - Prometheus metrics now include the quota use of each user for certificates and volumes. - A new controller option sets the maximum number of users, 5,000 by default. - The guest kernel now supports rate limits and traffic shaping inside the guest. - This release updates the guest kernel to Linux 6.12.114 and CoreDNS to 1.15.0. --- ## Tooling ### CLI #### Windows builds The CLI now runs on Windows, on both amd64 and arm64, alongside Linux and macOS. A PowerShell installer matches the existing shell installer, the release downloads include Windows builds, and `unikraft upgrade` works on Windows too. See [CLI overview](/cli/overview). **Breaking changes:** None. **Requirements and limitations:** `unikraft instance shell` isn't available on Windows yet, so use the CLI from WSL for that. Builds need a BuildKit that can build Linux images. No Scoop or winget package exists yet. **Why it matters:** Lots of people develop on Windows, and "use WSL" was the only answer for them so far. #### Root filesystem from an existing image A Kraftfile's root filesystem can now come straight from an existing image. Both regular container images and Unikraft images work. The image's command, environment, and labels carry over, with anything set in the Kraftfile on top. See [root filesystem from an existing image](/platform/images#root-filesystem-from-an-existing-image). **Breaking changes:** None. **Requirements and limitations:** You need to set the image source type explicitly, because the CLI never guesses it. The image must be a registry reference and include the platform you're building for. Regular container images need a BuildKit builder. **Why it matters:** If the image already exists, rebuilding it from a Dockerfile to get the same files back is a waste of time. **Enterprise only.** #### Images served over HTTP The platform can run images served over plain HTTP or HTTPS, without a registry. The CLI now understands these image references anywhere it takes an image. See [images served over HTTP](/cli/registries#images-served-over-http). **Breaking changes:** None. **Requirements and limitations:** `image inspect` and `image copy` don't support these references yet. **Why it matters:** Before this release, a single instance using such an image broke `instance list` for its whole metro. Serving images from a CDN or an internal server can be simpler than running a registry. #### Sandbox shell builtins `unikraft instance shell` can now manage the instance you're in without leaving the shell. You can inspect the instance and its volumes, edit it, mount and unmount volumes, and start, stop, restart, or suspend it. The same builtins also work in the dashboard's instance terminal. The sandbox commands also check for the sandbox plugin first. If it's missing, they offer to attach it and restart the instance, instead of failing. See [shell builtins](/features/plugins#shell-builtins). **Breaking changes:** The `--plugin` option of `shell`, `exec`, `copy`, `read`, and `write` becomes `--plugin-name`, with a new `--plugin-image` option alongside it. **Requirements and limitations:** Edits, mounts, and unmounts apply after the next restart. The offer to attach the plugin only appears in an interactive terminal. **Why it matters:** Mounting a volume or restarting used to mean opening a second terminal and reconnecting the shell afterward. Now you can do it from where you already are. #### Private hostnames and boot timing Two more instance fields from the platform are now in the CLI: - The private hostname that reaches the instance from inside Unikraft Cloud. - The full breakdown of boot timing, from start to ready. Checkpoints and templates get their own timing fields too. See [fields](/cli/fields). **Breaking changes:** None. **Requirements and limitations:** Both appear in `instance get`, but not in the default `instance list` table. Some timing fields need a newer platform, and stay empty otherwise. **Why it matters:** Instances talking to each other was the main reason left to reach for `unikraft api`. When a cold start is slow, the timing breakdown shows where the time went. **Enterprise only.** ### Dashboard #### CLI terminal A new **Terminal** entry in the sidebar runs the real `unikraft` CLI in the browser, already signed in as you. Each organization you belong to is available as a profile, and your token never reaches the browser. The terminal stays open while you move around the rest of the dashboard, with as many session tabs as you like. Links in command output open in the dashboard. The terminal always runs the latest stable CLI release. **Breaking changes:** None. **Requirements and limitations:** The terminal runs one command at a time and isn't a full shell, so pipes, redirects, and command substitution don't work. Commands that need your local machine, such as `build`, `instance tunnel`, `volume import`, the terminal UI, and interactive editing, only work in the native CLI. Sessions close after 30 minutes without activity. **Why it matters:** Sometimes you need to run only one command, and installing a CLI and logging in for a single command is a lot of ceremony. Anything the dashboard doesn't have a button for yet is now only a few keystrokes away. #### Edit instance environment variables You can now edit the **Environment variables** card on an instance's **Runtime** tab. Change, add, or remove variables, or paste a whole `.env` file and check the rows it creates. **Save** applies the change on the next restart, and **Save and restart** applies it now. **Breaking changes:** None. **Requirements and limitations:** Changes only take effect when the instance restarts. Secret values stay hidden and unchanged unless you edit them. **Why it matters:** Changing a single variable used to mean the CLI or the API. It's the most common edit to a running instance, so it should be the easiest one. #### More ways to create an instance The **New instance** page can now start an instance from more than an image: - **Image**, as before. - **Template**, which inherits the configuration and state the template holds. - **Checkpoint**, which resumes with the memory and disk captured at that moment. - **Fork**, which copies an existing instance, running or stopped, with its configuration and disk. You can now set more than one port forward, and scale-to-zero no longer needs a public port. The memory and vCPU sliders follow the limits of the selected metro. You can pick existing volumes from a list or create a new one, and mount volumes read-only. **Create from code** turns the form into the matching CLI command, or a Go, Python, or JavaScript SDK snippet, ready to copy. **Breaking changes:** None. **Requirements and limitations:** Template, checkpoint, and fork sources need the matching features on the metro. **Why it matters:** Most new instances come from something that already exists, and the form now covers those cases. Once something works in the form, **Create from code** is the shortest way to get it into a script. #### Billing and plans Organization settings have a new **Billing** tab, showing the organization's current plan and what each plan includes. Organization admins can upgrade, switch, or cancel a plan themselves, and new limits reach every metro shortly after payment. Downgrading shows the lower plan's limits before you confirm, and cancelling keeps the current plan until the end of the billing period. New organizations start on the Hobby plan. See the [pricing page](https://unikraft.com/pricing) for the details of each plan. **Breaking changes:** Some accounts had no Hobby plan limits applied, and now have the correct limits. **Requirements and limitations:** Only organization admins can change the plan. Unikraft manages the plan of organizations with custom quotas, so they can't change plans themselves. After a downgrade, instances above the new plan's limits don't start or wake up. **Why it matters:** Moving to a bigger plan used to mean talking to a person, and new organizations started with no quota at all. Now you can sign up, try things out, and upgrade when you hit the limits, without waiting on anyone. #### Metros and nodes Every member of an organization can now see the **Metros** tab. Each metro shows its API endpoints, ready to copy into the CLI or an SDK, and the organization's quota usage in it. Organizations running their own compute get a new **Nodes** tab, listing every node they own with search and filters by metro and status. Nodes no longer have to belong to a metro. **Breaking changes:** None. **Requirements and limitations:** The **Nodes** tab only appears for organizations that operate a metro. **Why it matters:** "Which endpoint does the CLI need?" is one of the first questions anyone asks, and most people asking it couldn't see the answer. **Enterprise only.** --- ## Integrations ### Kraftlet pod status from platform events Kraftlet now updates a pod's status as soon as its instance stops, fails, or errors, instead of waiting for the next status check. Regular status checks stay in place as a backstop. See [pod status](/integrations/kubernetes#pod-status). **Breaking changes:** None. The feature is on by default, and you can turn it off. **Requirements and limitations:** Changes into states such as running or standby don't trigger an update, so scale-to-zero doesn't flood the Kubernetes API. **Why it matters:** Up to 15 seconds is a long time for Kubernetes to think a dead pod is fine. Controllers now see a failure, and replace the pod, as soon as it happens. **Enterprise only.** ### Custom DNS for Kraftlet and Karpenter Kraftlet now passes a pod's DNS configuration, including nameservers, search domains, and options, through to its instance. Without one, the instance uses the DNS from the CNI result. The Karpenter provider can now also skip DNS entirely. New machines then get no DNS records, and the cluster reaches them at their cloud provider's internal hostname instead. See [custom networking with CNI](/integrations/kubernetes#custom-networking-with-cni) and [Karpenter](/integrations/karpenter). **Breaking changes:** None. **Requirements and limitations:** Pod DNS configuration only applies to CNI pods. Machines without DNS serve a self-signed certificate, the controller and Kraftlet must run inside the same VPC, and you can't change this setting after creation. **Why it matters:** Pods can use their own resolvers, including IPv6 ones. Clusters that live entirely inside a VPC no longer need a public DNS zone to provision nodes. **Enterprise only.** ### Network shield for Kraftlet pods The network shield is an egress firewall for sandboxes. It starts closed and only lets out the traffic a policy allows, by host, network, port, or protocol. Policies can inspect TLS traffic, rate-limit requests and connections, and add secrets to outgoing requests, so a sandbox never sees the real value of a secret it uses. In Kraftlet, a pod can now get its own shield that comes and goes with the pod. Pods can also share one shield. See [egress filtering with a network shield](/integrations/kubernetes#egress-filtering-with-a-network-shield). **Breaking changes:** None. Requires Kraftlet chart 0.11.0. **Requirements and limitations:** A pod and its shield must run on the same node and in the same namespace. Inspecting TLS traffic needs a certificate authority that every sandbox behind the shield trusts. A shield whose policy fails to apply stays closed, and you need to replace it. The shield doesn't support inbound policies yet. **Why it matters:** Running code you don't trust means controlling what it can reach. The shield used to be a separate pod that someone had to place on the same node by hand. Now it comes and goes with the pod it protects. **Enterprise only.** --- [Back to all releases](/releases) --- ## Document: Release 14: Harpalyke Unikraft Cloud Release 14 (Harpalyke) release notes. URL: /docs/releases/r14-harpalyke # Release 14: Harpalyke **Released:** 2026-10-05 :::note Read the [Release 14: Harpalyke announcement](https://unikraft.com/blog/harpalyke-14-release) on the Unikraft blog. ::: --- Release 14 lets instances grow and shrink their memory and vCPUs while they run, and frees GPUs whenever their instances stop, suspend, or scale-to-zero. Full VMs gain templates, ROMs, plugins, and scale-to-zero, while NVIDIA vGPU support lets instances share a physical GPU. The CLI adds sandboxes, plugins, Dockerfile builds, and autokill controls. The dashboard can create instances, manage named API tokens, take checkpoints, and show services and quotas. Kraftlet gains faster instance updates and `kubectl exec`, and the JavaScript SDK adds a high-level sandbox API. --- ## Platform ### Vertical scaling of memory and vCPUs MicroVMs can now grow and shrink their memory and number of vCPUs while they run. The platform adds memory when the guest's memory use is high and removes it when use falls. The guest asks for another vCPU when its processes wait for CPU time and returns idle vCPUs later. Enable vertical scaling per instance by setting a range: ```json { "memory_mb": 512, "min_memory_mb": 256, "max_memory_mb": 2048, "min_vcpus": 1, "max_vcpus": 4 } ``` Vertical scaling is active for a resource when you set at least one bound. If you set only one bound, the other defaults to the initial size for the minimum or the per-instance limit for the maximum. The instance boots with the minimum and grows to `memory_mb` or `vcpus` directly after boot when you set those fields. Memory changes in steps of 128 MiB by default. Instance templates and autoscale templates accept the same fields, and their clones inherit the range. The instance status reports the current size in `memory_mb` and `vcpus`, and the range in `min_memory_mb`, `max_memory_mb`, `min_vcpus`, and `max_vcpus`. An instance that resumes from a snapshot, including after scale-to-zero, keeps its current size. Added memory and vCPUs count against your quotas. The instance stops growing when it reaches a quota. See [vertical scaling](/features/vertical-scaling). **Breaking changes:** None. Instances without a range aren't affected. **Requirements and limitations:** Requires a license with the vertical scaling feature and a guest kernel built with vertical scaling support. Supports microVMs on x86-64 only. You can't change the range after creating the instance. You also can't set a range for branches, checkpoints, or clones of a template that has a snapshot. They inherit the range from their source. **Why it matters:** An instance no longer has to use resources sized for its peak load. It uses the resources its workload needs at that moment and returns them when demand falls, without a restart. **Enterprise only.** ### Templates, scale-to-zero, ROMs, and plugins for full VMs Full VMs (`"type": "full"`) can now suspend and resume, and they can scale-to-zero in stateful and stateless modes. On resume, the platform loads the instance's memory on demand. Templates, ROMs, plugins, and startdata are also available for full VMs, using the same API as microVMs: ```json { "type": "full", "roms": [ { "name": "models", "image": "user/models:latest", "at": "/models" } ] } ``` A full VM now reports `RUNNING` only after its guest boots. The platform forwards traffic to a port only after the guest starts listening on it, matching microVM behavior. **Breaking changes:** None. **Requirements and limitations:** Full VMs don't yet support branching or checkpoints. **Why it matters:** Full VMs run workloads such as headful browsers and GPU inference, which often sit idle between requests. With scale-to-zero, an idle full VM releases its node resources and resumes for the next request. **Enterprise only.** ### GPU snapshots and NVIDIA vGPU A GPU is now assigned to an instance only while that instance runs. When a GPU instance stops, suspends, or scales to zero, its GPU becomes available to other instances. The snapshot saves the GPU state. On resume, the instance gets the same GPU when it's free or another free GPU of the same model when one is available. Operators can now also offer NVIDIA vGPUs. A vGPU is a virtual GPU on a physical GPU, allowing instances to share one physical GPU. The operator lists vGPUs in the controller's GPU device list with `"type": "vgpu-mdev"`. Existing entries continue to work as passthrough GPUs. The controller reports the number of available and allocated GPUs through the `gpu_available` and `gpu_allocated` metrics. See [GPUs](/platform/instances#gpus). **Breaking changes:** A GPU is no longer reserved while an instance isn't running. Starting an instance can fail when all GPUs are in use. **Requirements and limitations:** Suspending a GPU instance requires a GPU host driver that can save device state. GPU images need a kernel that supports NVIDIA vGPU. Instances still support at most one GPU. **Why it matters:** GPUs are the most expensive resource on a node. An idle GPU instance no longer blocks its GPU, and vGPU lets smaller instances share the physical GPU. **Enterprise only.** ### Event attribution and event stream Instance events now report who or what caused a change. `vm.state_change` and the new `vm.start_failed` event carry an `attribution` object: ```json { "attribution": { "operation": "4f0c9a1e-...", "kind": "stop", "trigger": "requested", "origin": "api", "user": "e2a4a5b0-..." } } ``` `origin` names the source, such as `api`, `guest`, `proxy`, `autoscale`, `scale-to-zero`, `scheduled-op`, `restart`, or `system`. For API requests, `user` identifies the requesting user, and every change from one request shares the same `operation`. When an instance stops, the event also reports the cause, such as `api`, `scale-to-zero`, `app-exit`, `quota-exceeded`, or `no-gpu`. `vm.start_failed` reports a failed start and its error. You can subscribe to events as a server-sent event stream: ```text GET /v1/audit?events=vm.state_change&tags=prod ``` Use the optional `events`, `uuid`, and `tags` filters to select which events to receive. See [audit events](/platform/audit-events). **Breaking changes:** The instance UUID moved from `data.vm` to `object.uuid` in `vm.state_change`. Event timestamps now always contain nine fractional digits. Earlier releases wrote the wrong fraction for timestamps below 0.1 seconds. A reliable log sink now drops an event after a short wait, configured by `block_timeout_ms` and defaulting to 50 milliseconds, instead of blocking the controller. A timeout of `0` restores the previous behavior. **Requirements and limitations:** Each controller supports at most 64 subscriptions. The stream doesn't replay earlier events. The public API endpoint requires the proxy configuration from this release. **Why it matters:** Customers can keep an audit trail of who changed what, alert on specific causes, and follow instance changes as they happen without polling the API. **Enterprise only.** ### Managed network mode Operators can now run a node in managed network mode: ```sh NET_MODE="managed" ``` In this mode, the platform creates no network devices and assigns no IP addresses. Each network interface uses a TAP device that the operator created on the host. The address, gateway, and name server come from the request or a CNI annotation: ```json { "network_interfaces": [ { "tap_name": "tap-web0", "ip": "10.20.0.5" } ], "gateway": "10.20.0.1" } ``` The operator provides routing, network address translation, and firewall rules, for example through firewall hook scripts. The default `isolates` mode doesn't change. See [custom network configuration](/features/custom-network-configuration). **Breaking changes:** None for the default mode. This release removes the old `subnets` mode, and the platform rejects a configuration that still names it. **Requirements and limitations:** Requires the `net_manager` permission and a license with the custom network configuration feature. The TAP device must exist before the instance starts. Managed mode doesn't support autoscale, relay interfaces, internal service groups, or internal DNS for instance names. **Why it matters:** Operators can connect instances directly to networks they already manage, such as VPNs, overlay networks, or routed data-center networks. This keeps the platform's address pool and network address translation out of the way. **Enterprise only.** ### Nested virtualization on ARM64 The `nested-virt` feature flag now works on ARM64 hosts. The guest can use the virtualization extensions and run its own hypervisor, and the instance can suspend and resume. Before this release, ARM64 hosts accepted the flag without enabling nested virtualization. **Breaking changes:** An ARM64 instance with `nested-virt` no longer starts on a host without nested virtualization support. **Requirements and limitations:** The host needs a CPU with FEAT_NV2 and Linux 6.16 or later, booted with `kvm-arm.mode=nested`. The feature requires the `nested_virt` permission, as it does on x86-64. **Why it matters:** Software that runs its own VMs, such as Android emulators, can now use ARM64 capacity. **Enterprise only.** ### Routes in the CNI network configuration The guest now applies the `routes` from the CNI result document in the `unikraft.com/cni` annotation: ```json { "routes": [ { "dst": "0.0.0.0/0", "gw": "10.1.0.1" }, { "dst": "10.2.0.0/16", "gw": "10.1.0.254" } ] } ``` A route can also set `priority`, `table`, and `scope`. A route without `gw` uses the `gateway` from the matching `ips` entry. The guest applies the document at boot and again after resuming from a snapshot. See [guest network configuration with CNI](/features/annotations#guest-network-configuration-with-cni). **Breaking changes:** The guest ignored routes before this release. An invalid document or a route the guest can't apply now stops the instance at boot. **Requirements and limitations:** Each document supports at most 16 routes. A gateway alone creates no route. List the default route explicitly. Requires the guest kernel from this release. **Why it matters:** An external CNI plugin or IP address management system can set an instance's addresses and routes without custom boot scripts. **Enterprise only.** ### Query parameters for read requests Endpoints that read state now accept their parameters in the query string as an alternative to a JSON body: ```text GET /v1/instances//wait?state=running&timeout_s=30 GET /v1/instances//log?offset=-4096&limit=4096 ``` The `wait` and `metrics` endpoints for a single instance also accept `POST`, for clients that must send a body. See [instances](/platform/instances). **Breaking changes:** A request can no longer supply `uuid` or `name` in the query string and also send a body. A request to a single-instance endpoint with an empty body array now fails instead of returning every instance owned by the user. **Why it matters:** Shell scripts, monitoring probes, and browsers can wait for an instance or read its log with one request. ### Unpin images by image reference `DELETE /v1/images` now accepts an image reference, so you don't need to find the image UUID first: ```json { "url": "user/image:latest" } ``` A tag selects the image that the tag currently resolves to. A digest selects one exact image. See [images](/platform/images). **Requirements and limitations:** Requires the `image_manager` permission, as before. If a tag moved after you pinned it, unpin the old image by digest or UUID. **Why it matters:** Deployment pipelines can pin and unpin images with the same reference. **Enterprise only.** ### Guest defaults for out-of-memory and IPv6 When the guest runs out of memory, the guest kernel now stops the instance instead of killing one process. The workload no longer continues in a partial state. A process that must remain eligible for the out-of-memory killer can set `/proc/self/oom_score_adj` to `0`. The guest no longer configures IPv6 automatically from router advertisements, and it skips duplicate address detection. IPv6 link-local addresses are available immediately after boot. See [instances](/platform/instances). **Breaking changes:** Workloads that relied on the out-of-memory killer now stop. IPv6 addresses and default routes must come from the platform configuration or CNI document. **Requirements and limitations:** Applies to instances using the guest kernel from this release. **Enterprise only.** ### Other platform changes - The default snapshot prefetch size is now 32 KiB instead of 1 MiB, shortening resumes. - The plugin field changes from `rom` to `image`. `rom` still works, but the response contains a deprecation warning. - ROM names can contain at most 63 characters, and ROMs and volumes can't mount at `/uk` or below it. - The instance status now shows which instance owns a relay interface. - The controller starts on hosts that can't create IPv6 sockets and uses IPv4 for its API. - This release updates the guest kernel to Linux 6.12.110. --- ## Tooling ### CLI #### Sandboxes and plugins Sandboxes extend standard Unikraft Cloud instances through the plugin API, adding file access, and remote command execution. Load the sandbox plugin with `--plugin` on `create`, `run`, or `edit`: ```sh unikraft instance run \ --metro fra \ --image my-app:latest \ --plugin 'name=sandbox,image=plugins/sandbox:latest,config={"persist_path":"/data"}' ``` Plugins behave like other instance fields. You can list, filter, sort, and inspect `plugins.*.name`, `plugins.*.image`, and `plugins.*.config`. An instance with the sandbox plugin gains five commands: - `unikraft instance exec my-instance -- ls -la /var/log` runs a command on the remote instance. `--dir` and `--env` control the command's environment, and the CLI returns its exit status. - `unikraft instance shell my-instance` opens an interactive shell. The shell runs locally while each command runs on the instance, preserving local shell state while resolving paths against the instance. - `unikraft instance copy ./config.json my-instance:/etc/app.json` copies one file in either direction. Use `/:` to select a metro. `cp` is an alias. - `unikraft instance write my-instance ./data.bin /var/lib/app.bin --parents` uploads a file. Use `--append` to add to the remote file instead of replacing it. - `unikraft instance read my-instance /var/log/app.log` downloads a file. The local path is optional and defaults to the remote file's base name. See [plugins](/features/plugins). **Breaking changes:** None. The plugin option and sandbox commands are new. **Requirements and limitations:** The sandbox commands require the sandbox plugin on the target instance. The shell is experimental and doesn't support terminal emulation, full-screen programs such as `vim`, `top`, or `less`, password prompts, or job control. The copy, read, and write commands transfer one regular file at a time and don't preserve permissions. `exec` forwards standard input only from a redirected file or pipe. **Why it matters:** You can inspect and debug a running instance without adding tools to its image and redeploying it. #### Dockerfile builds Kernel-less images no longer need a Kraftfile. `unikraft build` accepts a Dockerfile directly and builds an image containing the root filesystem it describes: ```sh unikraft build ./Dockerfile --arch x86_64 --output my-org/my-app:latest ``` The input can be a project directory, Kraftfile, or Dockerfile. Its filename identifies the type, so names such as `Dockerfile.prod` work. A directory without a Kraftfile builds from its `Dockerfile`. When both files are present, the Kraftfile takes precedence. Passing a Kraftfile path directly now works, with relative sources resolved from the Kraftfile's directory. See [`unikraft build`](/cli/unikraft/build). **Breaking changes:** None. Directories containing a Kraftfile build as they did before. **Requirements and limitations:** A Dockerfile-only build has no target or runtime from which to infer an architecture, so you must pass `--arch`. The resulting image has no kernel and only runs in metros with a default kernel installed. **Why it matters:** Most applications already have a Dockerfile. That file is now enough to get from source to a running instance. #### Autokill The CLI can now configure the platform to delete resources automatically after they're no longer needed. Use `--autokill` on `create` and `edit` for instances, service groups, instance templates, and instance checkpoints: ```sh unikraft instance run \ --metro fra \ --image my-app:latest \ --autokill time=5m,num-requests=100 ``` The triggers depend on the resource: - Instances accept `time`, measured from when the instance stops, and `num-requests`, the maximum requests served before deletion. - Service groups accept `time`, measured from when the group becomes empty. - Templates accept `time`, measured from the last clone. - Checkpoints accept `time`, measured from the last restore. The fields `autokill.time` and `autokill.num-requests` work in list, filter, sort, and inspect output like other resource fields. See [autokill](/features/autokill). **Breaking changes:** None. The option is new, and the platform deletes nothing unless you set it. **Requirements and limitations:** The CLI doesn't yet support autokill for image pins because it has no image pin command. **Why it matters:** Short-lived workloads such as CI jobs, preview environments, one-shot tasks, branches, and checkpoints no longer need manual cleanup when you set their lifetime in advance. ### Dashboard #### Plugins and sandboxes The dashboard can dynamically add the official sandbox plugin to a running microVM and open a shell into it. This provides the same shell access as `unikraft instance shell` without leaving the instance page. **Breaking changes:** None. **Requirements and limitations:** The shell has the same limitations as the CLI: no terminal emulation for complex terminal applications, no job control, and no process suspend or resume. **Why it matters:** The dashboard now provides direct access to workloads running inside an instance. #### Create and delete instances The new **New instance** page creates an instance from an official image or your own registry. Choose the metro, name, memory, vCPUs, arguments, environment variables, and restart policy. The form stores its values in the page address. You can bookmark or share an unfinished configuration. The dashboard can also delete, lock, and unlock an instance. **Breaking changes:** None. **Requirements and limitations:** The dashboard doesn't yet expose every instance creation API field. **Why it matters:** You can now create and manage the full instance lifecycle without switching to the API or CLI. #### Named API tokens Organizations can now create more than one API token, give each token a unique name, and revoke one token without affecting the others. **Breaking changes:** None. This release migrates each organization's existing token as a named token. **Requirements and limitations:** Token names must be unique within an organization. **Why it matters:** Separate tokens for laptops, CI pipelines, and teammates make credentials independently traceable and revocable. #### Services page The service detail page shows traffic volume, connected instances, and per-instance information such as requests per second. **Breaking changes:** None. **Requirements and limitations:** The dashboard doesn't yet expose every service field, and autoscale configuration remains available through the API only. **Why it matters:** A service provides one view of the resources behind it, making it easier to identify pressure. #### Checkpoints The dashboard can now create a checkpoint from an instance and show every checkpoint for that instance. **Breaking changes:** None. **Requirements and limitations:** You can't yet roll back to a checkpoint from the dashboard. **Why it matters:** Core platform checkpointing is now available directly from the instance page. #### Quota management An organization's default quotas now appear in the **Quotas** tab on the organization settings page. The page includes every quota reported by each metro, including service groups, services, volumes, minimum volume size, memory, and vCPUs. **Breaking changes:** None. **Requirements and limitations:** The dashboard reports limits as conflicting when the organization uses both self-hosted and managed metros. **Why it matters:** Users can see the resource limits available to their organization in one place. --- ## Integrations ### Kraftlet instance cache Kraftlet uses an instance cache when updating pod status in the Kubernetes API. Before this release, polling the platform directly made instance creation and status updates slower as the number of instances grew. The cache removes that scale-dependent delay for deployments with thousands of pods on one node. **Breaking changes:** None. Kraftlet configuration doesn't change. **Why it matters:** Kraftlet deploys new instances and updates pod status faster at large scale. **Enterprise only.** ### Kraftlet support for `kubectl exec` Kraftlet now implements `kubectl exec` through Unikraft [plugins](/features/plugins), so commands work with Unikraft-backed pods as they do with other runtimes. See [Kubernetes integration](/integrations/kubernetes). **Breaking changes:** None. **Requirements and limitations:** Start Kraftlet with `exec-plugin-image` to select the plugin image. Each pod that needs command execution must set the `cloud.unikraft.v1.instances/enable-exec: "true"` annotation. **Why it matters:** Unikraft-backed pods now support the standard Kubernetes command-execution workflow. **Enterprise only.** ### Sandboxes in the JavaScript SDK The JavaScript SDK now provides a high-level sandbox API. It starts a Unikraft Cloud microVM, attaches the sandbox plugin, runs your task, and cleans up the resources: ```typescript import { Sandbox } from "@unikraft/cloud"; // `await using` cleans up the sandbox when the script finishes. await using sandbox = await Sandbox.create(); const { stdout } = await sandbox.exec("echo hello"); console.log(stdout); // hello ``` Attach a volume to persist work across sandbox sessions and use scale-to-zero to release resources while the sandbox is idle. See [JavaScript SDK sandboxes](/sdks/js/sandboxes). **Why it matters:** Applications can run untrusted or isolated work in short-lived microVMs through a small, typed API. --- [Back to all releases](/releases) --- ## Document: Release 13: Adrastea Unikraft Cloud Release 13 (Adrastea) release notes. URL: /docs/releases/r13-adrastea # Release 13: Adrastea **Released:** 2026-09-02 :::note Read the [Release 13: Adrastea announcement](https://unikraft.com/blog/adrastea-13-release) on the Unikraft blog. ::: --- Release 13 brings the platform to ARM64 hosts and decouples the guest kernel from the app image. Networking gains caller-chosen MAC addresses, address-less interfaces, and CNI-driven guest configuration. On the tooling side, the CLI builds for both architectures at once and drives branching, checkpointing, GPUs, and tunnels. A Karpenter provider makes Unikraft capacity elastic from inside Kubernetes, and a Go SDK reduces a plugin to about 20 lines. --- ## Platform ### ARM64 support The platform now runs on ARM64 hosts. Packages ship for `arm64` alongside `amd64` on every supported distribution. That covers Debian 11, 12, and 13, plus Ubuntu 24.04 and 26.04. MicroVMs, full VMs, the API, and all tooling behave the same as on x86-64. See [image architectures](/platform/images#architectures) for how architectures apply to images. **Breaking changes:** None. Nothing changes for existing x86-64 deployments. **Requirements and limitations:** ARM64 hosts don't yet support GPU passthrough for full VMs. **Why it matters:** ARM64 capacity is a large, and often cheaper, part of what cloud providers offer. The same platform, with the same API and tooling, now runs on ARM64 hosts. Use it to cut the cost per instance, or to serve ARM64 workloads natively instead of emulating them. **Enterprise only.** ### Platform kernel and kernel-less images An image no longer has to carry its own kernel. An image that ships only a config and an initrd boots with the kernel installed on the node, which the operator points the controller at: ```sh ukpd --images-kernel-path /usr/share/unikraft/kernel ... ``` The directory holds a `kernel` binary and a `config.json` with the same shape as an image config. Leaving the option empty disables the feature. The platform picks up a replaced kernel automatically. The next instance to start uses the new one, and running instances keep the kernel they booted with until they shut down. An instance that resumes from a snapshot always boots the exact kernel the snapshot captured, including across clone, branch, and template start. The guest kernel ships as its own package, so a node moves to a new guest kernel independently of the images that run on it. See [platform kernel](/platform/images#platform-kernel). **Breaking changes:** None. Images that carry a kernel behave exactly as before. **Requirements and limitations:** A kernel-less image must still provide an initrd. **Why it matters:** The guest kernel no longer ties to the app image. A kernel update, including a security fix, no longer means rebuilding and re-pushing every image, and the operator rolls out one kernel package for the whole node instead. Images also get considerably smaller, which shortens pulls and cold starts. **Enterprise only.** ### Scheduled checkpoints [Scheduled operations](/features/cron-jobs) gained a `checkpoint` action, so an instance checkpoints itself on a schedule with nothing driving it from the outside: ```json { "schedops": [ { "name": "nightly", "when": "*-*-* 03:00:00", "action": "checkpoint" } ] } ``` Each occurrence creates a checkpoint exactly as `POST /v1/instances//checkpoints` does, named after the instance with a random suffix. See [scheduled checkpoints](/features/cron-jobs#scheduled-checkpoints). **Breaking changes:** None. `checkpoint` adds to the existing `start`, `stop`, `delete`, and `exec` actions. **Requirements and limitations:** Requires a license with the checkpointing feature. Full VMs don't support checkpointing yet. **Why it matters:** Periodic checkpointing becomes a property of the instance rather than a job someone has to run and watch. Long-lived stateful instances build a rolling set of restore points on their own. **Enterprise only.** ### Nested virtualization An instance can now reach the host's virtualization extensions, so a hypervisor runs inside the guest. Request the feature per instance with the new `nested-virt` feature flag: ```json { "name": "my-nested-vm", "features": ["nested-virt"] } ``` Instances without the flag continue to see virtualization extensions masked off, exactly as before. The instance status reports the active flags in its `features` array. Snapshotting a nested-virt instance and resuming it works. See [nested virtualization](/platform/instances#nested-virtualization). **Breaking changes:** None. The flag is opt-in and defaults to off. **Requirements and limitations:** Requires the new `nested_virt` user permission, and the host must carry a nested virtualization configuration. An instance created from a snapshot, whether template, branch or checkpoint, can't take the flag: choose it for the instance that takes the snapshot. **Why it matters:** Customer software that ships its own VMs used to need bare metal. It now runs as an ordinary platform instance alongside everything else. **Enterprise only.** ### Custom MAC addresses and address-less interfaces Managed network interfaces became considerably more flexible. A custom interface carries a caller-chosen MAC address, and an interface can exist with no IP address at all: ```json { "network_interfaces": [ { "tap_name": "tap-custom0", "mac": "02:1a:2b:3c:4d:5e", "autoconfig": false } ] } ``` Setting `ip` or `mac` now requires a `tap_name`. Before, `ip` and `tap_name` had to appear together. The new `autoconfig` field controls whether the platform configures the address inside the guest. It defaults to on for the default interface and for interfaces with an IP, and must be off for an interface without one. An interface with no address reports no `private_ip`, and takes no part in DNS registration or service routing. MAC addresses must be unicast and must fall outside the platform's own address pool. Custom IPv4 networks are no longer limited to `/30` or smaller. See [custom network configuration](/features/custom-network-configuration). **Breaking changes:** None. Combinations the platform accepted before keep working. **Requirements and limitations:** Custom interfaces require the `net_manager` permission, as before. Custom interfaces don't offer the relay feature. The platform now rejects a request for it instead of accepting the request and ignoring the field. **Why it matters:** A guest whose licensing, clustering or peer identity ties to a MAC address keeps that identity across a recreation of the instance. An interface handed over without an address is what a customer-managed network needs, whether an external IPAM, a CNI plugin, or a virtual appliance doing its own addressing. The platform provides the link, and the tenant decides what runs on it. **Enterprise only.** ### Guest network configuration with CNI The guest reads a CNI result document from an instance annotation and applies it to its network interfaces: ```json { "annotations": { "unikraft.com/cni": "{\"cniVersion\":\"1.0.0\",\"interfaces\":[...],\"ips\":[...]}" } } ``` The document is a standard CNI Result (`types/100`). The guest matches addresses to interfaces by MAC address. Updating the annotation on a running instance reconciles against the current configuration, so an address change needs no restart. See [guest network configuration with CNI](/features/annotations#guest-network-configuration-with-cni). **Breaking changes:** None. Nothing changes for instances without the annotation. **Requirements and limitations:** The guest applies only `interfaces[].mac`, `ips[].interface`, and `ips[].address`. It ignores routes, gateways, DNS, and the maximum transmission unit. At most 8 interfaces and 16 addresses per document. Requires the latest guest kernel. **Why it matters:** This closes the loop for address-less interfaces. An external CNI plugin or IPAM system decides the addressing and writes its result into an annotation, and the guest configures itself from it. Nothing needs installing inside the guest, and no custom boot scripts need maintaining. **Enterprise only.** ### Instance annotation events The event log gained a `vm.annotate` event, which fires whenever an instance's annotations change: ```json { "type": "vm.annotate", "timestamp": "2026-08-19T10:04:11Z", "data": { "vm": "b495f451-0370-4d8e-8b94-a5b22d2212f9", "annotations": { "env": "prod", "team": "search" } } } ``` See [annotation events](/features/annotations#annotation-events). **Breaking changes:** `POST /v1/instances` and `PATCH /v1/instances` now reject annotation values containing ASCII control characters (`0x00` to `0x1f`, or `0x7f`, including tab, newline, and carriage return) with `400 Bad Request`. This breaks clients that stored binary or multi-line values. Any other byte sequence, including multi-byte UTF-8, is still accepted, and stored annotations keep their values. **Requirements and limitations:** Enable the event in the event configuration, like the other event types. **Why it matters:** Annotations are how you attach your own meaning to an instance: owner, environment, tenant, billing code. Emitting them as events lets external systems track changes rather than polling the instance API to find out that a label moved. It also maps instance UUIDs to annotations during log processing. **Enterprise only.** ### Autoscale pressure signal An [autoscale](/features/autoscale) group takes a high watermark, and the proxy reports a crossing back to the client on the response: ```json { "n_max": 50, "high_watermark": 0.8 } ``` `high_watermark` is a fraction in `[0, 1]` of `n_max`, and `0` disables the signal. The autoscale status endpoint reports it back in the same form. Enable the header on the proxy, and a response whose request triggered a scale-out carries the group's state. The format is a structured-field dictionary, as defined by [RFC 9651](https://www.rfc-editor.org/rfc/rfc9651.html): {/* vale off */} ```text Autoscale-Info: high-watermark-crossed=?1, n-total=41, n-max=50 ``` {/* vale on */} The header is off by default. The proxy configuration enables it, and optionally renames it, through `UKP_PROXY_AUTOSCALE_INFO_HEADER` and `UKP_PROXY_AUTOSCALE_INFO_HEADER_NAME`. See [high watermark](/features/autoscale#high-watermark). **Breaking changes:** None. `high_watermark` defaults to off, and the header is opt-in. **Requirements and limitations:** The signal rides on responses whose request caused a scale-out, so it reflects the state at that moment rather than forming a continuous stream. Update the controller and the proxy together. **Why it matters:** Spreading load across regions or clusters means knowing that a group is running out of room before it fills. The signal travels on the response path the client already reads. A client steers new work elsewhere without polling a separate metrics endpoint, and without waiting for a scale-out to fail. **Enterprise only.** ### Configurable plugin authorization Instance [plugins](/features/plugins) choose the rule that admits a request. A plugin description takes an optional `authorization` object: ```json { "plugins": [ { "name": "my-plugin", "rom": "...", "authorization": { "type": "bearer", "token": "s3cr3t" } } ] } ``` With `"type": "bearer"`, the plugin's own token authorizes the request. Without the object the previous behavior applies: the supplied token must belong to a platform user with access to the instance. The instance status reports a non-default authorization `type`, and never reports the token back. See [plugin authorization](/features/plugins#authorization). **Breaking changes:** None. A plugin without an `authorization` object behaves exactly as before. **Requirements and limitations:** A `bearer` token is mandatory, and it has a maximum length. Update the controller and the proxy together. **Why it matters:** Software that addresses a plugin often holds no platform account and needs none. Think of a sidecar, a customer's own control plane, or a webhook from a third-party service. A per-plugin token admits those callers without minting platform users and without granting them access to the platform API. **Enterprise only.** ### User information API Two new endpoints report what a user is and what it may do: ```text GET /v1/users GET /v1/users/ ``` ```json { "status": "success", "data": { "users": [ { "uuid": "e2a4a5b0-...", "created_at": "2026-08-12T09:14:03Z", "permissions": ["net_manager", "image_manager", "nested_virt"], "disabled": false } ] } } ``` A regular user sees itself. A root user queries any user by UUID, or lists every user by omitting the UUID. The API reports `disabled` to root only. **Breaking changes:** None. Both endpoints are new. **Requirements and limitations:** Listing users, or querying a user other than yourself, requires the `root` permission. **Why it matters:** Clients used to hard-code which capabilities their token holds, or discover them by experiment. They now ask instead. A UI hides features a user can't reach, and automation fails early with a clear reason rather than hitting a permission error deep inside a workflow. **Enterprise only.** --- ## Tooling ### CLI #### Multi-platform builds `x86_64` and `arm64` are now equal citizens. `unikraft build` produces an image for every architecture you ask for in a single invocation, and carries ROMs through multi-architecture builds as well. Because neither architecture is the default any more, the build no longer picks one for you. Every build names its architectures, through the `targets:` key in the Kraftfile or the new `--arch` flag. For an image with a `runtime`, little changes in practice, because the build still uses every one of the runtime's architectures unless you narrow them down. For a ROM, the target is now required: ```yaml spec: v0.7 roms: - ./Dockerfile targets: - kraftcloud/x86_64 ``` See [image architectures](/platform/images#architectures). **Breaking changes:** Building a ROM without `targets:` or `--arch` now fails rather than defaulting to a single architecture. The old default assumed that ROMs were platform-independent, which they aren't, so the explicit form is both more correct and simpler to follow. **Requirements and limitations:** BuildKit must be [configured on the host for multi-platform builds](https://docs.docker.com/build/building/multi-platform/). **Why it matters:** ARM hosts arrive with this release, and an image runs only on a host matching the architecture of its build. Naming targets explicitly means one Kraftfile builds for both, and nobody discovers the mismatch at deploy time. **Enterprise only.** #### Branching and checkpointing [Branching](/features/branching) and [checkpointing](/features/checkpoints) have been available on the platform for a while, and the CLI now drives both. You create an instance from one of four sources rather than only from an image: `--image`, `--template`, `--branch`, or `--checkpoint`: ```sh unikraft instance create --branch my-instance --set name=my-branch ``` Branching preserves the source's in-memory state, and works whether the source is running, stopped, or itself a template. Checkpoints and templates each get their own command group, `unikraft instance checkpoint` and `unikraft instance template`, with the usual `list`, `get`, `create`, `edit`, and `delete`, plus `checkpoint history` to walk the lineage of a checkpoint. **Breaking changes:** None. Every existing way to create an instance continues to work. **Requirements and limitations:** Branching and checkpointing require a licensed platform. A branch must stay within the metro of its source. **Why it matters:** These were the last big platform capabilities that still needed a hand-written `unikraft api` call. Branching and checkpointing now stay inside the CLI, with the same fields, filters, and output formats as the rest of it. #### GPUs Release 12 brought NVIDIA GPUs to the platform, and the CLI now speaks that language too. `--gpus` requests a GPU when creating an instance: ```sh unikraft instance create --type full --gpus 1 --image my-app:latest ``` Listings and inspect output show the GPUs an instance holds as `gpus.*.uuid` and `gpus.*.model`, alongside a `resources.gpus` count, so they filter and sort like every other field. See [GPUs](/platform/instances#gpus). **Breaking changes:** None. **Requirements and limitations:** Requires instance `type` `full` and a plan with GPU support. At most one GPU per instance for now, and you can't choose the specific GPU: the platform picks a free one from the host. **Why it matters:** You create a GPU instance the same way as any other instance. **Enterprise only.** #### Instance tunneling Not every port an instance listens on belongs on the public internet. Think of a database, an admin endpoint, or a debug server. `unikraft instance tunnel` forwards a local port to a port on an unexposed instance, through an intermediate TLS tunnel service: ```sh # Forward local port 8080 to instance "nginx" port 8080 unikraft instance tunnel nginx:8080 # Pick the local port yourself unikraft instance tunnel 8333:nginx:8080 # Several ports, several instances, one command unikraft instance tunnel 8080:my-instance1:8080/tcp 8443:my-instance2:8080/tcp ``` Targets take the form `[LOCAL_PORT:]:DEST_PORT[/TYPE]`. The command creates the tunnel proxy instance for you and deletes it again when it exits, so nothing stays running behind you. `port-forward` works as an alias for anyone arriving from `kubectl`. See [`unikraft instance tunnel`](/cli/unikraft/instances/tunnel). **Breaking changes:** None. The command comes across from `kraft cloud`. **Requirements and limitations:** The node must permit internal traffic between VMs (`--net-internal-traffic=vm`). The tunnel goes away when the command exits, so it's a foreground tool rather than a persistent route. **Why it matters:** Reaching an unexposed port used to mean publishing it temporarily, which is exactly what you were trying to avoid. This closes one of the last gaps against `kraft cloud` and makes the safe option the convenient one. #### Comparison operators in filters `--filter` used to test equality and nothing else, which suits states and names and suits nothing you can measure. It now understands the four comparison operators across every resource the CLI lists: ```sh unikraft instance list --filter "resources.memory>128mib" unikraft volume list --filter "size<=1gib" ``` Timestamps compare too, in absolute or relative form, so "everything created in the last week" becomes a filter rather than a script: ```sh unikraft instance list --filter "timestamps.created<7d" ``` Sizes, memory, vCPUs, quotas, and limits all work with all four operators, and the comparison follows the field's own type rather than string ordering. See [filtering](/cli/fields#filtering) and the [filter expression reference](/cli/fields#filter-expression-reference). **Breaking changes:** None. `=` behaves as it always did. **Why it matters:** Filtering is what makes a listing usable once an account holds more than a screenful of resources. Without comparisons, questions as ordinary as "which volumes are almost full" or "what appeared today" needed JSON piped into something else. #### Volume usage bars Listing volumes now reports how full each one is, not only the size it started with: ```text METRO NAME STATE USAGE FREE SIZE CREATED dal2 cloud-jetstream-data mounted 80% ⣿⣿⣿⣿⣿⣿⣿⣿⣀⣀ 25MiB 128MiB 4 weeks ago ``` The bar takes its color from how close the volume is to full, so a table reads at a glance. The `usage` field is real data rather than decoration, and it sorts and filters by the used/total ratio. Machine-readable output renders it as a plain `used/total` pair rather than braille. Quotas use the same display. **Breaking changes:** None. The listing gains a `USAGE` column, and keeps `FREE` and `SIZE` as they were. **Why it matters:** A volume filling up stays invisible right up until it isn't. The listing used to report only the size the volume started with, and finding out how much room remained meant looking somewhere else. #### Nested JSON for the `unikraft api` command [`unikraft api`](/cli/unikraft/api) is the escape hatch for anything the CLI doesn't model yet, but its `-d` flag used to take a complete JSON body, which meant hand-writing and quoting JSON on the command line. It now also accepts nested-JSON syntax: ```sh unikraft api /v1/volumes name=data size_mb:=256 unikraft api /v1/something 'tags[]=production' 'tags[]=eu-west' ``` `key=value` sets a string, and `key:=value` sets a raw JSON value. The `[]` and `.` markers build arrays and nested objects. The generated command documentation covers the syntax. **Breaking changes:** None. Passing a full JSON body to `-d` works exactly as before. **Why it matters:** The escape hatch sees the most use when something is urgent, which is the worst time to count quotes and braces. Ad-hoc API calls become something you type correctly the first time. ### Dashboard #### ROMs, templates, and checkpoints The dashboard shows the objects that branching and checkpointing produce. Instances gained templates and checkpoints views, each with a detail page. A checkpoint's page shows its lineage, so you see the checkpoint's source and everything descending from it. An instance's ROMs appear under its mounts tab. **Breaking changes:** None. **Requirements and limitations:** Templates and checkpoints require a licensed platform, so the views stay empty on installations without them. **Why it matters:** Branching and checkpointing turn an instance from one object into a small family tree, and a list of instance names is no way to understand one. Seeing the lineage is most of what makes the feature usable by anyone who didn't create the checkpoints. #### Consistent lists, navigation, and controls Every resource list now searches, sorts, filters, and pages the same way. A list's search, sort, and page position live in the address. Bookmark a filtered view, or send it to a colleague, and the same view comes back. Filtering came with it: instances by their own attributes, volumes by metro and state, and attachments by what they attach to. A filter marks itself as active while it holds a selection, so a list that looks short because of a filter says so. Live updates hold up better too, and a metro that fails to answer no longer stops the rest of the dashboard from updating. The title bar carries a breadcrumb trail, so a page says where it sits and offers a way back up. The trail used to appear only on narrow screens with a bare link to the organization root. It's now always there, which also lets the sidebar collapse to an icon rail at any width, and the sidebar remembers whether you left it collapsed. Image manifests get a crumb per hop rather than collapsing back to the image root, so digging into a manifest list and back out again works. Links between pages load in place rather than reloading everything, the document title keeps up as you navigate, and a 404 keeps its sidebar. The organization page moved into tabs and remembers which tab you were on across a reload. Account details went the same way and now lists your memberships. The instance page now organizes around the two questions people open it with, namely how to reach this instance and why it stopped, rather than one growing column of cards. Dialogs and buttons look and behave consistently across the dashboard, buttons grey out while they work, dropdown text is legible, and charts always show rather than hiding behind a toggle. **Breaking changes:** None. **Why it matters:** Resource lists are where most dashboard time goes, and inconsistency between them reads as unreliability. You stop trusting that sorting a column does what it did on the last page. --- ## Integrations ### Karpenter provider [Kraftlet](/integrations/kubernetes) lets a Kubernetes cluster schedule pods onto Unikraft, but the cluster still had to hold the nodes to schedule them onto. The new [Karpenter provider](/integrations/karpenter) closes that loop. When a pod finds no room, the provider provisions a node, installs Kraftlet onto it, and joins it to the cluster. It removes the node again once the capacity is no longer needed. A node class describes what a provisioned node looks like. It covers the machine type, whether nested virtualization applies, images to pre-pull so a new node doesn't arrive cold, and any custom user data to run. ARM and x86 nodes come from the same configuration. The provider supports Karpenter's capacity buffers for keeping warm headroom, and replaces a node whose class changed underneath it. A node joins the cluster only once the platform on it reports healthy, so pods don't land on a node that's still coming up. **Breaking changes:** None. This is a new component. **Requirements and limitations:** Preview. Requires a Kubernetes cluster running Kraftlet, and credentials for the node provisioning API. **Why it matters:** Without it, a Kraftlet cluster scales only as far as the nodes someone remembered to provision. This makes Unikraft capacity elastic from inside Kubernetes, driven by the autoscaler Kubernetes users already run. **Enterprise only.** ### Plugin SDK for Go The [Go plugin SDK](/sdks/plugin) wraps the instance plugin system. A plugin is a sidecar HTTP server that the platform loads next to your main app, from its own ROM image, and reaches over a per-instance, authenticated endpoint. Writing a plugin by hand means re-implementing the same boilerplate every time. That covers parsing the init command line, adopting the socket the platform hands you, and decoding the JSON configuration from STDIN. It also covers standing up a router, wiring graceful shutdown, and integrating scale-to-zero. The SDK does every part of that. You write a configuration declaration and a route registration function, and a complete plugin comes to about 20 lines: ```go package main import ( "context" "github.com/gin-gonic/gin" "unikraft.com/cloud/pluginsdk" ) // Config is populated from the platform `config` JSON on STDIN (by its `json:` // tag), then overridden by environment variables and command-line flags. type Config struct { Greeting string `json:"greeting" default:"Hello"` } func main() { pluginsdk.Main(&pluginsdk.Plugin[Config]{ Name: "example", Register: func(ctx context.Context, cfg *Config, engine *gin.Engine) error { engine.GET("/hello", func(g *gin.Context) { data := gin.H{"message": cfg.Greeting + ", world!"} env, code, _ := pluginsdk.OK(&data) g.JSON(code, env) }) return nil }, }) } ``` A plugin serves an OpenAPI contract, and the `sdkgen` tool renders the matching client-side Go SDK from it as a build artifact. The two version together, so callers get a typed client rather than hand-rolled HTTP calls: ```sh go run unikraft.com/cloud/pluginsdk/tools/sdkgen@prod-staging \ -i openapi.yaml \ -o ./sdk ``` **Breaking changes:** None. This is a new component. **Requirements and limitations:** Go is the only language in this release, and further SDKs follow. **Why it matters:** The plugin substrate has been reachable for a while, but only by re-implementing the same startup contract every time. Getting any part of it wrong shows up as a plugin the platform won't talk to. Handing that to an SDK reduces a plugin to the twenty lines that are specific to it. ### Sandbox plugin Sandboxes now ship as a ready-made [plugin](/features/plugins#sandbox-plugin), alongside the [JavaScript SDK](/sdks/js). **Breaking changes:** None. This is a new component. **Why it matters:** Running untrusted code per user session no longer means assembling the plugin yourself. **Enterprise only.** --- [Back to all releases](/releases) --- ## Document: Release 12: Thebe Unikraft Cloud Release 12 (Thebe) release notes. URL: /docs/releases/r12-thebe # Release 12: Thebe **Released:** 2026-08-04 :::note Read the [Release 12: Thebe announcement](https://unikraft.com/blog/thebe-12-release) on the Unikraft blog. Watch the [Release 12: Thebe video](https://www.youtube.com/shorts/-isk8d6wyqo) on YouTube. ::: --- Release 12 introduces GPU support for enterprise customers, shared instance templates across nodes, and a set of image management features including pinning, strict authorization, and pull timeouts. On the tooling side, the dashboard adds user invitations and richer instance and volume pages, and Kraftlet gains CNI plugin support. --- ## Platform ### GPU support The platform now supports NVIDIA GPUs. You select the instance type and GPU count when creating an instance via `POST /v1/instances`: ```json { "name": "my-gpu-instance", "type": "full", "gpus": 1 } ``` `type` can be `micro` (default) or `full`. Adding `"gpus": 1` requests a GPU for the instance. The platform picks a free GPU from the host and keeps it assigned for the instance's entire lifetime, including while it isn't running. The platform releases the GPU when you delete the instance. Running GPU workloads requires a guest kernel built with NVIDIA driver support, and the host must allow GPU passthrough. **Breaking changes:** Instances default to `micro`, so existing workloads aren't affected. **Requirements and limitations:** GPU support is in its first iteration. At most one GPU per instance, and selecting a specific GPU isn't yet possible. Block-based volumes work, but virtiofs volumes don't. GPU VMs report as `RUNNING` immediately and forward network traffic regardless of whether the guest is listening. Requires a valid license. GPU snapshots are on the roadmap for the next release. **Enterprise only.** ### Shared instance templates [Instance templates](/features/on-demand-templates) used to be node-local: every controller had to build its own copy before cloning instances from it. You can now publish templates to a directory shared between controllers: ```sh ukpd --vm-shared-template-dir /mnt/shared/templates ... ``` With this option, the node publishes every named template it creates as a self-contained bundle. The export runs in the background, so the request returns as soon as the template is ready locally. A bundle carries the template, its dependencies (volumes, ephemeral images, parent templates), the memory snapshot, and the volume files. The importing node pulls registry images by address as usual. Referencing a template by name now consults the shared directory. When `POST /v1/instances` names a template that isn't present locally, the platform imports the newest matching bundle. The platform replaces an existing local template when a newer bundle is available. Templates key on name and owner, so the platform only imports a bundle for the user that exported it. The platform versions bundles rather than overwriting them, and readers select the newest version. `--vm-template-gc-threshold-s` (default 6h) sets how long the platform retains older versions after a new export. `--vm-template-recheck-interval-s` (default 10s) throttles how often the platform checks the directory for updates. **Requirements and limitations:** All nodes must see the same directory (for example, a shared network filesystem) and know the same users. The platform only exports named, non-ephemeral, fully converted templates with block-based volumes. Reflink support (XFS) is strongly recommended for performance. **Why it matters:** Building a template is expensive. You can now build a template once and have it materialize on every other node on first reference. Scaling out to a fresh node no longer means a cold boot-and-snapshot cycle, and rolling out a new version is a single export. **Enterprise only.** ### Image pinning Two new endpoints, `POST /v1/images` and `DELETE /v1/images`, let you pull an image and pin it to a node ahead of time: ```json { "url": "user/image:latest", "timeout_s": 60 } ``` `timeout_s` lets you wait until the image is ready. If the platform can't pull the image, the API response contains the underlying error. [`GET /v1/images`](/platform/images) now also reports the state of each image: ```json { "state": "ready", "persistent": true, "url": "..." } ``` `state` is either `pending` (the platform is pulling it) or `ready` (usable). `persistent` is `true` for pinned or operator-defined images. Pinned images behave like operator-defined images, but you define them at runtime. The platform doesn't release either type automatically when no longer in use. You unpin an image with `DELETE /v1/images?uuid=X`. **Requirements and limitations:** Requires the `image_manager` permission. You must provide `timeout_s`. **Why it matters:** Pinning turns image availability into something you control explicitly. Operators running many regions can pre-stage a new image version everywhere and only then switch traffic to it. This removes on-demand pulls from the critical path of starting an instance. **Enterprise only.** ### Strict image authorization The platform caches image access permissions per user, so referencing the same image twice normally triggers only a single access check. For customers who operate their own registry with distinct registry users behind a single platform user, one authorized user's access can leak to another. The new controller option `--images-always-authorize` closes this gap by requiring valid credentials every time you specify an image. **Breaking changes:** None by default. The option is opt-in. When enabled, you can only use the `never` pull policy with pinned or operator-defined images. **Enterprise only.** ### Image pull timeout Image requests now have a timeout, configurable with `--images-request-timeout-s` (default 60 seconds, 0 disables it). On timeout, the instance moves to `STOPPED`. **Breaking changes:** Instances that would hang in `STARTING` now end up in `STOPPED` after 60 seconds by default. **Why it matters:** A failed pull now surfaces as a terminal state instead of a stuck instance. This lets error handling upstream react rather than waiting on a request that won't complete. **Enterprise only.** ### Image request merge control When an image isn't yet in `READY` state and you reference it with differing credentials, each distinct reference produces its own request. This release raises the per-image limit on parallel requests from 16 to 64. A new option on the image object controls whether a reference merges with existing ones: ```json { "url": "...", "merge_requests": false } ``` **Breaking changes:** `merge_requests` defaults to enabled, which differs from the previous behavior. **Requirements and limitations:** Setting `merge_requests` requires the `image_manager` permission. **Enterprise only.** --- ## Tooling #### Heartbeat signals Licensed Unikraft Cloud Platform installations now report regular health information to the central control plane. This currently includes simple, timestamped health data about the system. **Enterprise only.** #### Improved image pruning reliability Image pruning (introduced in Release 11) keeps disk space usage down, but a few configurations could cause unexpected behavior. This release fixes those edge cases. **Enterprise only.** ### Dashboard #### Organization user invitations Organization admins can now invite new team members directly from the dashboard. Create an invite link for a new admin or member and share it through your preferred channel. #### Improved instance and volume pages The instance and volume pages now show more information, making it faster to diagnose errors and understand the scale of your resources at a glance. ### Kraftlet Kraftlet is the Unikraft Cloud [integration for Kubernetes](/integrations/kubernetes), allowing you to run microVMs as Kubernetes pods. #### CNI plugin support Kraftlet now supports Container Network Interface (CNI) plugins for configuring the networking stack of Unikraft-backed pods. This allows integration with different networking solutions from vendors like AWS, Tigera, and Isovalent. A separate component, `remote-cni`, allows invoking CNI plugins via a gRPC service when Kraftlet isn't collocated with the platform. **Enterprise only.** --- [Back to all releases](/releases) --- ## Document: Release 11: Metis Unikraft Cloud Release 11 (Metis) release notes. URL: /docs/releases/r11-metis # Release 11: Metis **Released:** 2026-07-03 :::note Read the [Release 11: Metis announcement](https://unikraft.com/blog/metis-11-release) on the Unikraft blog. Watch the [Release 11: Metis video](https://www.youtube.com/shorts/L3AkA9s3e2c) on YouTube. ::: --- Release 11 is a major step forward for the platform. It brings advanced snapshotting capabilities, including branching and checkpointing, alongside custom filesystems for persistent storage. A plugin API extends a microVM with your own code. Enterprise customers already have access to a sandbox plugin built on it. Also new is the network shield. It sits between any microVM and the public Internet, filtering traffic and injecting tokens or secrets so they never have to live inside the microVM itself. The shield is a minimalist, scale-to-zero microVM, as isolated as everything else you run. All features in this release are available to enterprise customers now and coming to the hosted platform soon. Below, the notes cover platform features and the tooling that supports them. --- ## Platform ### Branching Branching clones an existing instance (microVM), including its full memory and volume state, by referencing a source instance in the `POST /v1/instances` call: ```json { "name": "my-branch", "branch_from": { "name": "my-instance" } } ``` As with creating an instance from a template, you can add ROMs, attach volumes, and configure services on the branch. The source VM experiences at most a few milliseconds of downtime when you take a branch, rather than pausing during a copy. **Breaking changes:** None. This is an additive capability. **Requirements and limitations:** Branching supports block-based volumes (for example, `ext4`). Requires a valid license. **Why it matters:** Branching is the foundation for stateful instance workflows on the platform. It enables fast, low-downtime cloning of live workloads for testing, scaling, and recovery. It applies to many use cases, including launching sub-agents or branching large production databases to develop on them without risk. **Learn more:** [Branching](/features/branching) ### Checkpointing Checkpointing captures the state of a microVM (running or in any other state), including its full memory and volume state, as a named, restorable checkpoint. You can take a checkpoint of a running instance without stopping it, keep a history of successive checkpoints, and create new instances from any checkpoint later on. Create checkpoints via `POST /v1/instances/checkpoints`: ```json { "name": "my-checkpoint", "from": { "name": "my-instance" } } ``` A checkpoint enters a `starting` state while the platform creates it, then transitions to a `checkpoint` state. If you omit a name, the checkpoint inherits the instance name plus a random suffix. The API provides a complete set of management endpoints: - **Per-instance history:** `GET /v1/instances/history?name=my-instance` - **Per-checkpoint lineage:** `GET /v1/instances/checkpoints/history?name=my-checkpoint` - **Checkpoint details:** `GET /v1/instances/checkpoints?name=my-checkpoint` - **List all checkpoints:** `GET /v1/instances/checkpoints` - **Delete:** `DELETE /v1/instances/checkpoints?name=my-checkpoint` - **Patch** (delete-lock, tags, autokill): `PATCH /v1/instances/checkpoints` Checkpoints support an `autokill` property (`{ "time_ms": X }`) that removes a checkpoint automatically if you haven't loaded it within the given time window. Deleting an instance that owns checkpoints doesn't remove those checkpoints. You load a checkpoint by creating a new instance from it via `POST /v1/instances`, configuring services, extra volumes, and ROMs on the new instance as you would with branching and templates: ```json { "name": "my-new-instance", "checkpoint": { "name": "my-checkpoint" } } ``` Coming soon: fully rewindable VMs that you can rewind to an arbitrary checkpoint without spawning a new one. **Breaking changes:** None. This is an additive capability. **Requirements and limitations:** Requires a valid license. Checkpointing only works with block-based volumes (for example, `ext4`). You load checkpoints by creating a new instance from them. In-place rewind of an instance to a previous checkpoint isn't part of this release. **Why it matters:** Checkpointing gives you low-latency, point-in-time recovery and reproducible starting states for running workloads. This helps with debugging, experimentation, and rollback. **Learn more:** [Checkpoints](/features/checkpoints) ### Custom filesystems and persistent volumes Unikraft Cloud supports defining custom filesystems with custom hooks for operations like format, mount, unmount, and delete. This enables volumes that you can share across a cluster, and so persistent volumes for use cases such as running ephemeral agents whose state lives in volumes. That state then stays available across the nodes or servers in a cluster, for example shared storage backed by S3 with a POSIX interface like JuiceFS provides. **Breaking changes:** None. This is an additive capability. **Requirements and limitations:** None. **Why it matters:** Enables cluster-level, shared filesystem capabilities for agentic and other use cases. **Learn more:** [Volumes](/platform/volumes#filesystems) ### Custom network configuration You can now create microVMs with many network interfaces and fully custom network configuration via `POST /v1/instances`: ```json { "network_interfaces": [ { "name": "my-instance-private" }, { "name": "my-instance-public", "ip": "192.168.0.1/30", "tap_name": "vpc0", "autoconfig": true } ], "gateway": "192.168.0.100", "nameserver": "1.2.3.4" } ``` The first interface is the **primary interface**: the `private_ip` that the instance returns always comes from it, and the proxy forwards traffic to the primary interface's IP. If you omit `ip` and `tap_name`, the platform chooses a TAP device from its pool. You must specify both fields or neither. Interface names are user-global rather than instance-local, so another instance can reference your interface as its relay. If you omit `name`, the platform autogenerates it as `-ethX`, falling back to `eth-`. `autoconfig` (default `true`) controls whether the guest kernel configures the interface or leaves it to the app/image, independent of the TAP device used. An empty `"network_interfaces": [ {} ]` is valid and allocates one pool interface with an autogenerated name, the same as omitting the field. When you don't specify `gateway` or `nameserver`, the platform derives them from the relevant interface's IP network. **Breaking changes:** None. The controller detects whether the guest kernel supports the feature, so existing images and workloads keep working. **Requirements and limitations:** Up to 4 interfaces per instance. Requires the latest `base-compat`, the `net_manager` permission, and a valid license. This release doesn't support PATCH for network configuration. **Why it matters:** Custom network interfaces enable real VPC-style topologies, such as public/private separation and custom gateways and nameservers. They also form the foundation that the network shield builds on, which makes them essential for secure sandbox networking. **Learn more:** [Custom network configuration](/features/custom-network-configuration) ### Network shield and relay Relay redirects all network traffic to and from a network interface toward another interface. This lets you set up a relay VM and configure its interface as the relay for another VM's interface via `POST /v1/instances`: ```json { "name": "my-sandbox", "network_interfaces": [ { "relay": { "name": "my-relay-iface", "relay_dns": true } } ] } ``` A network shield is one example of a relay. It sits between a microVM and the public Internet, filters traffic, and can inject credentials or secrets for agentic and other use cases. Relay is a core part of the sandbox support in Unikraft Cloud. A typical topology pairs a sandbox instance with a shield microVM that scans all traffic and injects secrets. The sandbox never has unfiltered network access and never holds any secrets itself. All traffic passes over the relay, and many VMs can share one. Unikraft Cloud supports managed interfaces with custom TAP devices, and you don't need to enable VM-to-VM communication (which you wouldn't want in a sandbox setup anyway). The relay can handle DNS requests, or the platform's default DNS server can answer them via `relay_dns` (default `true`). This simplifies setups that don't run a DNS server in the relay VM. A guest acting as a relay should set `net.ipv4.ip_forward=1` and `rp_filter=0`. **Breaking changes:** None. This is an additive capability built on the custom network configuration. **Requirements and limitations:** Requires the latest `ukp-networking` package and a reconfiguration of the firewall and TAP devices. As part of the custom network configuration, that feature gates relay. **Why it matters:** Shield and relay make a genuinely secure sandbox possible. They centralize traffic inspection and secret injection in a controlled gateway, which keeps untrusted workloads isolated from secrets and from unfiltered network access. Each shield runs as a separate microVM, so every shield instance stays fully isolated from the others. You get isolation for your main workload (such as sandboxes, browsers, and databases) and for the shield protecting it. **Learn more:** [Network shield and relay](/features/network-shield) ### Plugin API The plugin API powers the native sandbox experience. A plugin runs a small HTTP server that accepts commands for execution in the VM, offers filesystem services, and more. The mechanism is general and supports other plugin types as well. Unlike a regular service, plugin HTTP access uses the same authentication as a normal API call, requires no service group, and talks to the instance directly. When you enable scale-to-zero, the request wakes the instance, which stays up for the request's duration, exactly like a normal request. The plugin API loads plugins into an instance and exposes each plugin over an authenticated address of the form `https://api..unikraft.cloud/v1/instances//plugins//*`. The platform forwards everything after the plugin name (`*`) to the plugin as the path. You declare plugins at instance creation time via `POST /v1/instances`, where `config` accepts arbitrary JSON: ```json { "name": "my-instance", "plugins": [ { "name": "my-plugin", "image": "user/myplugin:latest", "config": {} } ] } ``` A plugin is a standard ROM image (see the docs) with an executable named `init` in its root. The platform loads and mounts it at `/uk/plugins/`, starts it, and passes its `config` via STDIN. It also passes an `--api_fd X` argument that carries the socket file descriptor for accepting connections. The controller assigns the socket port on a best-effort basis within a configurable range (`--vmm-plugin-port-start`, default 20000, and `--vmm-plugin-port-end`, default 29999). It skips ports already in use by the guest. Plugin names can be up to 63 characters and contain only `a-z`, `A-Z`, `0-9`, `-`, and `_`. The PATCH endpoint fully supports plugins. They load on the scale-to-zero, suspend, and restart cycle like ROMs, and an instance inherits them when you clone it via branching or checkpointing. **Breaking changes:** None. This is an additive capability. **Requirements and limitations:** Requires the latest `base-compat` and a valid license. You can load up to 8 plugins per instance. Plugin unloading arrives in the next release. **Why it matters:** The plugin API delivers a powerful, native sandbox experience: in-VM command execution and filesystem services over an authenticated channel, without exposing an unauthenticated service. It also provides a general extension point for custom in-instance functionality that applies to any use case. Dashboard support for the plugin API is coming soon. **Learn more:** [Plugins](/features/plugins) --- ## Tooling #### Image pruning The platform now automatically prunes unused images from disk. When no running or stopped instance references an image, the platform garbage-collects it to reclaim space. If you need the image again later, the platform pulls it on demand from the registry. **Breaking changes:** Images that you push directly to the [local registry](/cli/registries) now become eligible for pruning, so don't treat them as long-term storage. Push images you want to keep to the global registry instead. **Requirements and limitations:** Pruning applies only to images that no instance currently uses. **Why it matters:** Prevents hosts from filling their disks over time in environments with frequent image rebuilds or updates, which reduces operational toil and avoids unexpected failures from exhausted storage. **Learn more:** [Unikraft Cloud documentation](https://unikraft.com/docs/introduction) #### Improved health checks The `/v1/healthz` endpoint now supports more health checks, including admin-defined checks for workflows like marking a node as not ready during upgrades or draining. With root access, it can also include more metadata, such as version information and license status, to simplify remote inspection. **Breaking changes:** None. **Requirements and limitations:** The endpoint returns some fields only with elevated or root access. **Why it matters:** Makes it easier to automate operations and diagnose issues across a fleet using a single, scriptable health endpoint. **Learn more:** [Unikraft Cloud documentation](https://unikraft.com/docs/introduction) ### CLI #### Follow logs on start and restart `unikraft` already supported following logs for instances that you create and start via `unikraft run`, but `unikraft start` and `unikraft restart` didn't. This release adds `--follow` consistently across these commands. **Breaking changes:** None. **Requirements and limitations:** Log streaming depends on the platform's logging support for the instance. **Why it matters:** Removes a small but common workflow inconsistency and reduces friction when you check a start or restart. **Learn more:** [Unikraft Cloud documentation](https://unikraft.com/docs/introduction) ### Dashboard #### Revamped dashboard A powerful product deserves a beautiful interface, and the dashboard now has a fully revamped one. You can now check real-time metrics for all your resources and inspect instance logs. You can start and stop instances directly from the UI and manage your organization, including user management. Coming soon, you'll also be able to SSH directly into a particular instance and create or delete instances. You'll be able to inspect the lifetime of an instance as you checkpoint or fork it. **Breaking changes:** None. **Requirements and limitations:** None. **Why it matters:** Improves usability today and sets up a stronger UI foundation, so future dashboard features can ship faster and more consistently. #### Online enterprise documentation Enterprise documentation has moved from a static, periodically distributed document to a centralized online docs portal for enterprise customers. **Breaking changes:** None. **Requirements and limitations:** Only enterprise customers can access it. **Why it matters:** Keeps documentation current, makes updates available immediately, and provides a single source of truth for enterprise operators. --- [Back to all releases](/releases) --- ## Document: Release 10: Themisto Unikraft Cloud Release 10 (Themisto) release notes. URL: /docs/releases/r10-themisto # Release 10: Themisto **Released:** 2026-06-05 --- {/* vale off */} Release 10 focuses on instance lifecycle management and observability. New capabilities include runtime user management, instance annotations, hotpluggable volumes for scale-to-zero instances, indefinite instance retention with post-mortem metrics, and the ability to wake VMs from TAP device traffic. On the tooling side, the CLI gains a new `unikraft api` passthrough and EROFS rootfs by default, while Kraftlet picks up full CSI integration and ROM-based ephemeral volumes. {/* vale on */} --- ## Platform ### Image tag response format change The `GET /v1/images` and `GET /v1/images/list` endpoints now return only the actual OCI tag. For an image such as `oci://unikraft.io/user/image:latest`, the response used to contain `user/image:latest` and now contains `latest`. Update any clients that parse the `tag` field. ### Retained instance state renamed The lifecycle state for logically removed but still-retained instances changed from `dead` to `deleted`. Update any monitoring, automation, or filtering logic that matches on the old state name. ### User management at runtime via userdb A new user database (`userdb`) makes it possible to manage users at runtime rather than only at startup. This work also made internal database entry replacements atomic, improving robustness. This release removes the "no authorization" mode. Deployments must now run with authorization enabled. ### Instance annotations [Instances](/platform/instances) can now carry user-defined [annotations](/features/annotations), which you can also include in the instance log output. This makes it easier to correlate instance behavior with deployment-specific metadata such as environment, tenant, or release identifiers. ### Hotplug volumes for scale-to-zero and template-derived instances You can now hotplug new [volumes](/platform/volumes) to instances during [scale-to-zero](/features/scale-to-zero) cycles and add volumes to instances created from [templates](/features/on-demand-templates). This allows storage configuration to evolve over the lifetime of an instance without recreating it. ### Indefinite retention of instances You can now keep instances indefinitely instead of enforcing a time-based limit. This is useful for keeping post-mortem state available for longer investigation windows. The instance `DELETE` endpoint now accepts a `dont_retain` flag that removes a retained instance immediately instead of keeping it around for the configured retention window. This gives operators a direct way to reclaim resources held by retained instances. ### Metrics endpoint available for retained instances You can now query the `/v1/instances/metrics` endpoint for retained instances. Operators can continue to export [metrics](/platform/metrics) for instances that the platform still retains after removal, which is useful for debugging and accounting. ### Image pull policy forwarding The image pull policy configured in the API is now forwarded to the host platform. This allows eliminating image pull latency when you use the `if_not_present` policy together with background image updates configured in the host platform. ### Merge image default environment with instance environment When starting an instance, the environment variables defined as defaults on the image are now merged with any instance-level environment variables. The instance-level variables no longer fully replace the defaults. This change also fixes a problem where environment variables weren't escaped correctly. ### Image fetch errors propagated to the proxy Image fetch failures now reach the proxy layer. Clients triggering an instance via the proxy receive a more meaningful response when the platform can't pull the underlying image, instead of a generic failure. ### Wake VMs from TAP device traffic Incoming traffic on a TAP device can now wake instances, broadening the set of wake-up sources beyond the existing trigger mechanisms. This allows private VM instances without a service group to have scale-to-zero configured and wake up when needed. ### Configurable database storage location A new `--db-path` option lets operators choose where the platform stores its databases. This makes it easier to place the databases on dedicated storage, separate from logs or other state. --- ## Tooling ### CLI #### New `unikraft api` subcommand `unikraft api ` issues an authenticated HTTP request to the Unikraft Cloud API, reusing the current profile, default metro, and credentials. Use it for endpoints that don't yet have a dedicated subcommand, and for exploration and scripting. ```sh # List instances in the default metro unikraft api /v1/instances # Inspect a specific instance unikraft api /v1/instances/abc123-...-def456 # Create a 256MB volume in fra unikraft api /v1/volumes --metro fra -d '{"name":"data","size_mb":256}' # POST a request body from a file or stdin unikraft api /v1/volumes -d @volume.json cat volume.json | unikraft api /v1/volumes -d @- # Delete an instance unikraft api /v1/instances/abc123-...-def456 -X DELETE ``` #### EROFS rootfs by default `unikraft build` now produces EROFS root filesystems by default. EROFS images are smaller and faster to mount than the CPIO archives used before, which translates to lower cold-start latency and reduced per-image storage cost. The runtime's advertised feature flags drive the selection: if every target platform reports EROFS support, the builder picks EROFS. Otherwise it falls back to CPIO. Existing Kraftfiles continue to work without changes. #### Kraftfile schema updates Two changes have landed in the v0.7 Kraftfile schema. `rootfs.format` no longer defaults to `cpio` at parse time. Before this change, the parser rewrote a `rootfs` without an explicit `format` to `format: cpio`. The parser now leaves the field empty and lets the tooling pick a format based on the target runtime's advertised capabilities. Set `format:` explicitly if you need to pin one. ```yaml spec: v0.7 targets: - kraftcloud/x86_64 rootfs: source: ./rootfs # format: cpio # set explicitly to pin; previously implied ``` New optional `dockerfile:` field on `rootfs.source`. A Dockerfile-backed rootfs can now point at a Dockerfile independent of its build context, matching the behaviour of `docker build -f`. ```yaml spec: v0.7 targets: - kraftcloud/x86_64 rootfs: source: type: dockerfile path: . dockerfile: ./custom/path/Dockerfile ``` #### ROM support ROMs provide read-only image volumes that many instances can share at low overhead. The CLI can now build them, attach them at instance creation time, and edit them on existing instances. ```sh # Attach a ROM image at /rom on a new instance unikraft instance create \ --name demo \ --metro fra \ --image nginx:latest \ --rom image=myuser/my-rom:latest,at=/rom,name=docs # Attach an inline ROM built from a local directory unikraft instance create \ --name demo \ --metro fra \ --image nginx:latest \ --rom dir=./mydata,at=/rom # Add a ROM to an existing instance unikraft instance edit demo --rom image=myuser/my-rom:latest,at=/rom2 ``` ROMs are also surfaced as fields on `unikraft instance get`/`list` under `roms.*.{name,image,dir,at}`. #### `deleted` Instance state The CLI now understands the new `deleted` lifecycle state for instances that the platform has logically removed but still retains. You can see them in `unikraft instance list` and filter for them explicitly. ```sh unikraft instance list --filter 'state==deleted' ``` #### Volume access modes `unikraft volume create` now accepts an `--access-mode` flag that controls how many instances can share a volume. ```sh # Read-write, single attachment (default) unikraft volume create --name data --size 10GiB --access-mode rwo # Read-only, may be attached to many instances unikraft volume create --name assets --size 1GiB --access-mode rox # Read-write, may be attached to many instances unikraft volume create --name shared --size 1GiB --access-mode rwx ``` #### `--rm` Flag on `unikraft instance create` `unikraft instance create --rm` marks the VM for automatic deletion once it exits. This matches `docker run --rm` and is the preferred form for one-shot and CI workloads. ```sh unikraft instance create \ --name oneshot \ --metro fra \ --image my-job:latest \ --rm ``` #### `idle` Scale-to-zero policy `--scale-to-zero` now accepts `policy=idle` in addition to `on` and `off`. The platform scales idle instances down after `cooldown-time` of no activity, without requiring an explicit stateful/stateless distinction. ```sh unikraft instance create \ --name web \ --metro fra \ --image nginx:latest \ --scale-to-zero policy=idle,cooldown-time=30000 ``` #### Insecure registries `unikraft build` and `unikraft images` now accept `--insecure` to opt into HTTP or unverified-TLS connections to specific registries, for use with internal mirrors and self-signed deployments. ```sh unikraft build . --output my-org/my-app:latest --insecure registry.internal unikraft images pull my-org/my-app:latest --insecure ``` ### KraftKit #### Kraftfile schema v0.7 KraftKit picks up the v0.7 schema changes: `rootfs.format` is no longer defaulted at parse time, and a new optional `dockerfile:` field can point at a custom Dockerfile path. #### Fetch images from per-metro image stores `kraft cloud image list` and image-pull flows now also consult per-metro image stores alongside the global registry. When a metro has already cached an image locally, the pull resolves against it directly, shortening cold-start image pulls for clients colocated with the metro. ```sh # Lists images visible in the fra metro, including its local store kraft cloud image list --metro fra ``` ### Kraftlet Kraftlet is the Unikraft Cloud [integration for Kubernetes](/integrations/kubernetes), allowing you to run microVMs as Kubernetes pods. #### CSI integration Kraftlet now integrates with the Kubernetes CSI to provision and attach volumes from arbitrary storage backends to Unikraft Cloud instances. Kraftlet stages volumes by calling the appropriate CSI plugin based on the PVC's storage class, then publishes them to the VM via Unikraft Cloud managed volumes. The Helm chart now exposes the CSI endpoint and supports setting the required node annotations. ```yaml # values.yaml csi: endpoint: unix:///var/lib/kubelet/plugins/csi.example.com/csi.sock nodeAnnotations: volumes.kubernetes.io/controller-managed-attach-detach: "true" ``` #### Ephemeral volumes as ROMs `configMap`, `secret`, and projected volumes are now mounted as inline ROMs instead of going through the previous import flow. Pod startup no longer has to wait for an import instance to populate the volume contents, which reduces start time for pods that mount ConfigMaps or Secrets. #### Resource tagging Kraftlet tags all Unikraft Cloud resources it creates with their origin, including the Kraftlet node name, originating Kubernetes namespace and resource name, and CSI plugin and storage class where applicable. This allows attributing Unikraft Cloud resources back to their Kubernetes owners without maintaining a separate mapping. #### Custom node annotations Kraftlet can advertise arbitrary annotations on its Kubernetes node object, configured through the Helm chart. This underpins the CSI controller-managed-attach annotation and allows operators to attach cluster-side metadata to the Kraftlet node. --- [Back to all releases](/releases) --- ## Document: Release 1: Ganymede Unikraft Cloud Release 1 (Ganymede) release notes. URL: /docs/releases/r1-ganymede # Release 1: Ganymede Public release notes begin with [Release 10](/releases/r10-themisto). The team shared release notes for Release 1 (Ganymede) and earlier directly with customers. See the [releases overview](/releases/index) for the full list of releases. --- ## Document: Releases Release notes and changelog for Unikraft Cloud. URL: /docs/releases/index # Releases This section tracks all releases of Unikraft Cloud, including new features, improvements, and bug fixes. The team shared release notes for versions before Release 10 directly with customers. Public release notes begin with Release 10. {/* vale off */} Subscribe to the RSS feed to get notified of new releases. {/* vale on */} | Release | Date | Summary | |---|---|---| | [Release 15 — Isonoe](/releases/r15-isonoe) | 2026-11-04 | Live migration, Windows VMs, template updates, account suspension, CPU and memory-time accounting, a Windows CLI, a dashboard CLI terminal and self-service billing, and network shields for Kraftlet pods. | | [Release 14 — Harpalyke](/releases/r14-harpalyke) | 2026-10-05 | Vertical scaling, GPU snapshots and NVIDIA vGPU, scale-to-zero for full VMs, sandboxes and plugins, Dockerfile builds, and `kubectl exec` for Kraftlet. | | [Release 13 — Adrastea](/releases/r13-adrastea) | 2026-09-02 | ARM64 hosts and multi-platform builds, kernel-less images, scheduled checkpoints, nested virtualization, CNI guest networking, a Karpenter provider, and a Go plugin SDK. | | [Release 12 — Thebe](/releases/r12-thebe) | 2026-08-04 | GPU support, shared instance templates, image pinning and authorization, dashboard user invitations, and Kraftlet CNI support. | | [Release 11 — Metis](/releases/r11-metis) | 2026-07-03 | Branching and checkpointing, custom filesystems and persistent volumes, custom networking with network shield and relay, and a plugin API. | | [Release 10 — Themisto](/releases/r10-themisto) | 2026-06-05 | Instance lifecycle management, annotations, hotplug volumes, indefinite retention, TAP wake-up, new `unikraft api` CLI, EROFS rootfs, ROM support, and Kraftlet CSI integration. | | Releases 1–9 | | Ganymede, Callisto, Io, Europa, Amalthea, Lysithea, Carme, Ananke, Leda | --- ## Document: Python SDK URL: /docs/sdks/python # Python SDK {/* vale off */} {/* The prose below comes from the SDK README verbatim. */} `unikraft-cloud` is the official Python SDK for the [Unikraft Cloud](https://unikraft.com) platform and control-plane APIs. It has two layers. The **idiomatic** layer is what you reach for: envelope-free results, automatic pagination, chainable references, and multi-metro fan-out. The **plumbing** layer underneath mirrors the OpenAPI specification exactly, and stays available for anything the idiomatic layer does not cover yet. The SDK is async-only. ## Installation ```sh pip install unikraft-cloud ``` Requires Python 3.10 or newer. ## Quickstart ```python title="quickstart.py" import asyncio from unikraft_cloud import UnikraftCloud async def main() -> None: async with UnikraftCloud() as ukc: # token from UKC_TOKEN instance = await ukc.metro("fra").instances.create( image="nginx:latest", memory_mb=256, autostart=True ) print(instance.name, instance.uuid, instance.metro) asyncio.run(main()) ``` The client owns a connection pool, so close it when you are done — either with `async with`, or by awaiting `ukc.aclose()`. ## Authentication Pass a bearer `token` to the constructor, or set the `UKC_TOKEN` environment variable. Create a token in the [Unikraft Cloud console](https://console.unikraft.cloud). ## Configuration ```python ukc = UnikraftCloud( token="...", # falls back to UKC_TOKEN metro="fra", # falls back to UKC_METRO; omit to cover every metro ) ``` | Argument | Purpose | | --- | --- | | `token` | Bearer token. Falls back to `UKC_TOKEN`. | | `metro` | The metro code that operations default to, such as `fra`, or a full `http(s)://` URL for a staging or self-hosted deployment. Falls back to `UKC_METRO`. A metro code leaves the other metros reachable with `ukc.metro(...)`; a URL pins the client to that endpoint, and naming another metro then raises. | | `metros` | The metros operations cover by default: `"all"`, one metro, or a list. Creating a resource needs exactly one, so name a metro somewhere when you create. | | `base_url` | Explicit platform API base URL. It settles where requests go, so it overrides `metro` and `UKC_METRO` alike, and pins the client to that one endpoint. | | `control_plane_url` | Override the control-plane API base URL. | | `headers` | Extra headers sent with every request. | | `user_agent` | Override the default User-Agent. | | `http` | An `httpx.AsyncClient` to send through. Supplying one makes its lifetime yours. | | `transport` | An `httpx.AsyncBaseTransport`, chiefly for testing with `httpx.MockTransport`. | | `trust_env` | Honour `HTTP_PROXY`/`HTTPS_PROXY`/`NO_PROXY`. Defaults to `True`. | | `timeout` | Timeout for every request. Omitted, an injected `http` client keeps its own; otherwise the default bounds connecting but not reading, because `wait` operations block for as long as you asked. | ## Metros The platform API is metro-scoped. By default the client is **account-wide**: reads ask every metro the account can reach and merge the answers as they arrive, and each result carries the metro it came from. ```python # Every metro, merged as the pages arrive. Await the listing instead for a list. async for inst in ukc.instances.list(details=True): print(inst.metro, inst.name, inst.state) every = await ukc.instances.list(details=True) # one pass each: call list() again for more # One metro. Because it is known, no lookup is needed. await ukc.metro("fra").instances.get(name="web").suspend() # Several metros, for one call or for a whole client. async for inst in ukc.instances.list(metros=["fra", "dal"]): ... scoped = ukc.metros(["fra", "dal"]) # A listing you stop reading holds a page of every metro, so close it. async with ukc.instances.list() as listing: async for inst in listing: break # What the account can reach, as the control plane reports it. for endpoint in await ukc.available_metros(): print(endpoint.metro, endpoint.base_url) ``` Naming metros is also how you skip metro discovery, which is otherwise one extra request per client. The [metros page](/platform/metros) lists the regions themselves. ## References A resource is addressed by `name` or `uuid` — one or the other, because the API validates whichever field it is given. ```python await ukc.instances.get(name="web") await ukc.instances.get(uuid="550e8400-e29b-41d4-a716-446655440000") ``` A name is only unique **within** a metro, so the same name can exist in several. Add `metro=` to say which you mean, which also saves a lookup: ```python await ukc.instances.get(name="web", metro="fra") ``` Without it, and with more than one metro in scope, the SDK asks every metro. If the name matches in several it raises `AmbiguousRefError` rather than picking one — with the matches attached, so recovering costs no further requests: ```python from unikraft_cloud import AmbiguousRefError try: await ukc.instances.get(name="web") except AmbiguousRefError as err: print(err.metros) # ("fra", "dal") print([m.uuid for m in err.matches]) ``` To act on all of them deliberately, use `each()`: ```python await ukc.instances.each(name="web").suspend() # in every metro that has one ``` Bulk operations take a sequence of references, as `Ref` objects, plain dicts, or names: ```python from unikraft_cloud import Ref await ukc.instances.delete([Ref(uuid="a"), {"name": "b"}, "web"]) ``` An operation the API could only carry out in part raises, naming what failed. What did succeed is on `err.results`, so a partial failure costs nothing already done: ```python from unikraft_cloud import NotFoundError try: await ukc.instances.delete(["web", "gone"]) except NotFoundError as err: print([deleted.name for deleted in err.results]) # ["web"] ``` ## Chainable handles Single-resource operations return a **handle** rather than a coroutine, so they compose. A handle is awaitable too, so awaiting one gives you the resource: ```python inst = await ukc.instances.get(name="web") # the instance await ukc.instances.get(name="web").suspend() # the suspend logs = await ( ukc.metro("fra") .instances.create(image="nginx:latest") .wait(state="running", timeout_seconds=30) .logs(offset=-4096) ) ``` Nothing is sent until a handle is awaited or an operation is chained onto it. With one metro in scope, `get(name=...).suspend()` is a single request; when the scope spans metros, the instance is located first so the operation reaches the metro that holds it. A handle that is dropped without ever being awaited emits a `RuntimeWarning`: unlike a forgotten `await` on a coroutine, nothing else would tell you no request was sent. A handle is awaitable but is not a coroutine, so `asyncio.gather(...)` takes one while `asyncio.create_task(...)` does not; wrap it in `asyncio.ensure_future(...)` for a task. ## Updating a resource Properties are keyword arguments. A value sets the property, `REMOVE` clears it out, and anything omitted is left alone — all in one request. ```python from unikraft_cloud import REMOVE await ukc.instances.get(name="web").update(memory_mb=512, vcpus=2, autokill=REMOVE) ``` When `set` is not what you mean — merging into a property, or removing individual members — stage the operations and apply them together: ```python await ( ukc.instances.get(name="web") .edit() .set(memory_mb=512) .add(env={"LOG_LEVEL": "debug"}, tags=["prod"]) .delete(env=["OLD_FLAG"]) .apply() ) ``` `apply()` returns a handle, so the chain continues. For anything keyword arguments cannot express, `patch()` takes the raw triples. ## Resources `instances`, `volumes`, `services`, `certificates` and `users` hang off any scope — `ukc`, `ukc.metro("fra")` or `ukc.metros([...])`. Creating one takes the properties the API describes as keyword arguments, and a property it does not have is a `TypeError` rather than a field the server quietly ignores. ```python await ukc.volumes.get(name="data").attach(to="web", at="/data") await ukc.services.get(name="web").update(hard_limit=10) await ukc.certificates.get(name="tls").update(chain=chain_pem, pkey=key_pem) for quota in await ukc.users.quotas(): print(quota.metro, quota.used, quota.hard) ``` ## Errors Every failure is an `UnikraftCloudError`, so one `except` catches the lot. Its `kind` says which layer failed (`"http"`, `"network"`, `"parse"` or `"fanout"`) and `status` carries the HTTP status where there was one. ```python from unikraft_cloud import NotFoundError, UnikraftCloudError try: await ukc.instances.get(name="web") except NotFoundError: ... except UnikraftCloudError as err: print(err.kind, err.status, err.errors) ``` `AuthenticationError` (401/403), `NotFoundError` (404), `AlreadyExistsError` (409), `RateLimitError` (429) and `ServerError` (5xx) are raised for the statuses they name, and all subclass `UnikraftCloudError`. The API reports some failures inside an otherwise-200 envelope, per item; those carry the API's own code on `err.errors[n].code` and are raised with the status that says the same thing. A `wait()` that runs out of time raises `WaitTimeoutError`, which is also a builtin `TimeoutError`, and carries the state the API last saw: ```python try: await ukc.instances.get(name="web").wait(state="running", timeout_seconds=30) except TimeoutError as err: print(err.state) # e.g. "starting" ``` When the API attaches a warning to an answer — a deprecated field, say — the SDK issues it as a Python `UnikraftCloudWarning`, so the standard `warnings` filters apply. A multi-metro operation that only partly succeeded raises `MetroFanoutError`. An iteration yields everything the healthy metros returned *before* raising, so a partial failure never costs you the whole answer; operations that cannot yield as they go attach what did arrive to `err.results`. ```python from unikraft_cloud import MetroFanoutError try: async for inst in ukc.instances.list(): ... except MetroFanoutError as err: print([failure.metro for failure in err.failures]) ``` ## The plumbing layer Every operation in the specification is available raw, returning the response envelope untouched. Each client talks to exactly one metro, and a single call can be redirected with `base_url=`. ```python res = await ukc.api.platform.instances.get_instances(count=10) print(res.status, res.op_time_us, res.data.instances) await ukc.api.controlplane.metros.list_metros() # Or per resource, alongside its idiomatic client. await ukc.instances.api.get_instance_metrics(uuid=["..."]) ``` It can also be used on its own, without the idiomatic layer: ```python from unikraft_cloud import ApiClientConfig from unikraft_cloud.api.platform import PlatformApi config = ApiClientConfig(base_url="https://api.fra.unikraft.cloud", token=token) async with PlatformApi(config) as api: res = await api.instances.get_instances(count=10) ``` ## Self-hosted and staging deployments `metro` (and `UKC_METRO`) also accepts a full `http(s)://` base URL, which is used verbatim instead of being expanded into `https://api..unikraft.cloud`. A named endpoint is then the only endpoint there is: no metro discovery is attempted, and naming another metro raises. Point the control plane at a matching deployment with `control_plane_url`. ```python ukc = UnikraftCloud( token=token, metro="https://api.staging.example.internal", control_plane_url="https://controlplane.staging.example.internal", ) ``` {/* vale on */} ## Source The SDK source lives at [github.com/unikraft-cloud/python-sdk](https://github.com/unikraft-cloud/python-sdk), and the published package at [pypi.org/project/unikraft-cloud](https://pypi.org/project/unikraft-cloud). See [`examples/`](https://github.com/unikraft-cloud/python-sdk/tree/HEAD/examples) for complete programs. ## Learn more * [JavaScript SDK](/sdks/js): the client library for the same platform API in JavaScript and TypeScript. * [Go SDK](/sdks/go): the client library for the same platform API in Go. * [Metros](/platform/metros): the regions that the SDK fans out across. * [Instances](/platform/instances): create and manage the instances that the SDK drives. * Unikraft Cloud's [REST API reference](/api/platform/v1), which the plumbing layer mirrors. --- ## Document: Plugin SDK URL: /docs/sdks/plugin # Plugin SDK The Unikraft Cloud Plugin SDK is a small framework for building [plugins](/features/plugins) in Go. A plugin runs as a sidecar HTTP server next to your app inside an instance, and loads from its own [ROM image](/features/roms). It answers the requests that the platform forwards to it over a per-instance, authenticated endpoint. Every plugin meets the same platform contract. It parses the `init` command line, adopts the socket the platform hands it, and decodes the JSON `config` from `STDIN`. It also stands up a router with middleware and drains in-flight requests on shutdown. The SDK takes care of each step. You write a configuration struct and a route registration function, and `pluginsdk.Main` does the rest. A complete plugin takes about 25 lines. ## Installation ```bash go get unikraft.com/cloud/pluginsdk ``` Requires Go 1.26.4 or later. ## Quickstart ```go title="main.go" package main import ( "context" "github.com/gin-gonic/gin" "unikraft.com/cloud/pluginsdk" ) // Config takes its values from the platform `config` JSON on STDIN, through // each field's `json` tag. A --greeting flag overrides that value; an `env` // tag would bind an environment variable as well. type Config struct { Greeting string `json:"greeting" default:"Hello"` } func main() { pluginsdk.Main(&pluginsdk.Plugin[Config]{ Name: "example", Register: func(ctx context.Context, cfg *Config, engine *gin.Engine) error { engine.GET("/hello", func(g *gin.Context) { data := gin.H{"message": cfg.Greeting + ", world!"} env, code, _ := pluginsdk.OK(&data) g.JSON(code, env) }) return nil }, }) } ``` That covers the whole plugin. `pluginsdk.Main` takes these steps in order: 1. Parses the command line: `--api_fd`, `--api-addr`, `--log-level`, `--log-type`, and one flag per field of `Config`. 2. Reads the JSON `config` from `STDIN` and decodes it into `Config`. 3. Configures logging, then runs the optional `Setup` hook. 4. Adopts the `--api_fd` socket as its listener. 5. Builds the [gin](https://gin-gonic.com) engine with the default middleware and calls `Register`. 6. Serves until `SIGINT` or `SIGTERM` arrives, then drains and shuts down. Run the plugin locally with `--api-addr` in place of `--api_fd`: ```bash echo '{"greeting":"Hey"}' | go run . --api-addr :8080 --log-type text curl -s localhost:8080/hello ``` ```json {"status":"success","data":{"message":"Hey, world!"}} ``` ## Configuration A plugin declares its configuration once, as a plain Go struct, and that declaration acts as the single source of truth. It validates the platform `config`, supplies the defaults, and derives the command-line flags and environment variables. ### Where values come from Four sources feed a plugin's configuration. The SDK merges them in this order, from lowest precedence to highest: ``` struct default < platform config on STDIN < environment < command-line flag ``` - **Struct default**: the fallback that the `default` tag bakes into the binary. - **Platform `config`**: the JSON you attach to the plugin when you create the instance, which is the production path. - **Environment**: any field that carries an `env` tag. - **Command-line flag**: an explicit flag for local development and debugging. A field that defaults to `/tmp` stays `/tmp` unless the platform `config` sets `"workdir":"/data"`, unless `WORKDIR=/srv` sits in the environment, unless `--workdir /mnt` appears on the command line. ### Struct tags A config field carries two kinds of tag. A `json` tag names the field's key in the platform `config`, and [kong](https://github.com/alecthomas/kong) tags govern the command-line flag and the environment override. The two work independently: JSON keys conventionally use `snake_case` while flags use `kebab-case`, so `json:"source_type"` and `name:"source-type"` routinely sit on the same field. | Tag | Purpose | Example | |-----|---------|---------| | `json:"…"` | Key in the platform `config` on `STDIN` | `json:"source_url"` | | `name:"…"` | Flag name, in kebab-case | `name:"source-url"` | | `env:"…"` | Environment variable to bind the field to | `env:"SOURCE_URL"` | | `help:"…"` | Help text | `help:"Repository URL."` | | `default:"…"` | Default value | `default:"/tmp"` | | `enum:"a,b,c"` | Restrict the value to a set | `enum:"git,tar"` | | `placeholder:"…"` | Help placeholder | `placeholder:"dir"` | | `required:""` | Make the flag mandatory | `required:""` | | `hidden:""` | Hide the flag from `--help` | `hidden:""` | | `kong:"-"` | Ignore the field entirely | `kong:"-"` | The `json` tag belongs to the SDK rather than to kong. The SDK reads it to map each config key onto a field, and kong owns every other tag. A field without a `json` tag takes its exact Go field name as the key, so `Greeting` matches `"Greeting"` and misses `"greeting"`. That lookup runs case-sensitively, unlike `encoding/json`, so give every field a `json` tag. The map covers a struct's own exported fields, so a field inside an embedded struct gets no config key at all. ```go type Config struct { Workdir string `json:"workdir" name:"workdir" env:"WORKDIR" help:"Working directory." default:"/tmp"` SourceType string `json:"source_type" name:"source-type" env:"SOURCE_TYPE" help:"Source type." enum:"git,tar" default:"git"` Verbose bool `json:"verbose" name:"verbose" env:"VERBOSE" help:"Enable verbose output."` // Never surfaces as a flag or a config key; code populates it. internalToken string `json:"-" kong:"-"` } ``` ### The platform config Attach a `config` to the plugin in the [create instance](/api/platform/v1/instances#create-instance) request, and it reaches the plugin's `init` on `STDIN`: ```json { "name": "example", "image": "user/example:latest", "config": { "greeting": "Bonjour" } } ``` The platform accepts a non-object `config` as well: a bare string or a number counts as valid JSON. A value of that shape maps onto no fields, so reach for the raw bytes instead: ```go raw := pluginsdk.RawConfig(ctx) // []byte, exactly as STDIN delivered it ``` ## Routing `Register` receives the base context, your typed configuration, and the gin engine: ```go Register: func(ctx context.Context, cfg *Config, engine *gin.Engine) error { engine.GET("/files/:name", handleGetFile) engine.POST("/files", handleWriteFile) v1 := engine.Group("/v1") v1.GET("/status", handleStatus) return nil } ``` Define your routes relative to `/`. The platform strips the `plugins//` prefix before the request reaches the plugin, so a call to `.../plugins/example/files/list` arrives as `GET /files/list`. A plugin never needs to know its own name to route correctly. The SDK builds the engine with `gin.New()` in release mode, turns on `HandleMethodNotAllowed`, and rejects unknown JSON fields, which keeps generated types strict. ### Default middleware Ahead of your routes, the SDK installs a global middleware stack from `unikraft.com/x/middleware`: | Middleware | Effect | |------------|--------| | `CORS()` | Cross-origin resource sharing headers | | `ExtraHeaders()` | Static response headers | | `Logger(ctx)` | Structured per-request logging | | `DefaultCacheControl()` | Sensible `Cache-Control` defaults | Append your own middleware, or replace the defaults outright: ```go pluginsdk.Main(&pluginsdk.Plugin[Config]{ Name: "example", Register: register, Middleware: []gin.HandlerFunc{myMiddleware()}, // appended to the defaults // ...or drop the defaults and bring your own stack: // DisableDefaultMiddleware: true, }) ``` ### Generated services Plugins describe their API in [TypeSpec](https://typespec.io) and generate a typed gin service interface from it. Register that generated service from inside `Register`: ```go Register: func(ctx context.Context, cfg *Config, engine *gin.Engine) error { api.RegisterX(engine, handler, nil) return nil } ``` `pluginsdk.OK` and `pluginsdk.Error` return the `(payload, status, error)` triple that a generated handler method produces, so a handler body often comes down to a single `return pluginsdk.OK(&data)`. ## Responses The platform wraps every response in a standard envelope, and two helpers build it for you: ```go // Success. OK takes a pointer and returns three values. env, code, _ := pluginsdk.OK(&data) g.JSON(code, env) // Error. env, code, _ := pluginsdk.Error[any](http.StatusBadRequest, "bad input") g.JSON(code, env) ``` A handler that calls `OK` produces the same shape as the rest of the API: ```json { "status": "success", "data": { "message": "Bonjour, world!" } } ``` The `status` field holds `success`, `partial_success`, or `error`. In Go the envelope type is `platform.Response[T]` from `unikraft.com/cloud/sdk/platform`, which the [Go SDK](/sdks/go) uses as well. :::note `Error` derives the HTTP status from its `status` argument. A non-positive status yields `0`, which gin rejects, so always pass a real status code. ::: ## Global flags Every plugin accepts these four flags, whatever its own configuration declares: | Flag | Environment | Meaning | |------|-------------|---------| | `--api_fd ` | `API_FD` | Serve on file descriptor `n`, which the platform supplies | | `--api-addr ` | `API_ADDR` | Serve on a TCP address such as `:8080`, for local development | | `--log-level` | `LOG_LEVEL` | One of `trace`, `debug`, `info`, `warn`, `error`, `fatal` | | `--log-type` | `LOG_TYPE` | Either `text` or `json` | The platform spells `--api_fd` with an underscore, and every other flag uses a hyphen. ## Lifecycle `Main` runs this sequence: 1. Read the `config` from `STDIN`, then parse the command line. 2. Configure logging from `--log-level` and `--log-type`. 3. Run the optional `Setup` hook. 4. Bind the listener: `--api_fd` in production, `--api-addr` locally. 5. Build the router with the default and user middleware, then call the registration function. 6. Serve on the listener. 7. Block until a signal arrives or the context cancels. 8. Drain: stop accepting connections, finish in-flight requests, then shut down. `Serve` covers step 2, then steps 4 through 8. It takes an already-resolved configuration rather than reading the command line or `STDIN`, and it runs no `Setup` hook. `--api_fd` wins whenever the command line carries it, and otherwise the SDK falls back to `--api-addr`. At least one of the two has to resolve, or startup fails with `ErrNoListener`. `Serve` can also take a listener directly through `WithListener`, which outranks both flags. A shutdown gives in-flight requests a 30-second window to drain. ## Scale-to-zero [Scale-to-zero](/features/scale-to-zero) puts an idle instance to sleep. A plugin request wakes the instance and keeps it up for the length of that request. Background work that outlives the request has to hold the instance awake on its own. The `scaletozero` sub-package keeps a counter for exactly that: ```go import "unikraft.com/cloud/pluginsdk/scaletozero" // Hold the instance awake while the work runs. if err := scaletozero.Increment(); err != nil { return err } defer scaletozero.Decrement() ``` A counter rather than a flag means many independent workers increment and decrement without coordinating. Scale-to-zero stays suspended while the count sits above zero and resumes at zero. `Set(n)`, `IncrementBy(n)`, and `DecrementBy(n)` cover the remaining cases. ## Logging The SDK configures a structured logger from `--log-level` and `--log-type`, which default to `info` and `text`, and makes it available to your handlers through the request context. The logger comes from `unikraft.com/x/log`. Pass `--log-type json` in production. ## Project layout A plugin keeps its API description and its image recipes next to the source: ``` plugins// ├── api.tsp # TypeSpec API description (optional) ├── openapi.yaml # generated from api.tsp ├── main.go # the entrypoint ├── api/ # generated service: interface, models, register function ├── Dockerfile # builds the single `init` executable into a scratch image ├── Kraftfile # ROM image recipe └── go.mod ``` A small plugin needs no more than `main.go`, `go.mod`, a `Dockerfile`, and a `Kraftfile`. ## Building the plugin image The deliverable is a ROM image with a single `init` executable at its root. The platform appends `--api_fd ` and pipes the `config` JSON to `STDIN` at launch, so `/init` has to be the SDK-based binary that understands both. A plugin runs as a process inside the host instance rather than as its own unikernel, so `init` has to stand on its own. A static Go build produces that. ### Dockerfile A build stage produces `init`, and a `scratch` image carries it: ```dockerfile title="Dockerfile" FROM golang:1.26-bookworm AS build WORKDIR /plugin COPY . . RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /plugin/dist/init . FROM scratch COPY --from=build /plugin/dist/init /init ENTRYPOINT ["/init"] ``` ### Kraftfile A plugin ships as a ROM, which carries no kernel of its own. The platform mounts it at `/uk/plugins/` inside the host instance and runs its `init`. Declare the image under `roms`: ```yaml title="Kraftfile" spec: v0.7 targets: - kraftcloud/x86_64 roms: - ./Dockerfile ``` A ROM has no `runtime` to take an architecture from, so it names one under `targets`, or you pass `--arch` on the command line. A build that does neither fails, and each plugin needs the architecture of the instance that hosts it. :::caution Set no `runtime`. A `runtime` produces a bootable unikernel that the plugin loader can't mount, and the host instance then fails to boot. The platform runs the image's `init` and appends `--api_fd` on its own, so set no `cmd` and never hard-code `--api_fd`. ::: Package and push the ROM image: ```bash unikraft build . --output /example:latest ``` ## Deploying Attach the plugin when you create the instance, naming the ROM image and an optional `config`: ```bash unikraft instance create \ --name my-instance \ --metro fra \ --image my-app:latest \ --plugin 'name=example,image=/example:latest,config={"greeting":"Bonjour"}' ``` Then call the plugin through the instance's authenticated endpoint: ```bash curl -H "Authorization: Bearer $UKC_TOKEN" \ https://api.fra.unikraft.cloud/v1/instances//plugins/example/hello ``` The [plugins](/features/plugins) page covers the platform side in full: authorization, adding a plugin to an existing instance, and the limits. ## API reference ### Entrypoints `Main` suits the common case of one plugin and one server. It parses the command line, reads `STDIN`, serves, blocks, and exits with a non-zero status on error. ```go func Main[C any](p *Plugin[C]) ``` `Serve` takes an already-resolved configuration, reads neither the command line nor `STDIN`, and returns an error instead of exiting. Cancelling `ctx` triggers the graceful shutdown. Reach for it in tests, when you embed a plugin in a larger program, or when you bring your own command grammar. ```go func Serve[C any](ctx context.Context, cfg *C, register RegisterFunc[C], opts ...Option) error ``` ### The plugin declaration ```go type Plugin[C any] struct { Name string Version string Register RegisterFunc[C] Setup SetupFunc[C] Middleware []gin.HandlerFunc DisableDefaultMiddleware bool } type RegisterFunc[C any] func(ctx context.Context, cfg *C, engine *gin.Engine) error type SetupFunc[C any] func(ctx context.Context, cfg *C) error ``` | Field | Description | |-------|-------------| | `Name` | The plugin name, for logs and diagnostics only, since routing ignores it | | `Version` | An optional version string that the startup logs carry | | `Register` | Attaches the routes, and a non-nil error aborts startup (required) | | `Setup` | An optional hook that runs once after the SDK resolves the configuration and before the server accepts requests | | `Middleware` | Middleware to append to the default stack | | `DisableDefaultMiddleware` | Drops the built-in stack and leaves the plugin in control | ### Options An `Option` customizes `Serve`. A later option overwrites an earlier one that sets the same field. `WithMiddleware` appends instead of overwriting, and the listener keeps a fixed priority whatever the order: `WithListener`, then `WithAPIFD`, then `WithAddr`. | Option | Effect | |--------|--------| | `WithOptions(o Options)` | Seeds the runtime from the parsed global flags | | `WithAPIFD(fd int)` | Forces the listener to adopt a file descriptor | | `WithAddr(addr string)` | Forces a TCP listen address | | `WithListener(ln net.Listener)` | Serves on a listener you supply | | `WithMiddleware(mw ...gin.HandlerFunc)` | Appends global middleware | | `WithoutDefaultMiddleware()` | Drops the default middleware stack | | `WithRawConfig(raw []byte)` | Supplies the raw `STDIN` config for `RawConfig` | `Options` itself holds the four global flags as an embeddable kong struct: `APIFd`, `APIAddr`, `LogLevel`, and `LogType`. Embed it in your own command grammar to keep the standard flags. ### Context and config helpers Every request context descends from the base context, so these helpers work inside a handler through `g.Request.Context()`. | Symbol | Description | |--------|-------------| | `FromContext[C any](ctx) *C` | Retrieves the typed configuration from a request context | | `RawConfig(ctx) []byte` | The raw platform `config` bytes from `STDIN` | | `APIFD(ctx) (int, bool)` | The adopted file descriptor, and whether one exists | | `ReadConfig(stdin io.Reader) []byte` | Reads the `STDIN` config verbatim | | `NewConfigResolver[C any](raw []byte) (kong.Resolver, bool)` | Builds the kong resolver for a custom grammar | ### Response helpers | Symbol | Description | |--------|-------------| | `OK[T any](data *T) (*platform.Response[T], int, error)` | A success envelope that wraps `data` | | `Error[T any](status int, msg string) (platform.Response[T], int, error)` | An error envelope | ### Other exported symbols | Symbol | Description | |--------|-------------| | `ShutdownTimeout` | The graceful-drain bound, at 30 seconds | | `ReadHeaderTimeout` | The bound on waiting for request headers, at 30 seconds | | `ErrNoListener` | Neither `--api_fd` nor `--api-addr` resolved | | `ErrNoRegister` | A plugin reached `Serve` without a registration function | | `ErrInvalidAPIFD` | The `--api_fd` descriptor yielded no usable file | ## Bring your own command grammar `Main` wires one command line for you. A plugin that needs subcommands or extra pre-serve work defines its own kong grammar and calls `Serve` from the subcommand: ```go type CLI struct { pluginsdk.Options // --api_fd, --api-addr, --log-level, --log-type Config Config `embed:""` Run RunCmd `cmd:"" help:"Fetch source, then serve."` } type RunCmd struct { SourceURL string `name:"source-url" env:"SOURCE_URL" help:"Repository to clone."` } func (c *RunCmd) Run(ctx context.Context, cli *CLI, raw rawConfig) error { if err := fetchSource(ctx, c.SourceURL); err != nil { return err } return pluginsdk.Serve(ctx, &cli.Config, register, pluginsdk.WithOptions(cli.Options), pluginsdk.WithRawConfig(raw), ) } ``` Two exported building blocks tie it together in `main`. `ReadConfig` reads the platform config off `STDIN`, and `NewConfigResolver` turns those bytes into the kong resolver that gives you the standard precedence: ```go type rawConfig []byte func main() { ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer cancel() raw := pluginsdk.ReadConfig(os.Stdin) var cli CLI opts := []kong.Option{ kong.Name("example"), kong.UsageOnError(), kong.BindTo(ctx, (*context.Context)(nil)), kong.Bind(rawConfig(raw)), } if resolver, ok := pluginsdk.NewConfigResolver[Config](raw); ok { opts = append(opts, kong.Resolvers(resolver)) } k := kong.Parse(&cli, opts...) k.FatalIfErrorf(k.Run()) } ``` `pluginsdk.Options` contributes the standard global flags and `WithOptions` feeds them to `Serve`, so the listener selection and the logging match `Main` with your own command tree on top. Extra middleware alone calls for no custom grammar, since `Middleware` and `DisableDefaultMiddleware` on `Plugin` already cover that. ## Source The SDK source lives at [github.com/unikraft-cloud/plugin-sdk](https://github.com/unikraft-cloud/plugin-sdk), and the API reference at [pkg.go.dev/unikraft.com/cloud/pluginsdk](https://pkg.go.dev/unikraft.com/cloud/pluginsdk). ## Learn more * [Plugins](/features/plugins): the platform feature that this SDK targets. * [ROMs](/features/roms): the image format that a plugin ships as. * [Scale-to-zero](/features/scale-to-zero): how an idle instance wakes to serve a plugin request. * [Go SDK](/sdks/go): the client library for the platform API itself. * [JavaScript SDK](/sdks/js): the same platform API from JavaScript and TypeScript. * [Python SDK](/sdks/python): the same platform API from Python. * [Instances](/platform/instances): create and manage the instances that host plugins. --- ## Document: Unikraft Cloud SDKs Client libraries and extension SDKs for the Unikraft Cloud platform URL: /docs/sdks/overview # Unikraft Cloud SDKs {/* vale off */} Everything the [CLI](/cli/overview) and the [console](https://console.unikraft.cloud) do goes through two APIs: the per-metro [platform API](/api/platform/v1) for instances, volumes, services and certificates, and the global control-plane API for accounts, metro discovery, images and self-hosted nodes. The SDKs on this page put those same APIs in your own programs, so instances, volumes, services and certificates are things your code creates and drives directly. ## Client libraries `@unikraft/cloud` for Node.js and TypeScript. Fully typed, with chainable handles, multi-metro fan-out, and a [`Sandbox`](/sdks/js/sandboxes) class for running commands and moving files inside an instance. `unikraft-cloud` for Python 3.10 and newer. Async-only, with the same chainable references and multi-metro fan-out. `unikraft.com/cloud/sdk`, generated from the OpenAPI specification. The same client the `unikraft` CLI is built on. ## Extending the platform Build a [plugin](/features/plugins) in Go: the in-instance agent that adds services such as command execution and filesystem access to an instance, without hand-rolling the platform contract. ## Which layer to write against The JavaScript and Python client libraries are two layers each, and you pick per call: | Layer | What you get | When to reach for it | | --- | --- | --- | | Idiomatic | Envelope-free results, automatic pagination, chainable references, metro fan-out | Almost always | | Plumbing | The OpenAPI specification as written: one method per `operationId`, raw response envelope, one metro per call | Anything the idiomatic layer does not cover yet | The idiomatic layer holds a plumbing client rather than replacing it, so the raw API stays one property away. The Go SDK is the plumbing layer alone: the generated OpenAPI client, with no idiomatic layer on top. ## Authentication Every client library reads a bearer token from the `UKC_TOKEN` environment variable, and takes one explicitly as well. The plugin SDK runs inside an instance and needs no platform token. Create a token in the [Unikraft Cloud console](https://console.unikraft.cloud). ```bash export UKC_TOKEN=... # the token export UKC_METRO=fra # optional: pin to one metro ``` Left unset, `UKC_METRO` leaves the JavaScript and Python clients account-wide: reads ask every [metro](/platform/metros) the account can reach and merge the answers, each result tagged with the metro it came from. The Go client targets one metro per client, and defaults to `fra`. ## Beyond the SDKs Nothing here is a prerequisite for the others. The [`unikraft` CLI](/cli/overview) covers the same ground from a shell, and the [REST API](/api/platform/v1) is available directly for any language without an SDK. {/* vale on */} --- ## Document: JavaScript SDK Install the @unikraft/cloud package and make your first request URL: /docs/sdks/js # JavaScript SDK `@unikraft/cloud` is the official JavaScript and TypeScript SDK for the [Unikraft Cloud](https://unikraft.com) API. The package is fully typed, and it covers the per-metro [platform API](/api/platform/v1) and the global control plane API. ## Installation ```sh npm install @unikraft/cloud ``` The SDK needs Node.js 22.12 or later, and it ships as ECMAScript modules (ESM) only. From that version, Node.js can `require()` an ESM package, so CommonJS code can load it too. ## Quickstart This example creates a client, starts an instance in the `fra` metro, waits until the instance runs, then deletes it. ```ts title="quickstart.ts" import { UnikraftCloud } from "@unikraft/cloud"; const ukc = new UnikraftCloud({ token: process.env.UKC_TOKEN }); const fra = ukc.metro("fra"); const instance = await fra.instances.create({ image: "nginx:latest", memory_mb: 256, autostart: true, service_group: { services: [{ port: 443, handlers: ["tls", "http"], destination_port: 80 }], }, }); await fra.instances.get({ uuid: instance.uuid! }).wait({ state: "running" }); console.log(instance.name, instance.uuid); await fra.instances.delete([{ uuid: instance.uuid! }]); ``` Three ideas from this example come back on every page of this section: - `ukc.metro("fra")` limits the client to one [metro](/sdks/js/metros). Without a limit, a read covers every metro of the account. - `fra.instances.get(...)` returns a [handle](/sdks/js/resources#handles). A handle sends nothing until you await it, and every operation on it returns a new handle, so `.wait(...)` chains. - `{ uuid }` is a [ref](/sdks/js/resources#refs). A ref is `{ name }` or `{ uuid }`, and it names one resource. ## Authentication Every request carries a bearer token. Create the token in the [Unikraft Cloud console](https://console.unikraft.cloud), then pass it to the constructor: ```ts const ukc = new UnikraftCloud({ token: "..." }); ``` Or set the `UKC_TOKEN` environment variable, and create the client with no arguments: ```sh export UKC_TOKEN=... ``` ```ts const ukc = new UnikraftCloud(); ``` ## Two layers The SDK has two layers, and you choose the layer per call. | Layer | Import | What you get | | ------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | **Idiomatic** | `@unikraft/cloud` | Short verbs such as `ukc.instances.get`, results without the response envelope, automatic pagination, metro fan-out, chainable handles | | **Raw** | `@unikraft/cloud/api/platform`, `@unikraft/cloud/api/controlplane` | One method per `operationId` of the OpenAPI specification, the raw response envelope, one metro per call | Use the idiomatic layer by default. The raw layer is one property away, as `ukc.api`, for every operation that the idiomatic layer doesn't cover yet. See [the raw API](/sdks/js/resources#the-raw-api). ## Pages in this section | Page | Content | | -------------------------------- | -------------------------------------------------------------------------------- | | [Client](/sdks/js/client) | Constructor options, endpoints, self-hosted deployments, runtime, proxy support | | [Metros](/sdks/js/metros) | How one client covers many metros, and how names behave across them | | [Resources](/sdks/js/resources) | Refs, handles, updates, the method list, and the raw API | | [Errors](/sdks/js/errors) | The error classes, the error kinds, and how waits behave | | [Sandboxes](/sdks/js/sandboxes) | Run commands and move files inside a microVM | ## Source - Source code: [github.com/unikraft-cloud/js-sdk](https://github.com/unikraft-cloud/js-sdk) - Package: [npmjs.com/package/@unikraft/cloud](https://www.npmjs.com/package/@unikraft/cloud) - Complete programs: [`examples/`](https://github.com/unikraft-cloud/js-sdk/tree/HEAD/examples) ## Learn more * [Python SDK](/sdks/python): the client library for the same platform API in Python. * [Go SDK](/sdks/go): the client library for the same platform API in Go. * [REST API reference](/api/platform/v1): the platform API that the raw layer mirrors. --- ## Document: Go SDK URL: /docs/sdks/go # Go SDK The Unikraft Cloud Go SDK is an autogenerated client library that interfaces with the Unikraft Cloud platform API, based on the public OpenAPI specification at https://github.com/unikraft-cloud/openapi. ## Installation ```bash go get unikraft.com/cloud/sdk ``` Requires Go 1.27 or later. ## Quickstart ```go title="main.go" package main import ( "context" "fmt" "unikraft.com/cloud/sdk/platform" ) func main() { ctx := context.Background() // Create a new client. NewClientFromEnv reads the token and default metro // from environment variables (UKC_TOKEN and UKC_METRO). client := platform.NewClientFromEnv() // List all instances resp, err := client.GetInstances(ctx, nil, platform.GetInstancesOpts{}) if err != nil { panic(err) } // Check for API-level errors if resp.Status != platform.ResponseStatusSuccess { panic(resp.Message) } for _, inst := range resp.Data.Instances { fmt.Printf("Name: %s\n", inst.Name) fmt.Printf("UUID: %s\n", inst.Uuid) fmt.Printf("State: %s\n", inst.State) fmt.Printf("Image: %s\n", inst.Image) fmt.Println("---") } } ``` ## Authentication Pass your token via `platform.WithToken()`: ```go client := platform.NewClient( platform.WithToken("your-api-token"), ) ``` `NewClient` uses only the options you pass and doesn't read the environment. To configure the client from the environment, use `NewClientFromEnv`. It reads the token from the first of these environment variables that's set: 1. `UKC_TOKEN` 2. `UNIKRAFT_CLOUD_TOKEN` 3. `KRAFTCLOUD_TOKEN` (legacy name) It also reads the default metro from the `UKC_METRO` environment variable. If not specified, the SDK defaults to `fra`. Options you pass to `NewClientFromEnv` take precedence over the environment. ```go // Reads UKC_TOKEN (or a fallback) and UKC_METRO from the environment client := platform.NewClientFromEnv() ``` ## Client options | Option | Description | | ----------------------------- | -------------------------------------------------------------- | | `WithDefaultEndpoint(string)` | Full API endpoint address (for example, `https://api.fra.unikraft.cloud`) | | `WithDefaultMetro(string)` | Metro shorthand—converted to endpoint address automatically | | `WithToken(string)` | API token | | `WithHTTPClient(...)` | Custom `http.Client` | | `WithUserAgent(string)` | Override the `User-Agent` header | | `WithAllowInsecure(bool)` | Skip TLS verification (development only) | `WithDefaultEndpoint` and `WithDefaultMetro` both set the API endpoint. Use `WithDefaultMetro("fra")` as a shorthand for `WithDefaultEndpoint("https://api.fra.unikraft.cloud")`. The base address is `https://api..unikraft.cloud`. An existing client also has methods that return a client with one setting changed: | Method | Description | | ----------------------------- | -------------------------------------------------------------- | | `WithMetro(string)` | Use a different metro | | `WithTimeout(time.Duration)` | Set the request timeout | | `WithHTTPClient(...)` | Use a different HTTP client | ```go // Query another metro with the same token resp, err := client.WithMetro("sfo").GetInstances(ctx, nil, platform.GetInstancesOpts{}) ``` ## Resources The `platform.Client` interface covers the full Unikraft Cloud REST API surface. In the examples below: - `ids` is a `[]platform.NameOrUUID` that selects resources by name or UUID. Passing `nil` to a list method, such as `GetInstances` or `GetVolumes`, returns all resources. The autoscale read methods are the exception and need at least one service group. - `req` is the request type that the method takes, for example `platform.CreateInstanceRequest` or `[]platform.StartInstancesRequestItem`. - `platform.Opts{}` holds the optional query parameters, for example `platform.GetInstancesOpts{Details: &details, Tags: []string{"production"}}`. ### Instances ```go // Create client.CreateInstance(ctx, req) ``` ```go // Read client.GetInstances(ctx, ids, platform.GetInstancesOpts{}) client.GetInstanceByUUID(ctx, uuid, platform.GetInstanceByUUIDOpts{}) ``` ```go // Start / Stop / Suspend client.StartInstanceByUUID(ctx, uuid, platform.StartInstanceByUUIDRequestBody{}) client.StartInstances(ctx, req) client.StopInstanceByUUID(ctx, uuid, platform.StopInstanceByUUIDRequestBody{}) client.StopInstances(ctx, req) client.SuspendInstanceByUUID(ctx, uuid, platform.SuspendInstanceByUUIDRequestBody{}) client.SuspendInstances(ctx, req) ``` ```go // Update client.UpdateInstanceByUUID(ctx, uuid, req) client.UpdateInstances(ctx, req) ``` ```go // Delete client.DeleteInstanceByUUID(ctx, uuid, platform.DeleteInstanceByUUIDRequestBody{}) client.DeleteInstances(ctx, req) ``` ```go // Logs and metrics client.GetInstanceLogsByUUID(ctx, uuid, platform.GetInstanceLogsByUUIDRequestBody{}) client.GetInstanceLogs(ctx, req, platform.GetInstanceLogsOpts{}) client.GetInstanceMetricsByUUID(ctx, uuid) client.GetInstanceMetrics(ctx, ids, platform.GetInstanceMetricsOpts{}) ``` ```go // History client.GetInstanceHistoryByUUID(ctx, uuid) client.GetInstanceHistory(ctx, ids, platform.GetInstanceHistoryOpts{}) ``` ```go // Wait for a target state client.WaitInstanceByUUID(ctx, uuid, req) client.WaitInstances(ctx, req, platform.WaitInstancesOpts{}) ``` ### Instance templates ```go // Create client.CreateTemplateInstances(ctx, req) ``` ```go // Read client.GetTemplateInstances(ctx, ids, platform.GetTemplateInstancesOpts{}) client.GetTemplateInstanceByUUID(ctx, uuid, platform.GetTemplateInstanceByUUIDOpts{}) ``` ```go // Update client.UpdateTemplateInstanceByUUID(ctx, uuid, req) client.UpdateTemplateInstances(ctx, req) ``` ```go // Delete client.DeleteTemplateInstanceByUUID(ctx, uuid) client.DeleteTemplateInstances(ctx, ids) ``` ### Instance checkpoints ```go // Create client.CreateCheckpointInstances(ctx, req) ``` ```go // Read client.GetCheckpointInstances(ctx, ids, platform.GetCheckpointInstancesOpts{}) client.GetCheckpointInstanceByUUID(ctx, uuid, platform.GetCheckpointInstanceByUUIDOpts{}) ``` ```go // History client.GetCheckpointHistoryByUUID(ctx, uuid) client.GetCheckpointHistory(ctx, ids, platform.GetCheckpointHistoryOpts{}) ``` ```go // Update client.UpdateCheckpointInstanceByUUID(ctx, uuid, req) client.UpdateCheckpointInstances(ctx, req) ``` ```go // Delete client.DeleteCheckpointInstanceByUUID(ctx, uuid) client.DeleteCheckpointInstances(ctx, ids) ``` ### Service groups ```go // Create client.CreateServiceGroup(ctx, req) ``` ```go // Read client.GetServiceGroups(ctx, ids, platform.GetServiceGroupsOpts{}) client.GetServiceGroupByUUID(ctx, uuid, platform.GetServiceGroupByUUIDOpts{}) ``` ```go // Update client.UpdateServiceGroupByUUID(ctx, uuid, req) client.UpdateServiceGroups(ctx, req) ``` ```go // Delete client.DeleteServiceGroupByUUID(ctx, uuid) client.DeleteServiceGroups(ctx, ids) ``` ### Autoscale ```go // Configuration (by service group UUID) client.CreateAutoscaleConfigurationByServiceGroupUUID(ctx, uuid, req) client.GetAutoscaleConfigurationsByServiceGroupUUID(ctx, uuid) client.DeleteAutoscaleConfigurationsByServiceGroupUUID(ctx, uuid) ``` ```go // Configuration (batch) client.CreateAutoscaleConfigurations(ctx, req) client.GetAutoscaleConfigurations(ctx, ids, platform.GetAutoscaleConfigurationsOpts{}) client.DeleteAutoscaleConfigurations(ctx, ids) ``` ```go // Policies client.CreateAutoscaleConfigurationPolicy(ctx, uuid, req) client.GetAutoscaleConfigurationPolicies(ctx, uuid, req) client.GetAutoscaleConfigurationPolicyByName(ctx, uuid, name) client.DeleteAutoscaleConfigurationPolicies(ctx, uuid, req) client.DeleteAutoscaleConfigurationPolicyByName(ctx, uuid, name) ``` ### Volumes ```go // Create client.CreateVolume(ctx, req) ``` ```go // Read client.GetVolumes(ctx, ids, platform.GetVolumesOpts{}) client.GetVolumeByUUID(ctx, uuid, platform.GetVolumeByUUIDOpts{}) ``` ```go // Attach / Detach client.AttachVolumeByUUID(ctx, uuid, req) client.AttachVolumes(ctx, req) client.DetachVolumeByUUID(ctx, uuid, req) client.DetachVolumes(ctx, req) ``` ```go // Clone client.CloneVolumeByUUID(ctx, uuid, req) client.CloneVolumes(ctx, req) ``` ```go // Update client.UpdateVolumeByUUID(ctx, uuid, req) client.UpdateVolumes(ctx, req) ``` ```go // Delete client.DeleteVolumeByUUID(ctx, uuid) client.DeleteVolumes(ctx, ids) ``` ### Volume templates ```go // Create a template from existing volumes client.CreateTemplateVolume(ctx, ids) ``` ```go // Read client.GetTemplateVolumes(ctx, ids, platform.GetTemplateVolumesOpts{}) client.GetTemplateVolumeByUUID(ctx, uuid, platform.GetTemplateVolumeByUUIDOpts{}) ``` ```go // Update client.UpdateTemplateVolumeByUUID(ctx, uuid, req) client.UpdateTemplateVolumes(ctx, req) ``` ```go // Delete client.DeleteTemplateVolumeByUUID(ctx, uuid) client.DeleteTemplateVolumes(ctx, ids) ``` ### Certificates ```go // Create client.CreateCertificate(ctx, req) ``` ```go // Read client.GetCertificates(ctx, ids, platform.GetCertificatesOpts{}) client.GetCertificateByUUID(ctx, uuid) ``` ```go // Update client.UpdateCertificateByUUID(ctx, uuid, req) client.UpdateCertificates(ctx, req) ``` ```go // Delete client.DeleteCertificateByUUID(ctx, uuid) client.DeleteCertificates(ctx, ids) ``` ### Images In the image methods, `req` is a list of image references (`[]platform.GetImagesRequestTagOrDigest`, `[]platform.PinImageRequestItem` or `[]platform.UnpinImageRequestItem`). ```go // Read client.GetImages(ctx, req, platform.GetImagesOpts{}) client.GetImageStore(ctx, req, platform.GetImageStoreOpts{}) ``` ```go // Pin / Unpin client.PinImages(ctx, req) client.UnpinImages(ctx, req) ``` ### Users and quotas ```go // Read quotas client.GetUser(ctx) client.GetUserByUUID(ctx, uuid) ``` ### Audit events `SubscribeAuditEvents` streams [audit events](/platform/audit-events) on a channel: ```go events, err := client.SubscribeAuditEvents(ctx, platform.SubscribeAuditEventsOpts{}) if err != nil { panic(err) } for event := range events { fmt.Println(event) } ``` ### Health ```go // Check node health client.Healthz(ctx) ``` ## Source The SDK source and full API reference are available at [github.com/unikraft-cloud/go-sdk](https://github.com/unikraft-cloud/go-sdk) and [pkg.go.dev/unikraft.com/cloud/sdk](https://pkg.go.dev/unikraft.com/cloud/sdk). --- ## Document: Volumes URL: /docs/platform/volumes # Volumes A volume is a persistent storage device that keeps data across restarts and even redeployments. {/* vale off */} :::tip[Drive volumes from your own code] The [SDKs](/sdks/overview) wrap the volume API, including attaching and detaching volumes: ```python await ukc.volumes.get(name="data").attach(to="web", at="/data") ``` See the [JavaScript](/sdks/js), [Python](/sdks/python) and [Go](/sdks/go) SDKs. ::: {/* vale on */} ## Typical workflow ### Creating a persistent volume :::tip You can create an instance with attached volumes in a single API operation. This ties the volumes' lifetimes to the instance - when you delete the instance, the volumes disappear. This use case is still valid, because an instance with no volumes attached will write all its files into RAM and eventually exhaust its memory. ::: To start, create the volume with the CLI, with size 100 MiB and name `my-volume`: ```bash unikraft volumes create --metro fra \ --name my-volume \ --size 100M ``` ```ansi metro: fra name: my-volume uuid: 4d5576f7-315a-4eab-98b8-6c18fec076e0 state: available size: 100MiB filesystem: ext4 quota-policy: static persistent: true access-mode: rwo timestamps:  created: just now ``` You can list the volume with: ```bash unikraft volumes list ``` ```ansi METRO NAME STATE SIZE CREATED fra my-volume available 100MiB just now ``` The volume is in `available` state because it isn't attached to any instance yet. It's also marked as `persistent` because it will persist even after instance deletion. ### Importing local data To populate an empty volume with local data, use the CLI `import` subcommand. For example, assuming the data you want to import is in your `my-data` directory, you would run: ```bash unikraft volume import \ my-volume \ --source ./data ``` You should see output like: ```ansi │ importing data into volume size=16.47KiB volume=my-volume │ import complete free=89.52MiB total=89.69MiB volume=my-volume ``` ### Creating an instance Start from [this guide](/guides/httpserver-python3.12-flask3.0) to create a simple Flask web server: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/httpserver-python3.12-flask3.0 ``` Replace the contents of `server.py` with: ```py title="server.py" from flask import Flask, send_from_directory from datetime import datetime import os app = Flask(__name__) file_path = "/mnt/log.txt" def initialize_file(): if not os.path.exists(file_path): with open(file_path, 'w') as log_file: log_file.write("Log file created.\n") @app.route('/') def write_to_file(): initialize_file() current_datetime = datetime.now().strftime("%Y-%m-%d %H:%M:%S") # Write to file with open(file_path, 'a') as log_file: log_file.write(f"{current_datetime}\n") # Read contents of the file with open(file_path, 'r') as log_file: file_contents = log_file.read() return file_contents if __name__ == '__main__': app.run(host='0.0.0.0', port=8080) ``` On every request, this simple server will write a timestamp to a file on the mounted persistent volume and print out the current contents of the file. Build and deploy the Flask web server and mount the `my-volume` volume at `/mnt`: ```bash unikraft build . --output /httpserver-python312-flask30:latest unikraft run --metro fra \ -m 512M \ -p 443:8080/http+tls \ -v my-volume:/mnt \ --image /httpserver-python312-flask30:latest ``` You should see output like: ```ansi metro: fra name: httpserver-python312-flask30-bxwxm uuid: 3ff1ebad-2639-4214-bab4-ed35c4c32fa4 state: starting image: /httpserver-python312-flask30 resources:  memory: 512MiB  vcpus: 1 service:  uuid: 2021ca1e-5e47-a35c-bde2-7190f3815c07  name: damp-sunset-azd6dtyt  domains:  - fqdn: damp-sunset-azd6dtyt.fra.unikraft.app volumes: - name: my-volume  uuid: 4d5576f7-315a-4eab-98b8-6c18fec076e0  at: /mnt networks: - uuid: 4dc3b272-4fa2-ddb4-7ce0-5e66e8112a61  private-ip: 10.0.6.5  mac: 12:b0:66:99:b3:3f timestamps:  created: just now ``` To confirm that the platform attached the volume, run: ```bash unikraft volumes get my-volume ``` You should see output like: ```ansi METRO NAME STATE SIZE CREATED fra my-volume mounted 100MiB 2 minutes ago ``` :::note A volume can't mount at `/uk` or at any path below it, which the platform reserves in the guest for [plugins](/features/plugins#how-it-works). Such a mount point fails the request with `The mountpoint must not be '/uk' or a path below it`, whether you set it when you create the instance or when you attach the volume. ::: ### Testing The Flask server writes the time and date to `/mnt/log.txt` for each request. Test it by running `curl` several times. Example output: ```console $ curl https://holy-bonobo-px345skl.fra.unikraft.app Log file created. 2024-02-24 08:15:32 $ curl https://holy-bonobo-px345skl.fra.unikraft.app Log file created. 2024-02-24 08:15:32 2024-02-24 08:15:34 $ curl https://holy-bonobo-px345skl.fra.unikraft.app Log file created. 2024-02-24 08:15:32 2024-02-24 08:15:34 2024-02-24 08:15:35 ``` To test data persistence, you can stop the instance, then detach the volume from the instance: ```bash unikraft volumes detach my-volume --from httpserver-python312-flask30-bxwxm unikraft instances delete httpserver-python312-flask30-bxwxm ``` You should see that the volume is now `available` again. :::info While the instance is `stopped`, the volume is still attached to it, but **not** mounted. Mounting of an attached volume happens automatically when the instance starts. This is different from a [snapshotted](/features/snapshots/) instance, which is in `standby` state and has its volumes still mounted. ::: :::note If you try to detach a volume while the instance is running, the platform will queue the operation. The detach operation will trigger when the instance goes into `stopped`. ::: Now start another instance with the same volume: ```bash unikraft run --metro fra \ -m 512M \ -p 443:8080/http+tls \ -v my-volume:/mnt \ --image /httpserver-python312-flask30:latest ``` This should output something like: ```ansi metro: fra name: httpserver-python312-flask30-0s9c7 uuid: 59d06415-84b8-4bf5-85c7-997960382011 state: starting image: /httpserver-python312-flask30 resources:  memory: 512MiB  vcpus: 1 service:  uuid: ca8ae7e9-3767-85f6-3d70-b77bcf894a7c  name: winter-field-v6m37jgs  domains:  - fqdn: winter-field-v6m37jgs.fra.unikraft.app volumes: - name: my-volume  uuid: 4d5576f7-315a-4eab-98b8-6c18fec076e0  at: /mnt networks: - uuid: 935ba4ef-39c2-07e7-d2ae-8cd3c9aae07c  private-ip: 10.0.6.5  mac: 12:b0:66:99:b3:3f timestamps:  created: just now ``` :::note Mount the volume when you create the instance, as shown above. `unikraft volumes attach` on a running instance doesn't mount the volume right away. The platform queues the attach and applies it when the instance next stops, so the volume only appears after a restart. ::: Run one final `curl` to this new address. You should see the previous contents plus a new entry at the bottom: ```console $ curl https://winter-field-v6m37jgs.fra.unikraft.app Log file created. 2024-02-24 08:15:32 2024-02-24 08:15:34 2024-02-24 08:15:35 2024-02-24 08:20:58 ``` ### Cleaning up To clean up, first detach the volume from all instances and then remove it: ```bash unikraft instances stop httpserver-python312-flask30-0s9c7 unikraft volumes detach my-volume --from httpserver-python312-flask30-0s9c7 unikraft volumes delete my-volume unikraft instances delete httpserver-python312-flask30-0s9c7 unikraft instances delete httpserver-python312-flask30-bxwxm ``` :::info If the instance is already in `stopped` state, the detach operation will trigger immediately. Otherwise, the platform will queue it and trigger when the instance fully stops. Do **not** confuse a `stopped` instance with a [snapshotted](/features/snapshots/) instance, which is in `standby` state and has its volumes still mounted. ::: ## Warm-plug volume operations The platform supports **warm-plug** volume operations. This lets you attach volumes in more lifecycle states than before, but with important timing constraints. ### What's supported - You can attach a volume to an instance created from a snapshot (for example, from a template). - You can attach a volume to an instance that's currently `running`. ### Current behavior and limitations - Attaching to a `running` instance isn't applied instantly. - The platform schedules the attach operation and completes it on the next scale-to-zero cycle. - This is warm-plug support, not full hot-plug support. - Detaching a volume still requires the instance to be fully shut down (not only scaled to zero). - If you request detach while the instance isn't yet shut down, the platform schedules the update and applies it automatically once shutdown happens. ### API usage note For now, volume attach and detach updates are available through dedicated endpoints: - `/v1/volumes/attach` - `/v1/volumes/detach` You can invoke these endpoints via the CLI using [`unikraft volumes attach`](/cli/unikraft/volumes/attach) and [`unikraft volumes detach`](/cli/unikraft/volumes/detach). The instance `PATCH` endpoint doesn't currently support volume operations. ## Volume templates A volume template is a volume that you can use as a base to create new volumes with the same data. When you create a volume template, the platform converts the source volume to a template. :::caution Once a volume transitions to template state, it's immutable: you can clone it but not write to it or delete it while active clones exist. ::: Volume templates have their own create, read, update, and delete endpoints at [`/volumes/templates`](/api/platform/v1/volumes#create-template-volume). They support [delete locks](/platform/delete-locks) and [tags](/platform/tagging). :::tip Volume templates are useful because you can [clone](/platform/volumes#volume-cloning) them. ::: You can create volume templates from existing volumes using the CLI as well: ```bash unikraft volume template create my-volume ``` ```ansi metro: fra name: my-volume uuid: 4d5576f7-315a-4eab-98b8-6c18fec076e0 state: template size: 100MiB filesystem: ext4 persistent: true timestamps:  created: just now ``` You can list all volume templates with: ```bash unikraft volume template list ``` ```ansi METRO NAME STATE SIZE CREATED fra my-volume template 100MiB 1 minute ago ``` :::note The platform converts attached volumes to templates automatically when you convert an instance into an [instance template](/platform/instances#instance-templates). ::: ## Volume cloning A volume clone is an independent copy of an existing volume. The clone operation is **asynchronous**: the platform creates the new volume immediately in a `pending` state, and the data copy completes in the background. To clone one or more volumes, you can use the [`POST /volumes/clone`](/api/platform/v1/volumes#clone-volumes) endpoint. You can also use the `unikraft` CLI to clone a volume: ```bash unikraft volume clone my-volume --name my-volume-clone ``` ```ansi metro: fra name: my-volume-clone uuid: 3f0625b4-cad3-4875-8b61-113aad4b02c7 state: available size: 100MiB filesystem: ext4 quota-policy: static persistent: true access-mode: rwo timestamps:  created: just now ``` :::note The platform clones volumes implicitly when you clone an instance or create one from a template. It clones each attached volume and attaches the clone to the new instance. ::: :::caution You can't delete a source volume while it has active clones. ::: :::caution You can't clone a volume template. ::: ## Filesystems By default, volumes are block devices with `ext4` filesystem. There's also a `virtiofs` filesystem option, which is a shared filesystem that allows more than one instance to read and write the same volume simultaneously. To specify the filesystem, use the `filesystem` field when creating a volume with [`POST /volumes`](/api/platform/v1/volumes#create-volume): ```json title="POST /volumes" { "name": "my-volume", "size_mb": 512, "filesystem": "virtiofs" } ``` You can also use the `unikraft` CLI: ```bash unikraft volume create --metro fra \ --name my-volume \ --size 512M \ --filesystem virtiofs ``` {/* vale off */} :::tip For BYOC or on-prem Unikraft Cloud installations, you can configure custom filesystems that can be backed by other media, such as S3 or network storage. Custom filesystem drivers support lifecycle hooks for `format`, `fscheck`, `mount`, `unmount`, and `delete`, so operators can control how storage is prepared, attached, and cleaned up. This is useful when you want shared storage semantics over non-block backends, for example exposing S3 through a POSIX layer such as JuiceFS while still integrating with the platform's volume lifecycle. ::: {/* vale on */} ### Managed volumes :::caution[**Limited Access**] Managed volumes are available as part of enterprise plans. To enable them for your account, reach out to the [Unikraft Cloud Discord](https://unikraft.com/discord) or send an email to [support@unikraft.com](mailto:support@unikraft.com). Quotas aren't yet enforced by the platform for managed volumes, so use them with caution to avoid filling up your host's disk. ::: A [managed volume](/features/managed-volumes) points to an existing directory on the host rather than a platform-allocated storage file. Create a managed volume with a `host_path` field instead of `size_mb` when calling [`POST /volumes`](/api/platform/v1/volumes#create-volume): ```json title="POST /volumes" { "name": "my-managed-volume", "host_path": "/srv/data/mydir", "uid": 1000, "gid": 1000 } ``` Restrictions apply: - `host_path` must be an absolute, normalised path to an existing directory on the host (no `.`, `..`, or `:` components). - `uid` and `gid` set the ownership used when accessing the volume from the guest. Both default to `0` if not specified. - `size_mb` and `template` are mutually exclusive with `host_path`. - The platform doesn't create, format, resize, or delete the backing directory for managed volumes. ## Access modes On creation, you can specify an `access_mode` that controls how instances can access or share the volume: {/* vale off */} | Access mode | Description | | ----------- | ----------------------------------------------------------------------------------------------- | | `rwo` | Read-write once. The volume can only be mounted in read-write mode by a single instance at a time. This is the default access mode. | | `rox` | Read-only many. The volume can be mounted in read-only mode by multiple instances simultaneously. | | `rwx` | Read-write many. The volume can be mounted in read-write mode by multiple instances simultaneously. This access mode is only supported for volumes with `virtiofs` filesystem. | {/* vale on */} You can specify the access mode when [creating a volume](/api/platform/v1/volumes#create-volume) with the `access_mode` field: ```json title="POST /volumes" { "name": "my-volume", "size_mb": 512, "access_mode": "rwo" } ``` You can also use the `unikraft` CLI: ```bash unikraft volume create --metro fra \ --name my-volume \ --size 512M \ --access-mode rwo ``` ### Read-only many More than one instance can mount the same `rox` volume simultaneously as long as all mounts are in *read-only* mode. Read-only sharing is always permitted regardless of how many other instances have the volume mounted. Specify `readonly: true` when attaching a volume at [instance creation](/api/platform/v1/instances#create-instance) or via the [attach endpoint](/api/platform/v1/volumes#attach-volumes): ```json title="POST /instances" { ... "volumes": [ { "name": "my-shared-volume", "at": "/data", "readonly": true } ], ... } ``` ### Read-write many Read-write many (`rwx`) volumes are only supported for volumes with the `virtiofs` filesystem. The downside is that `virtiofs` volumes are slower than block volumes, so only use them for `rwx` or [managed volumes](/platform/volumes#managed-volumes). To create a `rwx` volume, you can use the [`POST /volumes`](/api/platform/v1/volumes#create-volume) endpoint: ```json title="POST /volumes" { "name": "my-volume", "size_mb": 512, "access_mode": "rwx", "filesystem": "virtiofs" } ``` You can also use the `unikraft` CLI: ```bash unikraft volume create --metro fra \ --name my-volume \ --size 512M \ --filesystem virtiofs \ --access-mode rwx ``` ## Learn more * The [CLI reference](/docs/cli/unikraft). * Unikraft Cloud's [REST API reference](/api/platform/v1), in particular the section on [volumes](/api/platform/v1/volumes). * [Tutorial](/tutorials/rootfs-volumes-roms) explaining the difference of storage media on Unikraft Cloud. --- ## Document: Troubleshooting URL: /docs/platform/troubleshooting # Troubleshooting This guide explains how to debug apps on Unikraft Cloud so you can fix issues or collect enough information for the support team to help you. It follows platform best practices and supports efficient troubleshooting. If you need help, reach out to Unikraft Cloud Support on [Discord](https://unikraft.com/discord) or write to [support@unikraft.cloud](mailto:support@unikraft.cloud). ## Debugging running apps The most direct way to debug an app is to use the app console output, which may include kernel output. To see it, after starting the Unikraft Cloud instance, use: ```bash unikraft instances logs ``` In case of a crash, you'll see a full crash output: ```ansi title="" Powered by Unikraft Telesto (0.16.2~9c264902) [ 0.066949] CRIT: [libukvmem] Cannot handle write page fault at 0x1000bb8024 (ec: 0x2): -12 [ 0.067680] CRIT: [libkvmplat] page fault handler returned error: -12 [ 0.068227] CRIT: [libkvmplat] Unhandled page fault vaddr=0x1000bb8024, error code=0x2 [ 0.068910] CRIT: [appelfloader] Unikraft crash - Telesto (0.16.2~9c264902) [ 0.069503] CRIT: [appelfloader] Thread "python3"@0x4017e4020 [ 0.069993] CRIT: [appelfloader] RIP: 0008:000000100041fdcb [ 0.070468] CRIT: [appelfloader] RSP: 0010:000000100017f760 EFLAGS: 00010206 ORIG_RAX: 0000000000000002 [ 0.071269] CRIT: [appelfloader] RAX: 0000001000bb8000 RBX: 00000004018012f0 RCX:0000000000000007 [ 0.072040] CRIT: [appelfloader] RDX: 0000000000000000 RSI: aaaaaaaaaaaaaaab RDI:00000010007bccc0 [ 0.072793] CRIT: [appelfloader] RBP: 000000000000001a R08: 0000000000000001 R09:00000010004e2b99 [ 0.073558] CRIT: [appelfloader] R10: bb9e744ce6503a27 R11: 0000000401861290 R12:00000010007bccc0 [ 0.074312] CRIT: [appelfloader] R13: 00000010007bbd48 R14: 000000100017f890 R15:0000000000000038 [ 0.075076] CRIT: [appelfloader] Stack: [ 0.075406] CRIT: [appelfloader] 100017f760 00 00 00 00 00 00 00 00 |........| [ 0.076043] CRIT: [appelfloader] 100017f768 a1 01 00 00 00 00 00 00 |........| [ 0.076680] CRIT: [appelfloader] 100017f770 48 bd 7b 00 10 00 00 00 |H.{.....| [ 0.077324] CRIT: [appelfloader] 100017f778 e0 92 7a 00 10 00 00 00 |..z.....| [ 0.077959] CRIT: [appelfloader] 100017f780 48 bd 7b 00 10 00 00 00 |H.{.....| [ 0.078596] CRIT: [appelfloader] 100017f788 90 f8 17 00 10 00 00 00 |........| [ 0.079233] CRIT: [appelfloader] 100017f790 38 00 00 00 00 00 00 00 |8.......| [ 0.079870] CRIT: [appelfloader] 100017f798 72 fc 41 00 10 00 00 00 |r.A.....| [ 0.080504] CRIT: [appelfloader] Call Trace: [ 0.080871] CRIT: [appelfloader] [0x000000100041fdcb] [ 0.081311] CRIT: [appelfloader] Bad frame pointer ``` or even ```ansi title="" [ 0.850992] Initramfs unpacking failed: write error ``` Inspect the instance details to see the stop reason: ```bash unikraft instances get ``` This yields output like: ```ansi metro: fra name: node-playwright-chromium-nwa95 uuid: 59cd6c94-611c-4ca8-911f-78c7db95ad12 state: stopped image: /node-playwright-chromium resources:  memory: 2.441GiB  vcpus: 1 service:  name: divine-dust-73sxgnb6  uuid: 3c7ce2b4-ae86-4a21-8562-55033d5409d5  domains:  - fqdn: divine-dust-73sxgnb6.fra0-fe-test.unikraft.app networks: - uuid: 62982265-6521-401c-878a-d7e31804c63c  private-ip: 10.0.0.109  mac: 12:b0:0a:00:00:6d timestamps:  created: 2 days ago scale-to-zero:  enabled: true  policy: on  cooldown-time: 1s stop:  reason: kernel crash: out of memory (ENOMEM) ``` This output shows the [stop reason](/platform/instances#stop-reason). Here the cause is insufficient memory. If the stop reason lacks detail, enable debug tracing for the instance. To do that, pick an app from the [examples repository](https://github.com/unikraft-cloud/examples/) or an app directory you created. Update the `runtime` entry in the `Kraftfile` to reference the debug build by adding `-dbg` to the runtime name. For example, if you want to run the [httpserver-go1.21 example](https://github.com/unikraft-cloud/examples/tree/main/httpserver-go1.21) with debug output, update its `Kraftfile` as follows: ```yaml title="Kraftfile" spec: v0.7 runtime: base-compat:latest-dbg targets: - kraftcloud/x86_64 rootfs: source: ./Dockerfile format: erofs cmd: ["/server"] ``` That is, change `base-compat:latest` to `base-compat:latest-dbg`. Now you're ready to re-deploy: ```bash unikraft build . --output /http-go-strace:latest unikraft run --metro fra \ --name http-go-strace \ -p 443:8080/http+tls \ --image /http-go-strace:latest ``` You can now inspect the logs as before and view system call tracing: ```bash unikraft instances logs http-go-strace ``` This mechanism works for all apps and runtimes. Contact Unikraft Cloud on the [Discord server](/discord) and include this output if you need more detail. :::tip While you debug an issue you can mitigate crashes by setting a restart policy. For example, use the CLI to set `--restart on-failure` so the platform restarts the app if it crashes. ```bash unikraft run --metro fra --restart on-failure --image my-app:latest ``` ::: ## Debugging the build and push steps The CLI performs steps on your local device before actually deploying your app to Unikraft Cloud: 1. Downloads the runtime image (defined in the `Kraftfile`) from the Unikraft Cloud registry. 1. Builds the app filesystem using the `Dockerfile` via [BuildKit](https://unikraft.org/guides/building-dockerfile-images-with-buildkit). 1. Packages the app filesystem and the runtime in an Open Container Initiative (OCI) image. 1. Pushes the OCI image to the to the Unikraft Cloud registry under your username's namespace. If any of these steps fail, enable CLI debugging with the `--log-level` and `--log-type` flags: ```bash unikraft build --log-level debug --log-type text ... ``` You should then see debug output for the 4 steps above. This output should give enough context to diagnose the issue. ## Caching the app's filesystem for faster builds When using a `Dockerfile` for the app filesystem, Unikraft Cloud passes the commands to [BuildKit](https://docs.docker.com/build/buildkit/). The `unikraft` CLI uses the first BuildKit builder that it finds, in this order: 1. The builder at the address in the `BUILDKIT_HOST` environment variable. 1. The builder on the default BuildKit socket. 1. The builder in Docker. The builder keeps its cache between builds. To control where the cache stays, start a BuildKit container and set `BUILDKIT_HOST` to its address: ```bash docker run -d --name buildkitd --privileged moby/buildkit:latest export BUILDKIT_HOST=docker-container://buildkitd ``` The approach above caches builds in the BuildKit container filesystem. Another approach saves the cache in a local host directory: ```bash title="" docker run -d --name buildkitd --privileged -v $HOME/.buildkit-cache:/var/lib/buildkit moby/buildkit:latest export BUILDKIT_HOST=docker-container://buildkitd ``` Here `$HOME/.buildkit-cache` is a local path on your machine where BuildKit stores cache data. ## Large filesystem build process gets stuck When the filesystem is larger than about `800 MB`, the build may get stuck. A limitation in [BuildKit](https://docs.docker.com/build/buildkit/), the current filesystem build component, causes the issue. Work around this by reducing the filesystem image below `800 MB`. Work is in progress to integrate a component without this limitation. ## Gateway error when accessing a service When you query a Unikraft Cloud service via its public address, such as: ```bash curl https://frosty-bobo-zeev783o.fra.unikraft.app ``` you may get a `Bad Gateway` response: ```html title="" 502 Bad Gateway

502 Bad Gateway


openresty
``` This happens when you specify the wrong internal app port (for example, the app exposes port 8080 but you use `443:80` instead of `443:8080`). Another error response you may get is: ```html title="" Service not found There is no service on this URL. ``` This happens when you connect with an HTTPS client (port `443`) and the app doesn't expose that port. Database services such as [MongoDB](/guides/mongodb) or [MariaDB](/guides/mariadb) use different ports (for example, `27017`, `3306`). Use the correct exposed port. You may need a TLS tunnel (see below). If you still have issues, it might mean that the app fails to start or crashes immediately. In that case, inspect the [app logs](/platform/troubleshooting#debugging-running-apps) first. ## Connecting to a non-TLS app Unikraft Cloud uses TLS to expose services to the outside world. Some apps (such as [MongoDB](/guides/mongodb) or [MariaDB](/guides/mariadb)) don't use TLS. Create a TLS tunnel with the [`unikraft instances tunnel`](/cli/unikraft/instances/tunnel) command or with `socat`. Both open a local endpoint (`localhost` / `127.0.0.1`) and forward the traffic over TLS. See the [MariaDB guide](/guides/mariadb) for an example. ## Launched app not visible in list `unikraft instances list` shows instances from every metro in the current profile. The most common reason is that you deployed the app with a different profile, or to a metro that isn't in this profile. `instances list` has no `--metro` flag, because `--metro` only selects where `create` and `run` deploy. To show only one metro, filter on it: ```bash unikraft instances list --filter metro== ``` ## Learn more * The [CLI reference](/docs/cli/unikraft). * Unikraft Cloud's [REST API reference](/api/platform/v1). * Many more guides [here](/guides/overview). --- ## Document: Tagging URL: /docs/platform/tagging # Tagging import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" Tags are labels that you can attach to instances, volumes, instance templates, and volume templates to organize and filter your resources. Don't confuse them with [image](/platform/images) tags which serve a different purpose. ## Tag format Tags can be up to 256 characters long and may contain alphanumeric characters plus `-`, `+`, `_`, `.`, `:`, and `=`. Each resource ([instance](/platform/instances), [volume](/platform/volumes), [instance template](/platform/instances#instance-templates), [volume template](/platform/volumes#volume-templates)) can have up to 16 tags. ## Adding tags Add tags to a resource at creation time: Instance Instance Template Volume Volume Template ```bash title="unikraft" unikraft instance create \ --metro fra \ --image ... \ --memory ... \ --tag production \ --tag customer_A ``` ```bash title="API" curl -X POST \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Content-Type: application/json" \ "https://api.fra.unikraft.cloud/v1/instances" \ -d '{ "image": "...", "memory_mb": ..., "tags": ["production", "customer_A"] }' ``` Converting an instance relabels it in place, so the template keeps the instance's tags. Tag the instance first, then convert it: ```bash title="unikraft" unikraft instance template create ``` ```bash title="API" curl -X POST \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Content-Type: application/json" \ "https://api.fra.unikraft.cloud/v1/instances/templates" \ -d '{ "name": "" }' ``` ```bash title="unikraft" unikraft volume create \ --metro fra \ --size ... \ --tag production \ --tag customer_A ``` ```bash title="API" curl -X POST \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Content-Type: application/json" \ "https://api.fra.unikraft.cloud/v1/volumes" \ -d '{ "size_mb": ..., "tags": ["production", "customer_A"] }' ``` Converting a volume relabels it in place, so the template keeps the volume's tags. Tag the volume first, then convert it: ```bash title="unikraft" unikraft volume template create ``` ```bash title="API" curl -X POST \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Content-Type: application/json" \ "https://api.fra.unikraft.cloud/v1/volumes/templates" \ -d '{ "name": "" }' ``` Add tags to an existing resource (`"op": "set"` will replace all existing tags): Instance Instance Template Volume Volume Template ```bash title="unikraft" unikraft instance edit \ --tag testsystem \ --tag customer_B ``` ```bash title="API" curl -X PATCH \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Content-Type: application/json" \ "https://api.fra.unikraft.cloud/v1/instances" \ -d '[{ "name": "", "prop": "tags", "op": "set", "value": ["testsystem", "customer_B"] }]' ``` ```bash title="unikraft" unikraft instance template edit \ --tag testsystem \ --tag customer_B ``` ```bash title="API" curl -X PATCH \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Content-Type: application/json" \ "https://api.fra.unikraft.cloud/v1/instances/templates" \ -d '[{ "name": "", "prop": "tags", "op": "set", "value": ["testsystem", "customer_B"] }]' ``` ```bash title="unikraft" unikraft volume edit \ --tag testsystem \ --tag customer_B ``` ```bash title="API" curl -X PATCH \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Content-Type: application/json" \ "https://api.fra.unikraft.cloud/v1/volumes" \ -d '[{ "name": "", "prop": "tags", "op": "set", "value": ["testsystem", "customer_B"] }]' ``` ```bash title="unikraft" unikraft volume template edit \ --tag testsystem \ --tag customer_B ``` ```bash title="API" curl -X PATCH \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Content-Type: application/json" \ "https://api.fra.unikraft.cloud/v1/volumes/templates" \ -d '[{ "name": "", "prop": "tags", "op": "set", "value": ["testsystem", "customer_B"] }]' ``` Add tags to a resource without replacing existing ones (`"op": "add"`): Instance Instance Template Volume Volume Template ```bash title="unikraft" unikraft instance edit --add tags=legacy ``` ```bash title="API" curl -X PATCH \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Content-Type: application/json" \ "https://api.fra.unikraft.cloud/v1/instances" \ -d '[{ "name": "", "prop": "tags", "op": "add", "value": ["legacy"] }]' ``` ```bash title="unikraft" unikraft instance template edit --add tags=legacy ``` ```bash title="API" curl -X PATCH \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Content-Type: application/json" \ "https://api.fra.unikraft.cloud/v1/instances/templates" \ -d '[{ "name": "", "prop": "tags", "op": "add", "value": ["legacy"] }]' ``` ```bash title="unikraft" unikraft volume edit --add tags=legacy ``` ```bash title="API" curl -X PATCH \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Content-Type: application/json" \ "https://api.fra.unikraft.cloud/v1/volumes" \ -d '[{ "name": "", "prop": "tags", "op": "add", "value": ["legacy"] }]' ``` ```bash title="unikraft" unikraft volume template edit --add tags=legacy ``` ```bash title="API" curl -X PATCH \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Content-Type: application/json" \ "https://api.fra.unikraft.cloud/v1/volumes/templates" \ -d '[{ "name": "", "prop": "tags", "op": "add", "value": ["legacy"] }]' ``` :::note Tags are automatically deduplicated. Adding a tag already attached to a resource has no effect. ::: ## Removing tags Remove specific tags from a resource (`"op": "del"`): Instance Instance Template Volume Volume Template ```bash title="unikraft" unikraft instance edit --del tags=legacy ``` ```bash title="API" curl -X PATCH \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Content-Type: application/json" \ "https://api.fra.unikraft.cloud/v1/instances" \ -d '[{ "name": "", "prop": "tags", "op": "del", "value": ["legacy"] }]' ``` ```bash title="unikraft" unikraft instance template edit --del tags=legacy ``` ```bash title="API" curl -X PATCH \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Content-Type: application/json" \ "https://api.fra.unikraft.cloud/v1/instances/templates" \ -d '[{ "name": "", "prop": "tags", "op": "del", "value": ["legacy"] }]' ``` ```bash title="unikraft" unikraft volume edit --del tags=legacy ``` ```bash title="API" curl -X PATCH \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Content-Type: application/json" \ "https://api.fra.unikraft.cloud/v1/volumes" \ -d '[{ "name": "", "prop": "tags", "op": "del", "value": ["legacy"] }]' ``` ```bash title="unikraft" unikraft volume template edit --del tags=legacy ``` ```bash title="API" curl -X PATCH \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Content-Type: application/json" \ "https://api.fra.unikraft.cloud/v1/volumes/templates" \ -d '[{ "name": "", "prop": "tags", "op": "del", "value": ["legacy"] }]' ``` ## Filtering by tag Filter the [instance list](/api/platform/v1/instances#list-instances), [volume list](/api/platform/v1/volumes#list-volumes), [instance template list](/api/platform/v1/instances#list-template-instances), or [volume template list](/api/platform/v1/volumes#list-template-volumes) by one or more tags. Only resources that have _all_ the specified tags appear in the results. Instance Instance Template Volume Volume Template ```bash title="unikraft" unikraft instance list --field +tags --filter 'tags.*==production,tags.*==customer_A' ``` ```bash title="API" curl -H "Authorization: Bearer ${UKC_TOKEN}" \ "https://api.fra.unikraft.cloud/v1/instances?tags=production,customer_A" ``` ```bash title="unikraft" unikraft instance template list --field +tags --filter 'tags.*==production,tags.*==customer_A' ``` ```bash title="API" curl -H "Authorization: Bearer ${UKC_TOKEN}" \ "https://api.fra.unikraft.cloud/v1/instances/templates?tags=production,customer_A" ``` ```bash title="unikraft" unikraft volume list --field +tags --filter 'tags.*==production,tags.*==customer_A' ``` ```bash title="API" curl -H "Authorization: Bearer ${UKC_TOKEN}" \ "https://api.fra.unikraft.cloud/v1/volumes?tags=production,customer_A" ``` ```bash title="unikraft" unikraft volume template list --field +tags --filter 'tags.*==production,tags.*==customer_A' ``` ```bash title="API" curl -H "Authorization: Bearer ${UKC_TOKEN}" \ "https://api.fra.unikraft.cloud/v1/volumes/templates?tags=production,customer_A" ``` In the API, separate tags by commas (`,`) in the `tags` query parameter. In the CLI, write one `tags.*==` selector per tag inside a single `--filter`, separated by commas: every selector must match. `tags==production,customer_A` doesn't work: the comma starts a second selector, so the CLI prints an empty list without an error. The default list table doesn't show tags, so `--field +tags` adds the column. Elsewhere, pass one tag per `--tag` flag—the CLI no longer splits a flag value on commas. ## Learn more * Unikraft Cloud's [REST API reference](/api/platform/v1). --- ## Document: Services URL: /docs/platform/services # Services import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" On creation, Unikraft Cloud assigns each instance a private IP address and private FQDN for internal connectivity, of the form `.internal`. To connect to an instance from the Internet, you need to create a service and attach the instance to it. A service defines how to reach a group of instances from the outside world. It maps an external FQDN port to an internal port common to all instances within the service. The platform [load balances](/features/load-balancing) incoming connections across instances. :::caution Avoid placing apps that expose different ports in the same service. ::: {/* vale off */} :::tip[Drive services from your own code] The [SDKs](/sdks/overview) wrap the service API, so a service group is declared where the instances that sit behind it are: ```ts await ukc.metro("fra").services.create({ services: [{ port: 443, handlers: ["tls", "http"], destination_port: 8080 }], }); ``` See the [JavaScript](/sdks/js), [Python](/sdks/python) and [Go](/sdks/go) SDKs. ::: {/* vale on */} ## Instance service groups If you specify a port mapping in the CLI deployment commands, the platform creates a service for you and attaches the instance to it: ```bash unikraft run --metro fra \ -m 256M \ -p 443:8080/http+tls \ --image /nginx:latest ``` ```ansi metro: fra name: nginx-67zbu uuid: 8a8bc1b9-0af6-420e-a426-190dc2da9eaa state: starting image: /nginx resources:  memory: 256MiB  vcpus: 1 service:  uuid: a942b9b5-ad17-3ffe-dcd2-ef4331f9087a  name: nameless-fog-0tvh1uov # <---- service name  domains:  - fqdn: nameless-fog-0tvh1uov.fra.unikraft.app # <---- service domain networks: - uuid: 62d9bbf0-aec8-61f6-7bdb-86edf63dd068  private-ip: 10.0.3.3  mac: 12:b0:c6:23:ed:15 timestamps:  created: just now ``` This ties the service to the lifetime of the instance, so the platform deletes it when you delete the instance. ## Persistent service groups The rest of this guide shows how to create a service first, then use the CLI to create and attach instances to it. First, create a new service with the CLI: ```bash unikraft services create --metro fra \ --name my-service \ --service 443:8080/http+tls ``` ```ansi metro: fra name: my-service uuid: 9bfe76ea-2679-482a-954e-577de7d122b3 persistent: true limits:  soft: 1  hard: 65535 timestamps:  created: just now domains: - fqdn: my-service-pc4xjplc.fra.unikraft.app services: - source: 443  destination: 8080  handlers: ["tls", "http"] ``` This creates a new service named `my-service` listening on port 443. Unikraft Cloud terminates TLS and sends HTTP traffic to the app's port 8080. This example assumes that the app opens port 8080. Now use the CLI with the service flag to attach the instance to the `my-service` service. For example, from the [Go HTTP server guide](/guides/httpserver-go1.21): ```bash git clone https://github.com/unikraft-cloud/examples cd examples/httpserver-go1.21/ unikraft build . --output /httpserver-go121:latest unikraft run --metro fra \ -m 256M \ --service my-service \ --image /httpserver-go121:latest ``` This creates a new Go web server instance and immediately attaches it to the `my-service` service. The output shows the instance address and other details: ```ansi metro: fra name: httpserver-go121-9a2wv uuid: 8bb34040-9434-4a28-bd1e-c24ee532e2da state: starting image: /httpserver-go121 resources:  memory: 256MiB  vcpus: 1 service:  uuid: 9bfe76ea-2679-482a-954e-577de7d122b3  name: my-service  domains:  - fqdn: my-service-pc4xjplc.fra.unikraft.app networks: - uuid: 51f79dc1-e989-908d-894b-bdf0a87e7901  private-ip: 10.0.3.3  mac: 12:b0:57:91:bb:a5 timestamps:  created: just now ``` In this case, the instance name is `httpserver-go121-9a2wv`. Notice that the service ID is the same from the service created earlier, and so is the FQDN, which is `https://my-service-pc4xjplc.fra.unikraft.app`. Use `curl` to query the Go HTTP server: ```bash curl https://my-service-pc4xjplc.fra.unikraft.app ``` ```text title="" Hello, world! ``` That's it. In the end, if you want to remove a service, use: ```bash unikraft services delete my-service ``` ## Handlers Handlers define how the service will handle incoming connections and forward traffic from the Internet to your app. For example, you can configure a service to end TLS connections, redirect HTTP traffic, or enable HTTP mode for load balancing. You configure the handlers for every published service port individually. Currently, there are 3 supported handlers: tls http redirect Terminate the TLS connection at the Unikraft Cloud gateway using the wildcard certificate issued for the `unikraft.cloud` domain. The gateway forwards the unencrypted traffic to your app. Enable HTTP mode on the load balancer to load balance on the level of individual HTTP requests. In this mode, the load balancer accepts only HTTP connections. If you don't set this option, the load balancer works in TCP mode and distributes TCP connections. Redirect traffic from the source port to the destination port. :::note The following set of constraints apply when publishing ports: - Port 80: **must** have `http` and **must not** have `tls` set. - Port 443: **must** have `http` and `tls` set. - You can only set the `redirect` handler on port 80 (HTTP) to redirect to port 443 (HTTPS). - All other ports **must** have `tls` and **must not** have `http` set. ::: For example, the following creates the service `my-service` with three published ports: - Port 443 with both `http` and `tls` handlers (HTTP mode). - Port 80 with the `http` and `redirect` handlers, redirecting to port 443 (HTTP mode). - Port 10000 with only the `tls` handler (TCP mode). ```bash unikraft services create --metro fra \ --name my-service \ --service 443:8080/tls+http \ --service 80:443/http+redirect \ --service 10000:10000/tls ``` {/* vale off */} ## UDP services {/* vale on */} :::caution[**Limited Access**] UDP services are only available in BYOC or on-prem Unikraft Cloud installations. CLI support is coming soon. ::: You can use UDP for both internal VM-to-VM traffic and external traffic. To create a UDP service, use `POST /services` and set both `protocol` and `ip`: ```json title="POST /services" { "services": [ { "port": 7777, "destination_port": 8080, "protocol": "udp", "ip": "a.b.c.d" } ] } ``` The IP address must be one of the IP addresses allocated to your user. ## Connection limits Every service group has a soft and a hard connection limit that the load balancer enforces. | Field | Default | Range | Description | |-------|---------|-------|-------------| | `soft_limit` | `1` | `1-65535` | The load balancer starts queuing new connections once the number of active connections reaches this value. | | `hard_limit` | `65535` | `1-65535` | The load balancer rejects new connections once the number of active connections reaches this value. | `soft_limit` must be less than or equal to `hard_limit`. Setting `soft_limit > hard_limit` returns a `400` error. You can use the CLI to set the connection limits for a service: ```bash unikraft services create --metro fra \ --name my-service \ --service 443:8080/http+tls \ --soft-limit 5 \ --hard-limit 10 ``` * The [CLI reference](/docs/cli/unikraft). * Unikraft Cloud's [REST API reference](/api/platform/v1), in particular the section on [service groups](/api/platform/v1/servicegroups). --- ## Document: Quotas Understand and track your Unikraft Cloud resource quotas. URL: /docs/platform/quotas # Quotas Unikraft Cloud enforces per-account resource quotas to govern how many instances, service groups, volumes, and compute resources you can provision. ```bash title="unikraft" unikraft quotas ``` ```bash title="API" curl -X GET \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Content-Type: application/json" \ "https://api.fra.unikraft.cloud/v1/users/quotas" ```
```ansi title="unikraft"  fra  user uuid: user name: image storage: ████████████████████████████████████ 702 MiB/1.0 GiB active instances: ████████████████████████████████████ 5/64 total instances: ████████████████████████████████████ 5/64 active vcpus: ████████████████████████████████████ 5/64 vcpu limit: 1-1 active used memory: ████████████████████████████████████ 2.5GiB/32GiB memory size limits: 16MiB-8GiB exposed services: ████████████████████████████████████ 10/253 services: ████████████████████████████████████ 5/253 volumes: enabled active volumes: ████████████████████████████████████ 0/253 used volume space: ████████████████████████████████████ 0B/20GiB volume size limits: 8MiB-20GiB autoscale: enabled autoscale limit: 0-16 scale-to-zero: enabled  Tab/←→: switch metro q: quit ``` ```json title="API" { "status": "success", "data": { "quotas": [ { "status": "success", "uuid": "", "used": { "instances": 5, "live_instances": 5, "live_vcpus": 5, "live_memory_mb": 2560, "service_groups": 5, "services": 10, "volumes": 0, "total_volume_mb": 0 }, "hard": { "instances": 64, "live_instances": 64, "live_vcpus": 64, "live_memory_mb": 32768, "service_groups": 253, "services": 253, "volumes": 253, "total_volume_mb": 20480 }, "limits": { "min_memory_mb": 16, "max_memory_mb": 8192, "min_vcpus": 1, "max_vcpus": 1, "min_volume_mb": 8, "max_volume_mb": 20480, "min_autoscale_size": 0, "max_autoscale_size": 16 } } ] }, "op_time_us": 46 } ``` :::note The `hard.live_instances` field is a compatibility alias for `hard.live_vcpus`. It's kept for backward compatibility and the platform may drop it in a future API version. Prefer `hard.live_vcpus`. ::: ## Response structure The response contains a top-level `quotas` array. Each element has three sections: `used`, `hard`, and `limits`. ### `used`: Current resource usage | Field | Type | Description | |-------|------|-------------| | `instances` | int | Total number of instances (stopped + running). | | `live_instances` | int | Number of currently running instances. | | `live_vcpus` | int | Number of vCPUs in use across running instances. | | `live_memory_mb` | int | Memory (MB) in use across running instances. | | `service_groups` | int | Number of allocated service groups. | | `services` | int | Number of allocated services (published ports). | | `volumes` | int | Number of allocated volumes. | | `total_volume_mb` | int | Total volume storage in use (MB). | ### `hard`: Maximum allowed allocations | Field | Type | Description | |-------|------|-------------| | `instances` | int | Maximum number of instances. | | `live_vcpus` | int | Maximum vCPUs running simultaneously. | | `live_memory_mb` | int | Maximum memory (MB) running simultaneously. | | `service_groups` | int | Maximum number of service groups. | | `services` | int | Maximum number of services. | | `volumes` | int | Maximum number of volumes. | | `total_volume_mb` | int | Maximum total volume storage (MB). | ### `limits`: Per-resource range constraints These define the minimum and maximum values you can specify when creating individual resources. | Field | Type | Description | |-------|------|-------------| | `min_memory_mb` | int | Minimum memory (MB) per instance. | | `max_memory_mb` | int | Maximum memory (MB) per instance. | | `min_vcpus` | int | Minimum vCPUs per instance. | | `max_vcpus` | int | Maximum vCPUs per instance. | | `min_volume_mb` | int | Minimum volume size (MB). | | `max_volume_mb` | int | Maximum volume size (MB). | | `min_autoscale_size` | int | Minimum autoscale group size. | | `max_autoscale_size` | int | Maximum autoscale group size. | ## Learn more * Unikraft Cloud's [REST API reference](/api/platform/v1), in particular the [quotas endpoint](/api/platform/v1/users#get-current-user-quotas). --- ## Document: Networking URL: /docs/platform/networking # Networking The platform separates Unikraft Cloud instances on a per-user level. This means that all instances within a user's scope can "see" each other on the private internal network and communicate with each other. This communication is private and unencrypted, and doesn't reach the outside world. You can communicate internally by using the Private IP or Private FQDN. At the same time, when using the Public FQDN the connection goes through the external load balancer applied to every public instance. This means that an entirely different code path resolves the connection, and also the connection is always TLS encrypted. ## External communication External communication refers to every request going over the public FQDN of an instance. This means that the request goes through the external load balancer, and as such, TLS encryption is mandatory. This also means that the instance will be load balanced with all other public instances behind the same [service](/features/load-balancing). The request will do a round trip from the client through the load balancer and to the instance. This communication is best suited for frontend-to-service communication, public APIs, web services, and more. ## Internal communication Internal communication refers to every request going over a user's private network. This means that the request stays within the user internal network, and doesn't go through the external load balancer. This also means that the system won't encrypt the connection by default. This communication is best suited for service-to-service communication, microservices, databases, caches, and more. For example, you wouldn't want to expose your database publicly, but you would want your app to be able to connect to it internally. :::tip Even when using internal communication, scale-to-zero triggers will still work as expected, and your instances will scale down to zero when not in use. ::: Every instance, by default, receives a private IP and a private FQDN (of the format `.internal`). You can see the private IP of your instance by grabbing its details: ```bash unikraft instance get ``` ```ansi ... networks: - uuid: afbd8ce2-daf5-4c47-888a-96bbf1c4a347  private-ip: 10.0.0.49  mac: 12:b0:0a:00:00:31 ... ``` The private FQDN isn't in the default output of `instance get` or `instance list`. Show it as `private-fqdn` with `-o long`, or select it with `-f`: ```bash unikraft instance list -f name,private-fqdn ``` Check out this [guide](/guides/httpserver-flask-redis) for an example of how to use internal communication to connect a Flask app to a Redis instance, without exposing the latter to the internet. :::tip When you use the [`unikraft instances tunnel`](/cli/unikraft/instances/tunnel) command to connect to your instances, the command uses the internal network to reach your instance. The command creates a public-facing instance that tunnels your requests to the private instance over the internal network. This is a good way to keep your instances private while still being able to access them from your local machine. ::: ## Learn more * [Load balancing](/features/load-balancing) of services. * [Platform services](/platform/services) and how they work. * [Scale to zero triggers](/tutorials/scale-to-zero-triggers) for automatic instance scaling. * Deploy a [Go HTTP server with Redis](/guides/httpserver-go1.22-redis) using internal communication. * The [CLI reference](/docs/cli/unikraft). --- ## Document: Metros URL: /docs/platform/metros # Metros By default, at present, Unikraft Cloud supports the following metros: | IATA code | | Region | |:-----------|-|:---------------------------| | `dal` | 🇺🇸 | Dallas, TX, USA | | `fra` | 🇩🇪 | Frankfurt, DE | | `sfo` | 🇺🇸 | San Francisco, USA | | `sin` | 🇸🇬 | Singapore | | `was` | 🇺🇸 | Washington DC, USA | You can list the metros available to your account via the CLI: ```bash unikraft metros list ``` {/* vale off */} :::tip[One client, every metro] The [SDKs](/sdks/overview) treat the metros in scope as one namespace: reads ask each of them at once and merge the answers, tagged with where they came from, so no list of endpoints is kept in your code. ```ts for await (const inst of ukc.instances.list()) console.log(inst.metro, inst.name); ``` Name a metro — `ukc.metro("fra")`, or `UKC_METRO` — to pin a client to one. See the [JavaScript](/sdks/js) and [Python](/sdks/python) SDKs. The [Go SDK](/sdks/go) targets one metro per client, so group results across metros yourself. ::: {/* vale on */} Old metros such as `fra0` are nearing end of life, and aren't available for new deployments. Unikraft Cloud continues to add new locations worldwide so you can provision services close to your users. If there's a metro location you'd need, please contact Unikraft Cloud on [Discord](https://unikraft.com/discord) or write to [support@unikraft.cloud](mailto:support@unikraft.cloud). The list of potential metros is: | IATA code | | Region | |:-----------|-|:---------------------------| | `ams` | 🇳🇱 | Amsterdam, Netherlands | | `atl` | 🇺🇸 | Atlanta, GA, USA | | `bom` | 🇮🇳 | Mumbai, India | | `dal` | 🇺🇸 | Dallas, TX, USA | | `dub` | 🇮🇪 | Dublin, NI | | `fra` | 🇩🇪 | Frankfurt, DE | | `hel` | 🇫🇮 | Helsinki, Finland | | `hkg` | 🇭🇰 | Hong Kong | | `lon` | 🇬🇧 | London, GB | | `lax` | 🇺🇸 | Los Angeles, CA, USA | | `mad` | 🇪🇸 | Madrid, Spain | | `man` | 🇬🇧 | Manchester, UK | | `mel` | 🇦🇺 | Melbourne, Australia | | `mia` | 🇺🇸 | Miami, FL, USA | | `ord` | 🇺🇸 | Chicago, IL, USA | | `osa` | 🇯🇵 | Osaka, Japan | | `par` | 🇫🇷 | Paris, France | | `sao` | 🇧🇷 | Sao Paulo, Brazil | | `sea` | 🇺🇸 | Seattle, WA, USA | | `sel` | 🇰🇷 | Seoul, South Korea | | `sjc` | 🇺🇸 | Silicon Valley, CA, USA | | `sin` | 🇸🇬 | Singapore | | `sto` | 🇸🇪 | Stockholm, Sweden | | `syd` | 🇦🇺 | Sydney, Australia | | `tyo` | 🇯🇵 | Tokyo, Japan | | `was` | 🇺🇸 | Washington DC, USA | | `yul` | 🇨🇦 | Montreal, QC, Canada | | `yyz` | 🇨🇦 | Toronto, OT, Canada | ## Intra-data center provider connectivity For more information on how Unikraft Cloud connects metros together, and for more information about how to reduce in/egress costs with existing deployments at other IaaS providers, please [get in touch](mailto:support@unikraft.cloud). ## Self-hosted/on-premise metros If you require running solutions in-house or on-premise, either for SOC/ISO/HIPPA compliance or for other security, performance or cost-saving reasons, please [get in contact](mailto:support@kunikraft.cloud) for more details. ## Usage You can set the metro you wish to use by setting the `--metro` flag. Use the IATA code descriptor to target the specific metro. For example with `fra`: ```bash unikraft run --metro fra \ -p 443:8080/http+tls \ --image /nginx:latest ``` For on-premise or enterprise users, you need to set the API endpoint of the metro you wish to use. To do this, define a custom [profile](/tutorials/kraftkit-to-unikraft#profile-management) and give an identifier to the metro you wish to use: ```bash unikraft profile use unikraft run --metro ... ``` --- ## Document: Metrics Retrieve runtime statistics for your Unikraft Cloud instances. URL: /docs/platform/metrics # Metrics import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" Unikraft Cloud exposes runtime metrics for each instance: CPU time, memory usage, network I/O, connection counts, and scale-to-zero wakeup latencies. :::caution You can currently only view metrics via the [API](/api/platform/v1/instances#get-instances-metrics) or [`unikraft api`](/cli/unikraft/api). ::: ## Endpoints ```text GET /instances/{uuid}/metrics POST /instances/{uuid}/metrics # (for clients that must send a body) GET /instances/metrics?uuid=[,…] POST /instances/metrics # (UUIDs or names in request body) ``` With no identifiers, the platform returns metrics for all instances in your account. You can also pass a list of names or UUIDs in the `POST` body. ## Response formats Set the `Accept` header to control the response format: | `Accept` header | Format | |-----------------|--------| | `application/json` | JSON object with an `instances` array. | | Any other value (or omitted) | Prometheus text exposition format. | ## Response fields Each entry in the `instances` array contains the following fields: ### Identity | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Instance UUID. | | `name` | string | Instance name. | ### Lifecycle | Field | Type | Description | |-------|------|-------------| | `state` | string | Current instance state. | | `start_count` | int | Number of times the instance has started. | | `restart_count` | int | Number of automatic restarts. Omitted if `0`. | | `started_at` | timestamp | Time of last start. Omitted if unset. | | `stopped_at` | timestamp | Time of last stop. Omitted if unset. | | `uptime_ms` | int64 | Current uptime in milliseconds. | ### Boot timing | Field | Type | Description | |-------|------|-------------| | `boot_time_us` | int | Time from boot trigger to app entry point, in microseconds. Omitted if `0`. | | `net_time_us` | int | Time to set up the network interface, in microseconds. Omitted if `0`. | | `template_time_us` | int | Time to create the template/snapshot, in microseconds. Omitted if `0`. | ### Resource usage | Field | Type | Description | |-------|------|-------------| | `rss_bytes` | int64 | Resident set size: physical memory in use, in bytes. | | `cpu_time_ms` | int64 | Total CPU time consumed since last start, in milliseconds. | | `mem_mb_ms` | int64 | Memory-time consumed since last start, in mebibyte-milliseconds. | Memory-time is the memory of the instance integrated over the time the instance runs. It follows the current size of an instance with [vertical scaling](/features/vertical-scaling), and a stopped instance or one in standby consumes none. ### Network throughput | Field | Type | Description | |-------|------|-------------| | `rx_bytes` | int64 | Bytes received since last start. | | `rx_packets` | int64 | Packets received since last start. | | `tx_bytes` | int64 | Bytes transmitted since last start. | | `tx_packets` | int64 | Packets transmitted since last start. | ### Connection activity These fields reflect the view from the Unikraft Cloud load balancer, not the instance itself. | Field | Type | Description | |-------|------|-------------| | `nconns` | int | Number of active TCP connections. | | `nreqs` | int | Number of active HTTP requests (non-zero only for HTTP-mode services). | | `nqueued` | int | Number of connections or requests waiting for acceptance. | | `ntotal` | int64 | Total connections or requests processed since last start. | ### Wakeup latency histogram For instances with [scale-to-zero](/features/scale-to-zero) enabled, the `wakeup_latency` field contains a histogram of the time between an incoming connection and the instance resuming execution. | Field | Type | Description | |-------|------|-------------| | `wakeup_latency` | array | Histogram buckets. Each bucket has `bucket_ms` (upper bound in ms, or `null` for the overflow bucket) and `count` (number of wakeups in that bucket). | | `wakeup_latency_sum` | uint64 | Sum of all recorded wakeup latencies in milliseconds. | ## Example JSON output Prometheus output ```bash title="unikraft" unikraft api /v1/instances/metrics?uuid=f31c1fa8-6188-4ebe-b6e0-b2de063ad7ea -X GET --metro fra ``` ```bash title="API" curl -X GET \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Accept: application/json" \ "https://api.fra.unikraft.cloud/v1/instances/metrics?uuid=f31c1fa8-6188-4ebe-b6e0-b2de063ad7ea" ``` ```json title="JSON" { "status": "success", "data": { "instances": [ { "status": "success", "uuid": "f31c1fa8-6188-4ebe-b6e0-b2de063ad7ea", "name": "httpserver-go1.21-wf9iz", "state": "standby", "start_count": 2, "started_at": "2026-06-05T11:44:32Z", "stopped_at": "2026-06-05T11:44:36Z", "uptime_ms": 8161, "boot_time_us": 97308, "net_time_us": 102963, "rss_bytes": 0, "cpu_time_ms": 150, "mem_mb_ms": 2089216, "rx_bytes": 2012, "rx_packets": 23, "tx_bytes": 1196, "tx_packets": 11, "nconns": 0, "nreqs": 0, "nqueued": 0, "ntotal": 2, "wakeup_latency": [ { "bucket_ms": 1, "count": 0 }, { "bucket_ms": 2, "count": 0 }, { "bucket_ms": 4, "count": 0 }, { "bucket_ms": 8, "count": 0 }, { "bucket_ms": 16, "count": 0 }, { "bucket_ms": 32, "count": 0 }, { "bucket_ms": 64, "count": 0 }, { "bucket_ms": 128, "count": 1 }, { "bucket_ms": 256, "count": 0 }, { "bucket_ms": 512, "count": 0 }, { "bucket_ms": 1024, "count": 0 }, { "bucket_ms": 2048, "count": 0 }, { "bucket_ms": 4096, "count": 0 }, { "bucket_ms": null, "count": 0 } ], "wakeup_latency_sum": 104 } ] } } ``` ```bash title="unikraft" unikraft api /v1/instances/metrics?uuid=f31c1fa8-6188-4ebe-b6e0-b2de063ad7ea -X GET --metro fra \ -H "Accept: text/plain" ``` ```bash title="API" curl -X GET \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Accept: text/plain" \ "https://api.fra.unikraft.cloud/v1/instances/metrics?uuid=f31c1fa8-6188-4ebe-b6e0-b2de063ad7ea" ``` ```text title="Prometheus" # HELP instance_state 0=stopped,1=starting,2=running,3=draining,4=stopping,5=standby,6=template,7=deleted # TYPE instance_state gauge instance_state{instance_uuid="f31c1fa8-6188-4ebe-b6e0-b2de063ad7ea"} 5 # HELP instance_start_count Number of times the instance has been started # TYPE instance_start_count counter instance_start_count{instance_uuid="f31c1fa8-6188-4ebe-b6e0-b2de063ad7ea"} 2 # HELP instance_restart_count Number of times the instance has been restarted # TYPE instance_restart_count counter instance_restart_count{instance_uuid="f31c1fa8-6188-4ebe-b6e0-b2de063ad7ea"} 0 # HELP instance_started_at Time when the instance started # TYPE instance_started_at gauge instance_started_at{instance_uuid="f31c1fa8-6188-4ebe-b6e0-b2de063ad7ea"} 1780659872 # HELP instance_stopped_at Time when the instance stopped # TYPE instance_stopped_at gauge instance_stopped_at{instance_uuid="f31c1fa8-6188-4ebe-b6e0-b2de063ad7ea"} 1780659876 # HELP instance_uptime_s Uptime in seconds # TYPE instance_uptime_s gauge instance_uptime_s{instance_uuid="f31c1fa8-6188-4ebe-b6e0-b2de063ad7ea"} 8.161000 # HELP instance_boot_time_s Boot time in seconds # TYPE instance_boot_time_s gauge instance_boot_time_s{instance_uuid="f31c1fa8-6188-4ebe-b6e0-b2de063ad7ea"} 0.097308 # HELP instance_net_time_s Net time in seconds # TYPE instance_net_time_s gauge instance_net_time_s{instance_uuid="f31c1fa8-6188-4ebe-b6e0-b2de063ad7ea"} 0.102963 # HELP instance_template_time_s Template creation time in seconds # TYPE instance_template_time_s gauge instance_template_time_s{instance_uuid="f31c1fa8-6188-4ebe-b6e0-b2de063ad7ea"} 0.000000 # HELP instance_rss_bytes Resident set size in bytes # TYPE instance_rss_bytes gauge instance_rss_bytes{instance_uuid="f31c1fa8-6188-4ebe-b6e0-b2de063ad7ea"} 0 # HELP instance_cpu_time_s Consumed CPU time in seconds # TYPE instance_cpu_time_s counter instance_cpu_time_s{instance_uuid="f31c1fa8-6188-4ebe-b6e0-b2de063ad7ea"} 0.150000 # HELP instance_mem_mb_s Consumed memory-time in mebibyte-seconds # TYPE instance_mem_mb_s counter instance_mem_mb_s{instance_uuid="f31c1fa8-6188-4ebe-b6e0-b2de063ad7ea"} 2089.216000 # HELP instance_rx_bytes Amount of bytes received over network # TYPE instance_rx_bytes counter instance_rx_bytes{instance_uuid="f31c1fa8-6188-4ebe-b6e0-b2de063ad7ea"} 2012 # HELP instance_tx_bytes Amount of bytes transmitted over network # TYPE instance_tx_bytes counter instance_tx_bytes{instance_uuid="f31c1fa8-6188-4ebe-b6e0-b2de063ad7ea"} 1196 # HELP instance_rx_packets Count of packets received from network # TYPE instance_rx_packets counter instance_rx_packets{instance_uuid="f31c1fa8-6188-4ebe-b6e0-b2de063ad7ea"} 23 # HELP instance_tx_packets Count of packets transmitted over network # TYPE instance_tx_packets counter instance_tx_packets{instance_uuid="f31c1fa8-6188-4ebe-b6e0-b2de063ad7ea"} 11 # HELP instance_nconns Number of active connections # TYPE instance_nconns gauge instance_nconns{instance_uuid="f31c1fa8-6188-4ebe-b6e0-b2de063ad7ea"} 0 # HELP instance_nreqs Number of active requests # TYPE instance_nreqs gauge instance_nreqs{instance_uuid="f31c1fa8-6188-4ebe-b6e0-b2de063ad7ea"} 0 # HELP instance_nqueued Number of queued connections/requests # TYPE instance_nqueued gauge instance_nqueued{instance_uuid="f31c1fa8-6188-4ebe-b6e0-b2de063ad7ea"} 0 # HELP instance_ntotal Number of processed connections/requests # TYPE instance_ntotal counter instance_ntotal{instance_uuid="f31c1fa8-6188-4ebe-b6e0-b2de063ad7ea"} 2 # HELP instance_wakeup_latency_seconds Wakeup latencies in seconds # TYPE instance_wakeup_latency_seconds histogram instance_wakeup_latency_seconds_bucket{instance_uuid="f31c1fa8-6188-4ebe-b6e0-b2de063ad7ea",le="0.001000"} 0 instance_wakeup_latency_seconds_bucket{instance_uuid="f31c1fa8-6188-4ebe-b6e0-b2de063ad7ea",le="0.002000"} 0 instance_wakeup_latency_seconds_bucket{instance_uuid="f31c1fa8-6188-4ebe-b6e0-b2de063ad7ea",le="0.004000"} 0 instance_wakeup_latency_seconds_bucket{instance_uuid="f31c1fa8-6188-4ebe-b6e0-b2de063ad7ea",le="0.008000"} 0 instance_wakeup_latency_seconds_bucket{instance_uuid="f31c1fa8-6188-4ebe-b6e0-b2de063ad7ea",le="0.016000"} 0 instance_wakeup_latency_seconds_bucket{instance_uuid="f31c1fa8-6188-4ebe-b6e0-b2de063ad7ea",le="0.032000"} 0 instance_wakeup_latency_seconds_bucket{instance_uuid="f31c1fa8-6188-4ebe-b6e0-b2de063ad7ea",le="0.064000"} 0 instance_wakeup_latency_seconds_bucket{instance_uuid="f31c1fa8-6188-4ebe-b6e0-b2de063ad7ea",le="0.128000"} 1 instance_wakeup_latency_seconds_bucket{instance_uuid="f31c1fa8-6188-4ebe-b6e0-b2de063ad7ea",le="0.256000"} 1 instance_wakeup_latency_seconds_bucket{instance_uuid="f31c1fa8-6188-4ebe-b6e0-b2de063ad7ea",le="0.512000"} 1 instance_wakeup_latency_seconds_bucket{instance_uuid="f31c1fa8-6188-4ebe-b6e0-b2de063ad7ea",le="1.024000"} 1 instance_wakeup_latency_seconds_bucket{instance_uuid="f31c1fa8-6188-4ebe-b6e0-b2de063ad7ea",le="2.048000"} 1 instance_wakeup_latency_seconds_bucket{instance_uuid="f31c1fa8-6188-4ebe-b6e0-b2de063ad7ea",le="4.096000"} 1 instance_wakeup_latency_seconds_bucket{instance_uuid="f31c1fa8-6188-4ebe-b6e0-b2de063ad7ea",le="+Inf"} 1 instance_wakeup_latency_seconds_sum{instance_uuid="f31c1fa8-6188-4ebe-b6e0-b2de063ad7ea"} 0.104000 instance_wakeup_latency_seconds_count{instance_uuid="f31c1fa8-6188-4ebe-b6e0-b2de063ad7ea"} 1 ``` ## Learn more * [Audit events](/platform/audit-events): follow instance state changes live and see who or what caused them. * Unikraft Cloud's [REST API reference](/api/platform/v1). * [Instance states](/platform/instances#instance-states). * [Scale-to-zero](/features/scale-to-zero) feature. --- ## Document: Instances Learn how to deploy your apps on the Unikraft Cloud platform. URL: /docs/platform/instances # Instances This document describes the Unikraft Cloud Instances API (v1) for managing Unikraft instances. An *instance* is a MicroVM running a single instance of your app. {/* vale off */} :::tip[Drive instances from your own code] Everything on this page is in the [SDKs](/sdks/overview), so instances are something your program creates and drives directly: ```ts const inst = await ukc.metro("fra").instances .create({ image: "nginx:latest", memory_mb: 256, autostart: true }) .wait({ state: "running" }); ``` See the [JavaScript](/sdks/js), [Python](/sdks/python) and [Go](/sdks/go) SDKs. ::: {/* vale on */} ## Instance states An instance can be in one of the following states: | State | Description | |------------|-------------| | `stopped` | The instance isn't running and doesn't count against live resource quotas. Connections can't establish. | | `starting` | The instance is booting up. This typically takes just a few milliseconds. | | `running` | Your app reached its main entry point. | | `draining` | The instance is draining connections before shutting down. No new connections can establish. | | `stopping` | The instance is shutting down. | | `standby` | The instance has scaled-to-zero. The instance isn't running, but will be automatically started when there are incoming requests. | Unikraft Cloud reports these as instance `state` values via the endpoints. You can visualize these states with the CLI. Here is an example of an instance that's in `stopped` state: ```bash unikraft instance get httpserver-go121-taud8 ``` ```ansi metro: fra name: httpserver-go121-taud8 uuid: bd281cc4-7640-4cbc-af48-5a44c2bd7b74 state: standby image: /httpserver-go121 resources:  memory: 256MiB  vcpus: 1 service:  name: floral-sun-54ixkmi6  uuid: d20c8a02-53a8-4917-8c25-94b99ab59a88  domains:  - fqdn: floral-sun-54ixkmi6.fra.unikraft.app networks: - uuid: 16ca6566-a368-45d3-b759-3abf0cc37d03  private-ip: 10.0.0.37  mac: 12:b0:0a:00:00:25 timestamps:  created: 6 days ago scale-to-zero:  enabled: true  policy: on  cooldown-time: 1s stop:  reason: platform stop  exit-code: 1 ``` You can retrieve even more instance details via the API: ```bash title="GET /instances/bd281cc4-7640-4cbc-af48-5a44c2bd7b74" curl -X GET \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Accept: application/json" \ https://api.fra.unikraft.cloud/v1/instances/bd281cc4-7640-4cbc-af48-5a44c2bd7b74 ``` ```json title="" { "status": "success", "data": { "instances": [ { "status": "success", "uuid": "bd281cc4-7640-4cbc-af48-5a44c2bd7b74", "name": "httpserver-go121-taud8", "created_at": "2026-05-28T13:53:40Z", "state": "standby", "private_fqdn": "httpserver-go121-taud8.internal", "image": "oci://unikraft.io//httpserver-go121:latest@sha256:7785ebdf6c1ffaf05c3beab4cffc2548790c088233a0ee51ac5d047e21e43d77", "memory_mb": 256, "vcpus": 1, "stop_reason": 39, "exit_code": 1, "stop_code": 33024, "restart_policy": "never", "scale_to_zero": { "enabled": true, "policy": "on", "cooldown_time_ms": 1000, "notify_time_ms": 0, "stateful": false }, "boot_time_us": 75698, "net_time_us": 79067, "start_count": 3, "started_at": "2026-05-28T13:54:43Z", "stopped_at": "2026-05-28T13:54:45Z", "uptime_ms": 47521, "service_group": { "uuid": "d20c8a02-53a8-4917-8c25-94b99ab59a88", "name": "floral-sun-54ixkmi6", "domains": [ { "fqdn": "floral-sun-54ixkmi6.fra.unikraft.app" } ] }, "network_interfaces": [ { "uuid": "16ca6566-a368-45d3-b759-3abf0cc37d03", "private_ip": "10.0.0.37", "mac": "12:b0:0a:00:00:25" } ], "private_ip": "10.0.0.37", "volumes": [] } ] } } ``` ### Stop reason To understand why Unikraft Cloud stopped an instance or is shutting it down, it provides information about the stop reason. You can retrieve this information via the [`GET /instances`](/api/platform/v1/instances#list-instances) endpoint when an instance is in the `draining`, `stopping`, `stopped` or `standby` state. The `stop_reason` contains a bitmask that tells you the origin of the shutdown: | Bit | 5 [Z] | 4 [F] | 3 [U] | 2 [P] | 1 [A] | 0 [K] | |------|---------------------------|---------------------------|----------------------------|--------------------------------|------------------------------------|---------------------------------------| | Desc.| Stop due to scale-to-zero | This was a force stop[^1] | Stop initiated by user[^2] | Stop initiated by platform[^3] | App exited - `exit_code` available | Kernel exited - `stop_code` available | For example, the `stop_reason` has the following values in these scenarios: | Value | Bitmask | Scenario | |-------|-------------------|----------| | `39` | `100111`/`Z--PAK` | Scale-to-zero shutdown. The platform initiated the stop because the instance scaled to zero, and both the app and kernel exited. | | `31` | `011111`/`-FUPAK` | Forced user-initiated shutdown where the app and kernel still reported clean exits before the stop completed. | | `28` | `011100`/`-FUP--` | Forced user-initiated shutdown without app or kernel exit information. | | `15` | `001111`/`--UPAK` | Regular user-initiated shutdown. The app and kernel have exited. The `exit_code` and `stop_code` show if the app and kernel shut down cleanly. | | `13` | `001101`/`--UP-K` | The user initiated a shutdown but the app was forcefully killed by the kernel during shutdown. This can happen if the image doesn't support a clean app exit or the app crashed after receiving a termination signal. In this case, ignore `exit_code`. | | `7` | `000111`/`---PAK` | Platform-initiated shutdown. The app and kernel have exited. The `exit_code` and `stop_code` show if they shut down cleanly. | | `3` | `000011`/`----AK` | The app exited. The `exit_code` and `stop_code` show if the app and kernel shut down cleanly. | | `1` | `000001`/`-----K` | The instance likely experienced a fatal crash and the `stop_code` contains more information about the cause of the crash. | | `0` | `000000`/`------` | The stop reason is unknown. | :::note There can be a short delay of a few milliseconds between the instance reaching the `stopped` state and Unikraft Cloud updating the `stop_reason` (or vice versa). ::: :::note The scale-to-zero shutdown is a special case where the platform initiates the stop because the instance scaled to zero, and both the app and kernel exited. In the API, the instance appears in the `standby` state, instead of `stopped`. ::: #### Exitcode The app exit code is what the app returns upon leaving its main entry point. The encoding of the `exit_code` is app specific. See the documentation of the app for more details. An `exit_code` of `0` indicates success or no failure. #### Stopcode {/* vale off */} If bit 0 [K] is set in `stop_reason`, `stop_code` contains the 32-bit code reported by the guest kernel. Unikraft Cloud also uses platform-defined values when the stop originates in the platform before the guest kernel can report its own code. {/* vale on */} Currently the platform-defined values are: | Value | Symbol | Scenario | |-------|--------------|----------| | `0` | `UNKNOWN` | No platform-specific stop code is available. | | `1` | `NOMEDIUM` | The instance couldn't start because the image or another required medium wasn't available, for example because an image pull failed. | When the guest kernel reports a stop code, it uses the following encoding: | Bits | 31 - 24 (8 bits) | 23 - 16 (8 bits) | 15 [T] | 14 - 8 (7 bits) | 7 - 0 (8 bits) | |------|------------------|------------------|----------------|-----------------|----------------| | Desc.| Reserved[^4] | `errno` | `shutdown_bit` | `initlvl` | `reason` | #### Reason The `reason` can be any of the following values: | Value | Symbol | Scenario | |-------|------------|----------| | `0` | `OK` | Successful shutdown. | | `1` | `EXP` | The system detected an invalid state and actively stopped execution to prevent data corruption. | | `2` | `MATH` | An arithmetic CPU error (for example, division by zero). | | `3` | `INVLOP` | Invalid CPU instruction or instruction error (for example, wrong operand alignment). | | `4` | `PGFAULT` | Page fault - see `errno` for further details. | | `5` | `SEGFAULT` | Segmentation fault. | | `6` | `HWERR` | Hardware error. | | `7` | `SECERR` | Security violation (for example, violation of memory access protections). | A `reason` of `0` indicates a clean shutdown. Ignore the other bits of `stop_code` when checking for a crash. #### Init level `initlvl` indicates the initialization or shutdown phase at stop time. A level of `127` means the instance was executing the app. #### Shutdown bit `shutdown_bit` indicates the system was shutting down. #### Error number `errno` is a [Linux error code number](https://www.man7.org/linux/man-pages/man3/errno.3.html) that provides more detail about the root cause. :::note For example, an out-of-memory (OOM) situation triggers a page fault `PGFAULT(4)` with `errno` set to `ENOMEM(12)`. In that case the `stop_code` is `0x000C7F04=818948` and the `stop_reason` is `-----K (1)` if the stop occurred during app execution. ::: By default, a guest that runs out of memory stops the whole instance instead of killing a single process. The workload then doesn't continue in a partial state. A process can opt in to the out-of-memory killer by setting its `/proc/self/oom_score_adj` to `0`. The kernel may then kill that process alone and keep the instance running, so the rest of the workload has to cope with losing it. ### Restart policy When an instance stops because the app exits or crashes, Unikraft Cloud can restart it automatically according to the restart policy. The policy can have the following values: | Policy | Description | |--------------|-------------| | `never` | Never restart the instance (default). | | `always` | Always restart the instance when Unikraft Cloud initiates the stop from within the instance (that is, the app exits or the instance crashes). | | `on-failure` | Only restart the instance if it crashes. | When an instance stops, Unikraft Cloud evaluates the stop reason and the restart policy to decide whether to restart. It uses an exponential back-off delay (immediate, 5s, 10s, 20s, 40s, 5m) to slow down restarts in tight crash loops. If an instance runs without problems for 10s, Unikraft Cloud resets the back-off delay and the restart sequence ends. The `restart.attempt` value in [`GET /instances`](/api/platform/v1/instances#list-instances) counts restarts in the current sequence. The number of completed restarts is `restart_count`. The `restart.next_at` field indicates when the next restart occurs if a back-off delay is in effect. A manual start or stop of the instance aborts the restart sequence and resets the back-off delay. {/* vale off */} [^1]: A forced stop doesn't give the instance a chance to perform a clean shutdown. Often, `exit_code` and `stop_code` are therefore not available. If the guest reports an exit while the forced shutdown is in progress, bits 0 [K] and 1 [A] can still be set. [^2]: A stop command originating from the user travels through the platform controller. This is why bit 2 [P] will also always occur for user-initiated stops. [^3]: Internally this bit is set when ukpd, the platform controller, initiates the stop. [^4]: The system sets reserved bits to 0; ignore them. {/* vale on */} ## Creating instances ### Instance types Every instance runs as one of two virtual machine types, which you pick with `--type`: | Type | Description | |------|-------------| | `micro` | The default, running on Firecracker. | | `full` | Runs on QEMU, which [GPU support](#gpus) requires, and needs a plan with full VM support. | Full VMs running on QEMU are required for [GPU support](#gpus). They don't support [branching](/features/branching), [checkpointing](/features/checkpoints), or [vertical scaling](/features/vertical-scaling) yet, and work only with block-based volumes rather than [virtiofs](/platform/volumes#filesystems). A create request therefore can't combine `--type=full` with `--branch` or `--checkpoint`. ### Replicas The [`POST /instances`](/api/platform/v1/instances#create-instance) request accepts a `replicas` field (default 0) that creates more copies of the instance alongside the base one. | Value | Instances created | |-------|-------------------| | `0` | 1 (the base instance) | | `1` | 2 (base + 1 replica) | | `N` | N + 1 | All instances share the same image, memory, arguments, and service group configuration. Replicas receive independent names and UUIDs. You can do this with the CLI as well: ```bash unikraft instance create --replicas 2 ... ``` ### Vertical scaling An instance can grow and shrink its memory and vCPUs while it runs. Opt in by setting a range in the create request: `min_memory_mb` and `max_memory_mb` for memory, `min_vcpus` and `max_vcpus` for vCPUs. The instance boots with the minimum, and the platform adjusts the size within the range based on the instance's memory utilization and CPU pressure. See [Vertical scaling](/features/vertical-scaling) for the details, requirements, and limitations. ### GPUs :::caution[**Limited Access**] GPU support is currently only available as a preview for enterprise customers. To try out the preview, reach out to the [Unikraft Cloud Discord](https://unikraft.com/discord) or send an email to [support@unikraft.com](mailto:support@unikraft.com). The interface described here reflects the current implementation and may change before general availability. ::: Instances can run with an NVIDIA GPU attached. Depending on the host, that GPU is either a whole physical GPU or a vGPU. A vGPU is a virtual GPU that the host carves out of a physical GPU, so more than one instance shares the physical GPU. A vGPU reports the same model as the physical GPU it comes from, and your workload uses it like any other GPU. Pass `--gpus` when you create the instance, together with the `full` instance type that GPUs require: ```bash unikraft instance create --metro fra \ --name my-inference-app \ --image my-inference-app:latest \ --memory 4096 \ --type full \ --gpus 1 ``` An instance can currently hold at most one GPU, and you can't choose which one. Build the workload image on a glibc-based distribution rather than Alpine, because the NVIDIA user-space libraries don't run on musl. #### GPU allocation The platform allocates a GPU when the instance starts, not when you create it, and this includes instances created with autostart. On the first start, it picks any free GPU on the host and records that GPU's UUID and model on the instance. If no GPU is free, the start fails with an error stating that no GPU matching the instance is currently available. The instance holds the GPU only while it runs. Stopping the instance, or scaling it to zero, returns the GPU to the host's pool so another instance can use it in the meantime. On every later start, the platform first tries to reacquire the same GPU, and falls back to any free GPU of the same model if another instance holds that one. It never assigns a GPU of a different model, because a resumed guest expects the same hardware as before the suspend. To reserve GPU capacity for a set of instances, limit how many of them run at the same time. To see the GPU an instance holds, inspect it: ```bash unikraft instance get my-inference-app ``` The `gpus.*.allocated` field reports whether the instance holds a GPU right now. The `gpus.*.uuid` and `gpus.*.model` fields identify that GPU. The model is the GPU's vendor and device ID in hexadecimal form, such as `10de:1eb8` for an NVIDIA Tesla T4. A stopped instance holds no GPU, so it reports `allocated` as `false` and no UUID, but keeps the model it last ran on. `resources.gpus` reports how many GPUs the instance requests. Listings sort and filter on these fields like any other [field](/docs/cli/fields). #### Scale-to-zero and templates for GPU instances GPU instances backed by a vGPU support [scale-to-zero](/features/scale-to-zero), suspend and resume, and [instance templates](#instance-templates). Suspending a GPU instance requires a GPU that can save and restore its device state, which vGPUs support. When a vGPU instance scales to zero, the platform snapshots the VM together with the state of its GPU, detaches the GPU, and returns it to the pool. On the next request, the platform reacquires a GPU as described in [GPU allocation](#gpu-allocation) and resumes the VM from the snapshot with the GPU attached. This lets a host serve more GPU instances than it has GPUs. An idle instance releases its GPU, and a woken instance resumes on any free GPU of the same model. A GPU accesses guest memory directly and can't wait for a page the platform hasn't loaded yet. Resuming a GPU instance loads its whole memory from the snapshot before the VM continues, so resume time grows with the instance's memory size. CPU-only instances load memory on demand instead. Instances created from a template of a GPU instance inherit its GPU configuration and model. A create request that uses such a template can't set `--gpus`. The value comes from the template. ### Wait for running By default [`POST /instances`](/api/platform/v1/instances#create-instance) returns as soon as the platform queues the instance. Set `timeout_s` non-zero to block until the instance reaches the `running` state or the timeout expires. For example: ```json title="POST /instances" { ... "image": "nginx:latest", "memory_mb": 256, "autostart": true, "timeout_s": 10, ... } ``` :::note `wait_timeout_ms` is a deprecated compatibility field. When set, the platform rounds the value up to the next full second. Use `timeout_s` instead. ::: :::note The `unikraft` CLI automatically waits for the instance to be running after creation, up until a preconfigured timeout of 10 seconds. ::: To wait for an existing instance, pass query parameters to the `wait` endpoint: ```bash unikraft api '/v1/instances//wait?state=running&timeout_s=30' ``` ### Delete on stop Pass `delete-on-stop` in the features array to automatically delete the instance when it stops: ```json title="POST /instances" { ... "image": "nginx:latest", "memory_mb": 256, "features": ["delete-on-stop"], "restart_policy": "never", ... } ``` This is useful for ephemeral workloads—batch jobs, one-shot tasks—where you don't need to keep the stopped instance. You can also use the CLI to set this up: ```bash unikraft instance create --rm ... ``` :::note To use the "delete-on-stop" feature, set the `restart_policy` to `never`. ::: ### Nested virtualization {/* vale off */} :::caution[**Limited Access**] Nested virtualization is available as part of enterprise plans. To enable it for your account, reach out to the [Unikraft Cloud Discord](https://unikraft.com/discord) or send an email to [support@unikraft.com](mailto:support@unikraft.com). ::: {/* vale on */} An instance runs without the host's virtualization extensions by default, since the platform hides them from the guest. Pass `nested-virt` in the features array to expose them, so that a workload shipping its own virtual machines runs as an ordinary instance: ```json title="POST /instances" { ... "image": "my-workload:latest", "memory_mb": 4096, "features": ["nested-virt"], ... } ``` You can also use the CLI to set this up: ```bash unikraft instance create --feature nested-virt ... ``` The guest kernel needs the matching driver to use the extensions, and exposes `/dev/kvm` once the driver is active. On `x86_64`, a Linux guest needs `CONFIG_KVM` together with `CONFIG_KVM_INTEL` or `CONFIG_KVM_AMD`. On `arm64`, a Linux guest needs `CONFIG_KVM`, which the kernel builds in rather than loading as a module. The host must support nested virtualization itself. An instance with `nested-virt` doesn't start on a host without this support. :::note A create request that starts from a snapshot—an [instance template](#instance-templates), a [branch](/features/branching), or a [checkpoint](/features/checkpoints)—can't add `nested-virt`. Set the feature on the instance that you snapshot instead, and every instance that starts from that snapshot inherits it. ::: ## Stopping instances ### Drain timeout By default [`PUT /instances/stop`](/api/platform/v1/instances#stop-instances) stops the instance immediately. Set `drain_timeout_ms` to allow the instance to finish serving in-flight connections before it stops. | Value | Behaviour | |-------|-----------| | `0` | Stop immediately (default). | | `-1` | Use the platform's maximum drain timeout. | | `N` | Drain for up to N milliseconds, then stop. | ```json title="PUT /instances/stop" { ... "name": "my-instance", "drain_timeout_ms": 5000, ... } ``` You can also use the CLI to set this up: ```bash unikraft instance stop --drain-timeout 5s my-instance ``` While draining, the instance enters the `draining` state. The platform accepts no new connections. The instance stops once all connections close or the timeout elapses. :::note You can't combine `drain_timeout_ms` with `force: true`. Setting both returns a `400` error. ::: ## Instance templates An instance template is a snapshotted instance that acts as a source for cloning new instances. Cloning a template creates a new instance that resumes from the exact original system state. It preserves memory contents, open files, and populated caches to bypass the standard boot sequence. Once you convert an instance into a template, you can't reverse the process. To transition an actively running instance into a template, from the guest write the value 1 to `/uk/libukp/template_instance`: ```bash title="Convert instance to template" echo 1 > /uk/libukp/template_instance ``` This action instructs the controller to freeze running processes and save the instance as a reusable template. The write blocks until the platform has snapshotted the instance and created the new instance from the resulting template. When the write returns, the guest is already running as the freshly cloned instance, restored from the snapshot rather than continuing as the original. {/* vale Microsoft.Dashes = NO */} This lets the guest finish any instance-specific setup—such as reading per-instance data from an attached [inline ROM](/features/roms#inline-roms)—immediately after the write completes. {/* vale Microsoft.Dashes = YES */} If the template already exists, the clone happens right away and the write returns without re-snapshotting. You can also create a template from an instance (even when it's running) by calling [`POST /instances/templates`](/api/platform/v1/instances#create-template-instances-from-instances), or using the CLI: ```bash unikraft instance template create httpserver-go121-taud8 ``` ```ansi metro: fra name: httpserver-go121-taud8 uuid: bd281cc4-7640-4cbc-af48-5a44c2bd7b74 state: template image: /httpserver-go121 resources:  memory: 256MiB  vcpus: 1 timestamps:  created: 6 days ago scale-to-zero:  enabled: true  policy: on  cooldown-time: 1s ``` You can list templates with [`GET /instances/templates`](/api/platform/v1/instances#list-template-instances) or the CLI: ```bash unikraft instance template list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS CREATED fra httpserver-go121-taud8 template /httpserver-go121 256MiB 1 6 days ago ``` Now, you can create an instance from the template using the `unikraft` CLI: ```bash unikraft instance create --metro fra \ --template httpserver-go121-taud8 \ --name httpserver-go121-clone ``` ```ansi metro: fra name: httpserver-go121-clone uuid: 91796fdb-bd41-4c7b-856f-9e60356c96bd state: standby image: acioc/httpserver-go121 resources:  memory: 256MiB  vcpus: 1 networks: - uuid: 8c778b8b-6ede-4e87-a825-159a2269fd19  private-ip: 10.0.0.85  mac: 12:b0:0a:00:00:55 timestamps:  created: just now scale-to-zero:  enabled: true  policy: on  cooldown-time: 1s ``` Templates support [delete locks](/platform/delete-locks), [tags](/platform/tagging), and [autokill](/features/autokill). The platform measures autokill on a template from the time of last clone, not from instance stop time. :::info You can also have the platform create a template implicitly as part of an instance-create call, instead of converting and managing it yourself. See [On-demand templates](/features/on-demand-templates). ::: ### Updating a template You can move a template to new images without recreating it. A `PATCH /instances/templates` request changes the image of a template, and the images of its [ROMs](/features/roms) and [plugins](/features/plugins): ```bash unikraft api /v1/instances/templates -X PATCH --metro fra \ '[ { "name": "web-base", "prop": "image", "op": "set", "value": "/web:v2" } ]' ``` These properties only take the `set` operation. The patches for one template in a request make up a single update, and if one of them fails, none of them applies. The platform boots a new revision of the template with the new images, and turns it into a template the same way as the first revision. Once the new revision is ready, it takes over the UUID and the name of the template. While the update runs, the status of the template lists it in `updates` as `pending`. If the new revision fails, the template doesn't change, and `updates` reports the update as `failed`, with the reason in `error`. The instances of the template then follow the update: - An instance created from the template uses the new revision at its next start, unless it has a snapshot of its own. This includes instances that [scale to zero](/features/scale-to-zero) in stateless mode. - An instance with a snapshot of its own, for example a suspended one, keeps its revision as long as it has that snapshot. - Templates created from the updated template follow it, and [autoscale](/features/autoscale) uses the new revision for new instances. - An instance with an image of its own, or with ROMs or plugins that replace those of the template, keeps its revision. That holds whether you set them at creation or later. The status of an instance created from a template names its source in `clone_source`. Note the following: - You can't update a template with volumes, or a template without exactly one network interface. - A template takes one update at a time. A request for a template whose update still runs fails. - An update of the ROMs or plugins must list every ROM or plugin of the template, except those inherited from a parent template, with the same names, paths, and authorization. Only their images and the plugin configuration change. - A template created from another template changes its image only together with its parent. ### Nested templates You can clone a template from another template, creating a hierarchy. Each clone inherits the parent's full state at clone time and can independently accumulate further state before you convert it into its own template. This enables layered configurations, for example a base template with a warm database connection pool, and child templates specialized for different query workloads. No depth limit applies to nesting, and circular references aren't possible because the template state is immutable once set. :::info You can also build such a hierarchy in a single API call with [on-demand templates](/features/on-demand-templates#nested-templates), where the platform creates each level only if it doesn't already exist. ::: For more information, check out the API reference for instance templates [here](/api/platform/v1/instances#create-template-instances-from-instances). ## Instance metrics Use the [`GET /instances/metrics`](/api/platform/v1/instances#get-instances-metrics) endpoint to retrieve runtime statistics for one or more instances. With no identifiers, the platform returns metrics for all instances in your account. For more information, check out this dedicated section on [instance metrics](/platform/metrics). ## Instance logs Use the [`GET /instances/logs`](/api/platform/v1/instances#get-instances-logs) endpoint to retrieve logs for one or more instances. The logs capture the instance's `stdout` and `stderr` output, and they're preserved across restarts and stops. Read endpoints accept query parameters as an alternative to a JSON body. For example, to read the last 4 KiB of an instance's log: ```bash unikraft api '/v1/instances//log?offset=-4096&limit=4096' ``` A request that sends a body can't also give query parameters, including `uuid` or `name`. With the CLI, you can also follow the logs in real time: ```bash unikraft instance logs --follow my-instance ``` ## Learn more * The [CLI reference](/docs/cli/unikraft). * Unikraft Cloud's [REST API reference](/api/platform/v1), in particular the section on [instances](/api/platform/v1/instances). --- ## Document: Images URL: /docs/platform/images # Images > **Availability** > > Available on Unikraft Cloud (with differences, see the notes), dedicated hosts, BYOC and on-prem. > > Exposed by the API, the CLI and the SDKs. > > - Unikraft Cloud: The local registry is only available on dedicated, BYOC, and on-prem installations. [Registries](https://unikraft.com/docs/cli/registries) > - Unikraft Cloud: Kernel-less images and arm64 hosts are part of enterprise plans. Contact support to use them. [support@unikraft.com](mailto:support@unikraft.com) > > See [Hosting options](https://unikraft.com/docs/platform/hosting-options). Unikraft Cloud uses a **registry** to store images used to instantiate apps. At a high level, you use the CLI to build and push an image to the registry, and then ask the controller to start an app from it. This process utilizes two types of registries: central and local. Built images default to the central registry (`index.unikraft.io`). The platform pulls from this registry automatically upon starting an instance for the first time. You can choose to push an image directly to a node's local registry instead. This bypasses the central registry entirely. Skipping this extra network round trip speeds up deployments. It also reduces bandwidth consumption. Read more about this in the [registries documentation](/cli/registries). {/* vale off */} ## Dockerfile, Kraftfile and runtime {/* vale on */} On Unikraft Cloud, a `Dockerfile` guides the process of building images, and a `Kraftfile` guides the process of deploying the resulting image. Use the Python running example: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/httpserver-python3.12 ``` The directory contains the following `Kraftfile`: ```yaml title="Kraftfile" spec: v0.7 runtime: base-compat:latest targets: - kraftcloud/x86_64 rootfs: source: ./Dockerfile format: erofs cmd: ["/usr/bin/python3", "/src/server.py"] ``` The file is simple. It defines the start command, instructs the CLI to build the root filesystem, specifies the `base-compat:latest` runtime, and targets `kraftcloud` on `x86_64`. On Unikraft Cloud, a runtime provides a base image containing the minimal code your app needs to execute. The CLI combines your app files with this base image during the build step. It uses the `Dockerfile` as the source to generate the directory structure. When packaging these files into a root filesystem, Unikraft offers two main options: [CPIO and EROFS](/tutorials/rootfs-formats). The file specifies the EROFS format to create a read-only disk image, which is the [preferred format for Unikraft Cloud](/tutorials/rootfs-formats#conclusion). For a more in depth explanation of the `Kraftfile` you can check the [reference](/kraftfile/v0.7). The `Dockerfile` itself for the app looks as follows: ```dockerfile title="Dockerfile" FROM python:3.12 AS build RUN set -xe; \ /usr/sbin/ldconfig /usr/local/lib # copy the python image COPY ./server.py /src/server.py ``` If you're familiar with `Dockerfile`s there is nothing unusual here, other than that Unikraft Cloud prefers usage of `FROM scratch` to keep images lean. This multi-stage build creates a minimal image. First, the process loads a standard Python 3.12 image to configure system dependencies. Next, it starts an empty filesystem called `scratch`. The builder extracts the compiled Python binary and essential shared system libraries from the first stage. It moves these exact files into the empty environment. Finally, the builder copies your app script into the container. To add your app's code to the build, change the `COPY` commands as needed. All [guides](/guides/overview) on Unikraft Cloud, and the [examples](https://github.com/unikraft-cloud/examples) they rely on underneath come with `Kraftfile`s and `Dockerfile`s for you to get started. :::tip You can also try a standard base image (for example, `FROM python:alpine`). This is a good start for [porting existing Dockerfiles to Unikraft Cloud](/tutorials/docker-to-ukc). Be aware that this choice may increase image size, memory use, and boot time. ::: ### Root filesystem from an existing image If the image you want to run already exists, take its root filesystem straight from the registry instead of rebuilding it from a `Dockerfile`. Set the `rootfs` source type to `oci`: ```yaml title="Kraftfile" spec: v0.7 runtime: base-compat:latest targets: - kraftcloud/x86_64 rootfs: source: type: oci path: docker.io/library/nginx:latest format: erofs ``` The build flattens the layers of a container image into the requested format. For a Unikraft image, it reuses the existing root filesystem unchanged. The image's own command, environment, and labels carry over, and anything you set in the `Kraftfile` applies on top. Note the following: - Set `type: oci` explicitly. The CLI never guesses it from the path. - The path has to be a registry reference, and the image has to provide the platform you build for. - A container image still needs a BuildKit builder to flatten its layers. See the [Kraftfile reference](/kraftfile/v0.7) for the other source types. ## Platform kernel {/* vale off */} {/* vale on */} An image doesn't have to carry its own kernel. An image that ships only a rootfs boots on the kernel installed on the node, which your metro operator maintains as a separate package. Images that do ship a kernel keep working exactly as before. This decouples the guest kernel from your app. A kernel update, including a security fix, becomes available to workloads that cold-boot after the operator rolls out the new kernel package. You no longer rebuild and re-push every image to pick one up. Images also get smaller, which shortens image pull times. A new kernel on the node applies to the next instance that starts. Running instances keep the kernel they booted with until they fully shut down, and an instance resuming from a [snapshot](/features/snapshots) always boots the kernel that snapshot captured. ### Build from a `Dockerfile` A kernel-less image doesn't need a `Kraftfile`. Pass a `Dockerfile` to `unikraft build`, and it builds an image with only the rootfs that the `Dockerfile` describes: ```bash unikraft build ./Dockerfile --arch x86_64 --output /my-app:latest ``` The CLI identifies a `Dockerfile` by its filename, so names such as `Dockerfile.prod` also work. A directory without a `Kraftfile` builds from its `Dockerfile`. If both files are present, the `Kraftfile` takes precedence. A `Dockerfile` has no targets to take an architecture from, so you must pass `--arch`. The image runs only in metros that have a platform kernel installed. ## Architectures The Unikraft Cloud platform runs on both `x86_64` and `arm64` hosts, and the hosted platform runs on `x86_64`. The architecture of the guest images must match the host architecture. {/* vale off */} {/* vale on */} List the architectures you want under `targets` in the `Kraftfile`: ```yaml title="Kraftfile" spec: v0.7 runtime: base-compat:latest targets: - kraftcloud/x86_64 - kraftcloud/arm64 rootfs: source: ./Dockerfile format: erofs cmd: ["/usr/bin/python3", "/src/server.py"] ``` A single `unikraft build` invocation then produces an image for every architecture in the list. The `--arch` flag names architectures on the command line instead, which is handy for narrowing a multi-architecture `Kraftfile` down to one architecture. How the build resolves architectures depends on what the `Kraftfile` declares: | `Kraftfile` | Architectures used | |-------------|--------------------| | `runtime` with no `targets` or `--arch` | `x86_64` only, even when the runtime provides more. | | `runtime` with `targets` or `--arch` | Only the architectures listed in both `targets` and `--arch`, which must be ones the runtime provides. | | [`roms`](/features/roms) with no `runtime` | Only the architectures listed in both `targets` and `--arch`, and the build fails when you supply neither. | :::caution Multi-platform builds need a correctly configured BuildKit builder on the host. Learn more [here](https://docs.docker.com/build/building/multi-platform/) ::: ### Local outputs `--output` names an image reference unless it looks like a path. A local path starts with `./`, `../`, or `/`, and the build saves the image there as an OCI archive or an OCI layout instead of pushing it: ```bash unikraft build . --arch x86_64 -o ./app.tar ``` ```text title="" │ saved image output=oci-archive://./app.tar ``` The build prints where the image went. Add the `oci-archive://` or `oci-layout://` prefix to make the choice explicit. The same rule applies to `image get`, `image delete`, and `image copy`. :::caution An output such as `app.tar` or `dist/` is an image reference, so the build pushes it to the registry instead of writing it locally. Add `./` to keep writing to a local path. ::: ## Example workflows This guide uses a Python [app](/guides/httpserver-python3.12) as an example to show three workflows: 1. How to create an image and launch an instance from it. 2. How to create an image and launch many instances from it. 3. How to launch instances from an existing image. ### Create an image and an instance from it Start with the simplest workflow: create an image from a Python app and start an instance from it: ```bash unikraft build . --output /httpserver-python312:latest unikraft run --metro fra \ -p 443:8080/http+tls \ -m 512M \ --image /httpserver-python312:latest ``` The output should look like: ```ansi metro: fra name: httpserver-python312-ma2i9 uuid: e7389eee-9808-4152-b2ec-1f3c0541fd05 state: running image: /httpserver-python312 resources:  memory: 512MiB  vcpus: 1 service:  uuid: 51a41f63-7e88-c443-b9bf-83cd7c04d975  name: young-night-5fpf0jj8  domains:  - fqdn: young-night-5fpf0jj8.fra.unikraft.app networks: - uuid: 53da3490-c6f5-3718-25f1-219a65163c73  private-ip: 10.0.3.3  mac: 12:b0:18:0c:cb:aa timestamps:  created: just now ``` This command builds an image named `httpserver-python312@sha256:278cb8b1...` using the `Kraftfile` and `Dockerfile`. It then packages it, pushes it to the registry, and starts an instance named `httpserver-python312-ma2i9` from it. The platform fetches the image from the registry to start the instance. You can see your images by running the following command: ```bash unikraft images list ``` You should see output like: ```ansi REF DIGEST /httpserver-python312 sha256:278cb8b14f9faf9c2702dddd8bfb6124912d82c11b4a2c6590b6e32fc4049472 ``` And you can remove an image from the central registry with the following command: ```bash unikraft images rm /httpserver-python312:latest ``` Use the full `/` reference: a bare name such as `httpserver-python312` resolves to the `official/` namespace, not to your images. :::note There may be a delay of a few minutes between removing an image from the registry and the image list reflecting the change. ::: ### Create an image and many instances from it For the next workflow, start many instances: ```bash unikraft build . --output /httpserver-python312:latest unikraft run --metro fra \ -p 443:8080/http+tls \ -m 512M \ --replicas 2 \ --image /httpserver-python312:latest ``` ```ansi metro: fra name: httpserver-python312-lscmn uuid: 1121225d-a678-45a2-a8ce-335a9a6a2683 state: running image: /httpserver-python312 resources:  memory: 512MiB  vcpus: 1 service:  name: restless-orangutan-1lklj6z5  uuid: 16f4b86d-df8f-452c-b189-41ec34250e58  domains:  - fqdn: restless-orangutan-1lklj6z5.fra.unikraft.app networks: - uuid: da9445a3-c7b7-4327-8499-3bd7001cd829  private-ip: 10.0.6.37  mac: 12:b0:0a:00:06:25 timestamps:  created: just now metro: fra name: httpserver-python312-xfgvc uuid: d021379d-9e03-4d0b-92a6-6fdf038e076a state: running image: /httpserver-python312 resources:  memory: 512MiB  vcpus: 1 service:  name: icy-violet-hce1qwm5  uuid: 4cd63083-d538-44df-92b9-74bd6eb54ec0  domains:  - fqdn: icy-violet-hce1qwm5.fra.unikraft.app networks: - uuid: 51c147cc-8f8d-4347-b628-d2e0f4f9b3be  private-ip: 10.0.0.121  mac: 12:b0:0a:00:00:79 timestamps:  created: just now metro: fra name: httpserver-python312-7lha2 uuid: 0706c648-0c56-4c74-8dda-5a1aeff7d8b8 state: running image: /httpserver-python312 resources:  memory: 512MiB  vcpus: 1 service:  name: hidden-silence-l3g520dw  uuid: 38703973-ccbd-47ae-b4c0-f9f59c1751e7  domains:  - fqdn: hidden-silence-l3g520dw.fra.unikraft.app networks: - uuid: 6c20f9b2-dc39-41d1-a4bd-250a52a75b19  private-ip: 10.0.9.185  mac: 12:b0:0a:00:09:b9 timestamps:  created: just now ``` Check that it worked by listing all instances with: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra httpserver-python312-7lha2 running /httpserver-python312 512MiB 1 hidden-silence-l3g520dw.fra.unikraft.app just now fra httpserver-python312-xfgvc running /httpserver-python312 512MiB 1 icy-violet-hce1qwm5.fra.unikraft.app just now fra httpserver-python312-lscmn running /httpserver-python312 512MiB 1 restless-orangutan-1lklj6z5.fra.unikraft.app just now ``` Three instances run—the original plus two replicas. ### Create instances from an existing image In this final workflow, take the existing image and start new instances from it: ```bash unikraft run --metro fra \ -p 443:8080/http+tls \ -m 512M \ --image /httpserver-python312@sha256:1b815914eb568a06ca4bbfdfb7d6cf484a9e9a0947ba8e0e0f1664d972a25bca ``` :::note Pinning the digest is optional. Providing the exact hash guarantees you deploy the exact same immutable image every time. This prevents unexpected behavior if a developer overwrites a standard tag like latest. If you omit the digest, the CLI automatically pulls the newest version of your image. ::: You now have a new instance created from the existing image. ## Learn more * The [CLI reference](/docs/cli/unikraft). * Unikraft Cloud's [REST API reference](/api/platform/v1). * [Porting an existing container app to Unikraft Cloud](/tutorials/docker-to-ukc). * [Tutorial](/tutorials/rootfs-volumes-roms) explaining the difference of storage media on Unikraft Cloud. * [Rootfs image formats](/tutorials/rootfs-formats). * [Rootfs image compression](/tutorials/rootfs-compression). --- ## Document: Hosting options Where Unikraft Cloud runs: the hosted platform, dedicated hosts, your own cloud, or your own servers. URL: /docs/platform/hosting-options # Hosting options Unikraft Cloud runs in four places. The availability block at the top of a feature page tells you where that feature works. | Option | Who runs the nodes | Who can use it | |---|---|---| | [Unikraft Cloud](#unikraft-cloud) | Unikraft, in the [metros](/platform/metros) | Everyone with an account | | [Dedicated hosts](#dedicated-hosts) | Unikraft, on nodes reserved for you | Enterprise customers | | [BYOC](#byoc) | Unikraft, in your cloud account | Enterprise customers | | [On-prem](#on-prem) | You, in your data center | Enterprise customers | ## Unikraft Cloud Unikraft Cloud is the hosted platform. Unikraft operates the nodes in the public [metros](/platform/metros), and you deploy with the [CLI](/cli/overview), the [API](/api/platform/v1), or an [SDK](/sdks/overview). [Sign up](https://console.unikraft.cloud/signup) to get an access token. Some features need an enterprise plan on Unikraft Cloud. The availability block shows an **Enterprise** hint on the Unikraft Cloud chip for those features. To enable one for your account, send an email to [support@unikraft.com](mailto:support@unikraft.com). ## Dedicated hosts With dedicated hosts, Unikraft Cloud runs on nodes that serve only your organization. Unikraft operates the nodes, on bare metal or on cloud VMs, and no other customer shares them. This is the option when you want control of the host but no cloud account or data center of your own. ## BYOC With BYOC, short for bring your own cloud, Unikraft Cloud runs on servers in your own cloud account. You keep the data and the network inside your account, and Unikraft operates the installation. ## On-prem With on-prem, Unikraft Cloud runs on your own servers in your own data center. This is the option for compliance programs and for workloads that must not leave your network. ## Dedicated, BYOC, and on-prem installations A dedicated, BYOC, or on-prem installation runs under a license, so every feature on it belongs to an enterprise plan. Some features exist only on these installations, because they need control of the nodes. Examples are [live migration](/features/live-migration), [managed network mode](/features/custom-network-configuration#managed-network-mode), and the [local registry](/cli/registries). Each installation is a metro of its own. To target it with the CLI, define a [profile](/tutorials/kraftkit-to-unikraft#profile-management) with the API endpoint of the installation, then pass its name with `--metro`. To discuss a dedicated, BYOC, or on-prem installation, [get in touch](https://unikraft.com/contact). ## Reading the availability block - **Available on** lists the four hosting options. A check mark means the feature works there. A clock means it's coming soon. A cross means it's not available there. A **Differs** hint means the page describes something that behaves differently on that option, and the notes explain how. - **Interfaces** shows whether the API, the CLI, and the SDKs expose the feature. - **Notes** give the next step when something is missing. A note can name a workaround or a contact address. --- ## Document: Domains URL: /docs/platform/domains # Domains When attaching a [service](/platform/services) to an instance, Unikraft Cloud exposes the instance on a subdomain of `unikraft.app` by default. You can also use your own custom domain instead. ## Configuring your external DNS provider Before Unikraft Cloud can launch an app, you need to ensure that you have configured things correctly with your DNS provider: * For a subdomain you own such as `docs.mydomain.com`, a CNAME is enough. In this case, you can just add a CNAME record with your sub-domain (`docs`) as the host and point the record to Unikraft Cloud's metro (for example, `fra.unikraft.app`). * For an apex domain (for example, `mydomain.com`), add one of: ALIAS[^1], ANAME[^1], or a flattened CNAME record. Depending on your provider, leave the host field empty or enter `@`. Point the record to the metro (for example, `fra.unikraft.app`). If the provider doesn't support these record types, add an A record pointing to the metro IP address. :::tip You can find information about the Unikraft Cloud metros available to you, as well as their IP addresses, via the CLI. ```bash unikraft metros list ``` ::: :::note You can have many domains for the same service. Unikraft Cloud also supports [wildcard domains](/platform/certificates). ::: {/* vale off */} [^1]: ALIAS (apex alias) and ANAME (authoritative alias) are DNS record types some providers support. {/* vale on */} ## Launching your app Assume the goal is to use NGINX as an app: ```bash title="" git clone https://github.com/unikraft-cloud/examples cd examples/nginx ``` Log into Unikraft Cloud and select a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, DE): ```bash unikraft login ``` With this in place, use the CLI to create an instance of the web server and link it to a custom name. Unikraft Cloud does this through the domain flag: ```bash unikraft build . --output /nginx:latest unikraft run --metro fra \ -m 256M \ -p 443:8080/http+tls \ --domain mydomain.com \ --image /nginx:latest ``` The resulting output of the deploy command should be like: ```ansi metro: fra name: nginx-67zbu uuid: 8a8bc1b9-0af6-420e-a426-190dc2da9eaa state: starting image: /nginx resources:  memory: 256MiB  vcpus: 1 service:  uuid: a942b9b5-ad17-3ffe-dcd2-ef4331f9087a  name: nameless-fog-0tvh1uov  domains:  - fqdn: mydomain.com  certificate:  name: mydomain.com-savtl  uuid: 51efebaf-4858-450c-8ba5-8bd1df96c20b networks: - uuid: 62d9bbf0-aec8-61f6-7bdb-86edf63dd068  private-ip: 10.0.3.3  mac: 12:b0:c6:23:ed:15 timestamps:  created: just now ``` When you issue the domain flag, Unikraft Cloud requests a new [certificate](/platform/certificates) from the public certificate authority. To check the validation status of the certificate, use the CLI: ```bash unikraft certificates list ``` You should see it first in `pending`: ```ansi METRO NAME COMMON-NAME STATE CREATED NOT-AFTER fra mydomain.com-savtl mydomain.com pending just now never ``` {/* vale off */} :::caution Unikraft Cloud requests a certificate from Let's Encrypt using the [ACME protocol](https://letsencrypt.org/how-it-works/). It can take a few seconds before issuing the certificate. If there are misconfigurations or DNS changes that your external DNS provider hasn't propagated yet, this can take much longer. ::: {/* vale on */} After their state changes to `valid`, check the deployment with `curl`: ```bash curl https://mydomain.com ``` ```text title="" Welcome to nginx! [...] ``` ## Learn more * The [CLI reference](/docs/cli/unikraft). * Unikraft Cloud's [REST API reference](/api/platform/v1), in particular the sections on [instances](/api/platform/v1/instances), [service groups](/api/platform/v1/servicegroups) and [certificates](/api/platform/v1/certificates). * The [certificates documentation](/platform/certificates) for more information on how to use custom certificates with your services. --- ## Document: Delete Locks URL: /docs/platform/delete-locks # Delete Locks import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" :::caution You can currently only manage delete locks via the [API](/api/platform/v1) or [`unikraft api`](/cli/unikraft/api). ::: Delete locks prevent accidental deletion of [instances](/platform/instances) and [volumes](/platform/volumes). When you set a delete lock on a resource, any attempt to delete it returns an error until you remove it. Delete locks apply to: - [Instances](/platform/instances) - [Instance templates](/platform/instances#instance-templates) - [Volumes](/platform/volumes) - [Volume templates](/platform/volumes#volume-templates) ## Enabling a delete lock You can set a delete lock on a resource via a `PATCH` operation: Instance Instance Template Volume Volume Template ```bash title="unikraft" unikraft api /v1/instances -X PATCH --metro fra \ '[{ "name": "", "prop": "delete_lock", "op": "set", "value": true }]' ``` ```bash title="API" curl -X PATCH \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Content-Type: application/json" \ "https://api.fra.unikraft.cloud/v1/instances" \ -d '[{ "name": "", "prop": "delete_lock", "op": "set", "value": true }]' ``` ```bash title="unikraft" unikraft api /v1/instances/templates -X PATCH --metro fra \ '[{ "name": "", "prop": "delete_lock", "op": "set", "value": true }]' ``` ```bash title="API" curl -X PATCH \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Content-Type: application/json" \ "https://api.fra.unikraft.cloud/v1/instances/templates" \ -d '[{ "name": "", "prop": "delete_lock", "op": "set", "value": true }]' ``` ```bash title="unikraft" unikraft api /v1/volumes -X PATCH --metro fra \ '[{ "name": "", "prop": "delete_lock", "op": "set", "value": true }]' ``` ```bash title="API" curl -X PATCH \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Content-Type: application/json" \ "https://api.fra.unikraft.cloud/v1/volumes" \ -d '[{ "name": "", "prop": "delete_lock", "op": "set", "value": true }]' ``` ```bash title="unikraft" unikraft api /v1/volumes/templates -X PATCH --metro fra \ '[{ "name": "", "prop": "delete_lock", "op": "set", "value": true }]' ``` ```bash title="API" curl -X PATCH \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Content-Type: application/json" \ "https://api.fra.unikraft.cloud/v1/volumes/templates" \ -d '[{ "name": "", "prop": "delete_lock", "op": "set", "value": true }]' ``` ## Removing a delete lock Before you can delete a resource, you need to remove the delete lock by setting its value to `false`: Instance Instance Template Volume Volume Template ```bash title="unikraft" unikraft api /v1/instances -X PATCH --metro fra \ '[{ "name": "", "prop": "delete_lock", "op": "set", "value": false }]' ``` ```bash title="API" curl -X PATCH \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Content-Type: application/json" \ "https://api.fra.unikraft.cloud/v1/instances" \ -d '[{ "name": "", "prop": "delete_lock", "op": "set", "value": false }]' ``` ```bash title="unikraft" unikraft api /v1/instances/templates -X PATCH --metro fra \ '[{ "name": "", "prop": "delete_lock", "op": "set", "value": false }]' ``` ```bash title="API" curl -X PATCH \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Content-Type: application/json" \ "https://api.fra.unikraft.cloud/v1/instances/templates" \ -d '[{ "name": "", "prop": "delete_lock", "op": "set", "value": false }]' ``` ```bash title="unikraft" unikraft api /v1/volumes -X PATCH --metro fra \ '[{ "name": "", "prop": "delete_lock", "op": "set", "value": false }]' ``` ```bash title="API" curl -X PATCH \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Content-Type: application/json" \ "https://api.fra.unikraft.cloud/v1/volumes" \ -d '[{ "name": "", "prop": "delete_lock", "op": "set", "value": false }]' ``` ```bash title="unikraft" unikraft api /v1/volumes/templates -X PATCH --metro fra \ '[{ "name": "", "prop": "delete_lock", "op": "set", "value": false }]' ``` ```bash title="API" curl -X PATCH \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Content-Type: application/json" \ "https://api.fra.unikraft.cloud/v1/volumes/templates" \ -d '[{ "name": "", "prop": "delete_lock", "op": "set", "value": false }]' ``` :::caution The [autokill](/features/autokill) subsystem skips instances and service groups that have a delete lock set. ::: ## Learn more * Unikraft Cloud's [REST API reference](/api/platform/v1), in particular the sections on [instances](/api/platform/v1/instances) and [volumes](/api/platform/v1/volumes). --- ## Document: Certificates URL: /docs/platform/certificates # Certificates When attaching a [service](/platform/services) to an instance, Unikraft Cloud exposes the instance on a subdomain of `unikraft.app` by default. It automatically issues a certificate for the domain from a public certificate authority. {/* vale off */} :::tip[Drive certificates from your own code] The [SDKs](/sdks/overview) wrap the certificate API, so renewals are a job your own code can run: ```python await ukc.certificates.get(name="tls").update(chain=chain_pem, pkey=key_pem) ``` See the [JavaScript](/sdks/js), [Python](/sdks/python) and [Go](/sdks/go) SDKs. ::: {/* vale on */} ## Custom certificates You can upload your own certificate to use with your services: ```bash unikraft certificates create --metro fra \ --cn demo.unikraft.dev. \ --name mydomain-cert \ --pkey \ --chain ``` :::caution Make sure that the common name given ends with a dot (`.`). ::: :::info Unikraft Cloud checks both the `CN` and `SAN` fields of the certificate. The provided common name (CN) must match the one on the certificate file, and the certificate must be valid for the domain you want to use. It can also be a wildcard domain. ::: Use the CLI to view and manage certificates. For example, to list certificates: ```bash unikraft certificates list ``` You should see output like: ```ansi METRO NAME COMMON-NAME STATE CREATED NOT-AFTER fra mydomain-cert demo.unikraft.dev valid 1 minute ago in 12 months fra mydomain.com-sa4x9 *.mydomain.com valid 5 days ago in 12 months ``` Retrieve full information about a certificate with: ```bash unikraft certificates get mydomain-cert ``` You should see output like: ```ansi metro: fra name: mydomain-cert uuid: 9fabfc61-a168-439e-89e2-0d5308ee9f13 common-name: demo.unikraft.dev subject: CN=demo.unikraft.dev issuer: CN=demo.unikraft.dev serial-number: 43C149067F939CD41100A74905B80560C315E92F state: valid timestamps:  created: just now  not-before: 1 minute ago  not-after: in 12 months ``` ## Attaching certificates to services You attach the certificate to a service group implicitly. You do this by creating an instance with a [domain](/platform/domains) that matches the common name of the certificate: ```bash unikraft run --metro fra \ -m 256M \ -p 443:8080/http+tls \ --domain demo.unikraft.dev \ --image /nginx:latest ``` ```ansi metro: fra name: nginx-67zbu uuid: 8a8bc1b9-0af6-420e-a426-190dc2da9eaa state: starting image: /nginx resources:  memory: 256MiB  vcpus: 1 service:  uuid: a942b9b5-ad17-3ffe-dcd2-ef4331f9087a  name: nameless-fog-0tvh1uov  domains:  - fqdn: demo.unikraft.dev  certificate:  name: mydomain-cert  uuid: 9fabfc61-a168-439e-89e2-0d5308ee9f13 networks: - uuid: 62d9bbf0-aec8-61f6-7bdb-86edf63dd068  private-ip: 10.0.3.3  mac: 12:b0:c6:23:ed:15 timestamps:  created: just now ``` To remove a certificate, first remove any instances from the relevant service and then remove the service. Remove the certificate with: ```bash unikraft certificates delete mydomain-cert ``` ## Learn more * The [CLI reference](/docs/cli/unikraft). * Unikraft Cloud's [REST API reference](/api/platform/v1) and, in particular, the section on [certificates](/api/platform/v1/certificates). * The [domains documentation](/platform/domains) for more information on how to expose your services on custom domains. --- ## Document: Audit events Follow instance state changes live and see who or what caused each one. URL: /docs/platform/audit-events # Audit events > **Availability** > > Available on dedicated hosts, BYOC and on-prem. Coming soon on Unikraft Cloud. > > Exposed by the API. Not exposed by the CLI and the SDKs yet. > > - Unikraft Cloud: Audit events are coming soon to Unikraft Cloud. [Contact support to try them](mailto:support@unikraft.com) > - CLI: The unikraft api command waits for the response to end, so it shows nothing for a stream. Use a client such as curl -N. [Subscribe to the stream](https://unikraft.com/docs/platform/audit-events#subscribing-to-the-stream) > > See [Hosting options](https://unikraft.com/docs/platform/hosting-options). import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" **Audit events** tell you who or what changed an instance. Every `vm.state_change` and `vm.start_failed` event carries an `attribution` that names its source, such as an API request, the guest, or scale-to-zero, and the user behind an API request. You can subscribe to these events as a live stream. Use the stream to: - Keep an audit trail of who changed what. - Alert on specific causes, such as an instance that ran out of quota. - Follow instance changes as they happen, without polling the API. ## Subscribing to the stream Open a `GET` request on `/v1/audit`. The response is a [server-sent events](https://html.spec.whatwg.org/multipage/server-sent-events.html) stream that stays open and delivers each event as it happens: ```bash title="curl" curl -N \ -H "Authorization: Bearer $UKC_TOKEN" \ "https://api.fra.unikraft.cloud/v1/audit" ``` :::caution Use a client that prints the response as it arrives, such as `curl -N`. The [`unikraft api`](/cli/unikraft/api) command currently waits for the response to end, so it shows nothing for a stream. ::: Any valid API token can subscribe. The stream only delivers events for instances that your account owns, and that still holds after you delete the instance. The stream starts empty and only carries events that happen after you connect. When nothing happens for 30 seconds, the platform sends a `: heartbeat` comment line to keep the connection alive. Event stream clients ignore comment lines. ## Filtering events Three optional query parameters narrow down the stream. Each takes a comma-separated list: | Parameter | Matches an event when | |---|---| | `events` | its type is one of the listed types: `vm.state_change`, `vm.start_failed` | | `uuid` | it's about one of the listed instances | | `tags` | its instance carries **all** the listed [tags](/platform/tagging) | An event has to match every parameter you set. A parameter you leave out, or leave empty, matches everything. If you repeat a parameter, the last one wins. For example, to follow the state changes of all instances tagged `prod`: ```bash title="curl" curl -N \ -H "Authorization: Bearer $UKC_TOKEN" \ "https://api.fra.unikraft.cloud/v1/audit?events=vm.state_change&tags=prod" ``` And to follow two specific instances: ```bash title="curl" curl -N \ -H "Authorization: Bearer $UKC_TOKEN" \ "https://api.fra.unikraft.cloud/v1/audit?uuid=186ac04c-6ede-46eb-b559-7d6a97792e6b,303575f0-2b1e-4c9d-9d0e-7a1f4e2c8b55" ``` The platform answers with `400 Bad Request` for an unknown parameter, an invalid UUID or tag, or a request that carries a body. The same goes for an unknown event type, or one the stream doesn't carry such as `vm.annotate`. It answers with `429 Too Many Requests` when the controller already holds its [limit of subscriptions](#limitations). ## Event format Each event arrives as an `event:` line with its type, followed by a `data:` line with a JSON document, and a blank line: ```text event: vm.state_change data: {"type":"vm.state_change","timestamp":"2026-09-28T10:01:22.306418204Z", ...} ``` Here is the full document of an API request that stopped an instance: ```json { "type": "vm.state_change", "timestamp": "2026-09-28T10:01:22.347105522Z", "object": { "type": "i", "uuid": "186ac04c-6ede-46eb-b559-7d6a97792e6b", "owner": "242e0044-7918-462b-b4c7-d3edf94517a4", "tags": ["audit-test"] }, "attribution": { "operation": "61fbce49-8a0e-4f5b-9a3c-0d2c7e81f6a4", "kind": "stop", "trigger": "observed", "origin": "api", "user": "242e0044-7918-462b-b4c7-d3edf94517a4" }, "data": { "prev": "stopping", "new": "stopped", "stop": { "reason": ["sys-exit", "app-exit", "ukpd", "api"], "code": 33024, "exit_code": 1 } } } ``` | Field | Description | |---|---| | `type` | Event type, the same as the `event:` line | | `timestamp` | Time of the change in UTC, [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339) with nine fraction digits | | `object.type` | Kind of object: `i` for an instance | | `object.uuid` | UUID of the instance | | `object.owner` | UUID of the user that owns the instance | | `object.tags` | Tags of the instance at the time of the event | | `attribution` | Who or what caused the change, see [attribution](#attribution) | | `data` | Event-specific payload, see [event types](#event-types) | A document leaves out a field that has no value, rather than setting it to `null`. ## Event types ### `vm.state_change` The platform sends `vm.state_change` whenever an instance moves to a different state. `data.prev` and `data.new` hold the old and the new state: {/* vale off */} | State | Meaning | |---|---| | `stopped` | Not running | | `standby` | Not running, but starts automatically on incoming requests, for example after [scale-to-zero](/features/scale-to-zero) | | `starting` | Booting or resuming | | `running` | Running | | `draining` | Draining connections before it shuts down | | `stopping` | Shutting down | | `template` | Serving as a [template](/platform/instances#instance-templates) | {/* vale on */} An instance also changes between `stopped` and `standby` when only its autostart setting changes. When an instance comes to rest in `stopped` or `standby`, `data.stop` tells why it stopped. See [stop information](#stop-information). ### `vm.start_failed` The platform sends `vm.start_failed` when a start doesn't happen, for example because the start would exceed a quota. ```json { "type": "vm.start_failed", "timestamp": "2026-09-28T10:12:40.118302951Z", "object": { "type": "i", "uuid": "...", "owner": "..." }, "attribution": { "operation": "...", "kind": "start", "trigger": "requested", "origin": "api", "user": "..." }, "data": { "state": "stopped", "error": "EDQUOT", "stop": { "reason": ["ukpd"], "cause": "quota-exceeded" } } } ``` `data.state` holds the state the instance stays in, and `data.error` names the error. `data.stop` tells why, if the platform had already recorded a cause. A start that the platform turns down before it changes anything produces no `vm.state_change`. ### Stop information `data.stop` describes why an instance stopped. It holds the same information as the [stop reason](/platform/instances#stop-reason) of the instances API, with names in place of bits: | Field | Description | |---|---| | `reason` | List of what took part in the stop, see below | | `code` | Stop code of the kernel | | `exit_code` | Exit code of the app, only present with `app-exit` | | `cause` | Why the platform stopped the instance, only present without `sys-exit` | {/* vale off */} | `reason` value | Meaning | |---|---| | `sys-exit` | The kernel exited | | `app-exit` | The app exited | | `ukpd` | The platform initiated the stop | | `api` | A user initiated the stop through the API | | `force` | A force stop | | `scale-to-zero` | [Scale-to-zero](/features/scale-to-zero) or a suspend stopped the instance | | `cause` value | Meaning | |---|---| | `quota-exceeded` | Starting the instance would exceed a [quota](/platform/quotas) | | `no-gpu` | No GPU was free for the instance | | `image-unavailable` | The platform couldn't get the image | | `branch-failed` | [Branching](/features/branching) the instance failed | | `no-mem-vscaling` | The instance has a memory range for [vertical scaling](/features/vertical-scaling), but its image doesn't support memory vertical scaling | | `no-cpu-vscaling` | The instance has a vCPU range for [vertical scaling](/features/vertical-scaling), but its image doesn't support vCPU vertical scaling | {/* vale on */} ## Attribution `attribution` names who or what caused a change: | Field | Description | |---|---| | `origin` | Source of the change, see below | | `user` | UUID of the requesting user, for changes that an API request caused | | `operation` | ID that all the changes of one action share | | `kind` | What the operation does: `start`, `stop`, `drain`, `suspend`, or `restart`, absent when the change belongs to no operation or the operation has no kind to name | | `trigger` | `requested` for the change that the operation asked for, `observed` for the changes that follow from it | {/* vale off */} | `origin` | Source | |---|---| | `api` | An API request | | `guest` | The instance itself, for example because its app exited | | `proxy` | The platform proxy, for example waking up an instance for an incoming request | | `scale-to-zero` | [Scale-to-zero](/features/scale-to-zero) putting an idle instance to sleep | | `autoscale` | [Autoscale](/features/autoscale) adding or removing instances | | `scheduled-op` | A [scheduled operation](/features/cron-jobs) reaching its due time | | `restart` | The [restart policy](/platform/instances#restart-policy) of a failed instance | | `update` | An update that the platform applies to the instance | | `network` | A packet arriving for a stopped instance | | `autokill` | [Autokill](/features/autokill) | | `mtss` | The [snapshot](/features/snapshots) system | | `system` | The platform, with no other cause to name | | `unknown` | The platform couldn't tell | {/* vale on */} ### Following an operation One action often causes more than one state change. They all share the same `operation`, so you can group them. The first one has the trigger `requested`, and the rest have `observed`. Stopping an instance through the API gives two events: ```text running → stopping origin=api kind=stop trigger=requested operation=61fbce49… stopping → stopped origin=api kind=stop trigger=observed operation=61fbce49… ``` An instance with scale-to-zero that goes idle, and then wakes up for an incoming request, gives two operations: ```text running → stopping origin=scale-to-zero kind=stop trigger=requested operation=9d955682… stopping → standby origin=scale-to-zero kind=stop trigger=observed operation=9d955682… standby → starting origin=proxy kind=start trigger=requested operation=11b15061… starting → running origin=proxy kind=start trigger=observed operation=11b15061… ``` A change that the platform can't match to an operation carries the origin `system` and no `operation`. ## Missed events If your client falls behind the stream, the platform drops the oldest events it holds for you, and sends a `gap` event in their place: ```text event: gap data: {"dropped":12} ``` After a gap, read the current state of the instances you follow from the [instances API](/api/platform/v1/instances), since the stream no longer holds the whole history. ## Changes from earlier releases :::caution Release 14 changes the format of instance events in the node event log: - `vm.state_change` carries the instance UUID in `object.uuid` rather than `data.vm`. - Timestamps always have nine fraction digits. Earlier releases wrote fractions below 0.1 seconds incorrectly. - A reliable log sink drops an event after a short wait instead of blocking the controller. The sink setting `block_timeout_ms` sets the wait, 50 ms by default, and `0` restores the old blocking behavior. The platform configures log sinks at the node level, so coordinate with Unikraft to change it. ::: The same state change before and after release 14: Release 14 Earlier releases ```json { "type": "vm.state_change", "timestamp": "2026-09-28T10:01:02.873148018Z", "object": { "type": "i", "uuid": "186ac04c-6ede-46eb-b559-7d6a97792e6b", "owner": "242e0044-7918-462b-b4c7-d3edf94517a4" }, "attribution": { "operation": "2990b90d-6f27-411b-bca4-5e26f6b8ab94", "kind": "start", "trigger": "observed", "origin": "api", "user": "242e0044-7918-462b-b4c7-d3edf94517a4" }, "data": { "prev": "starting", "new": "running" } } ``` ```json { "type": "vm.state_change", "timestamp": "2026-09-28T10:01:02.873148018Z", "data": { "vm": "186ac04c-6ede-46eb-b559-7d6a97792e6b", "prev": "starting", "new": "running" } } ``` ## Limitations - A controller accepts at most 64 subscriptions at a time, across all users. Beyond that, a new subscription fails until another one closes. - The stream doesn't replay older events. - Through the public API endpoint, the stream needs the proxy configuration of release 14. - The only audit events are `vm.state_change` and `vm.start_failed`, alongside the [`gap`](#missed-events) control event. Creating or deleting an instance has no event of its own, and you can't subscribe to [`vm.annotate`](/features/annotations#annotation-events). - A restart through the CLI shows up as a stop operation followed by a start operation, not as one `restart` operation. - [Autokill](/features/autokill) after a set count of requests shows up with the origin `proxy`. ## Learn more * [Metrics](/platform/metrics): runtime statistics for your instances. * [Instances](/platform/instances): instance states and lifecycle. * [Tagging](/platform/tagging): label instances to filter the stream. * [Scale-to-zero](/features/scale-to-zero) and [autokill](/features/autokill): the platform features that change instance state on their own. * [Annotations](/features/annotations#annotation-events): the `vm.annotate` event in the node event log. * Unikraft Cloud's [REST API reference](/api/platform/v1). --- ## Document: Kubernetes URL: /docs/integrations/kubernetes # Kubernetes Unikraft Cloud integrates seamlessly with any Kubernetes cluster through a virtual [kubelet](https://kubernetes.io/docs/reference/command-line-tools-reference/kubelet/) known as **Kraftlet**. This is a lightweight Kubernetes node implementation which connects your cluster to Unikraft Cloud's high-performance compute instead of running real pods locally. This enables developers to deploy and manage Unikraft microVMs as if they were native Kubernetes pods. This integration extends Kubernetes' scheduling and orchestration capabilities to the Unikraft Cloud platform. This allows workloads to take advantage of microVM-level I/O performance, security, cold start and transparent scale-to-zero efficiency while retaining full compatibility with existing Kubernetes tooling. Upon startup, Kraftlet will register itself as a worker node with the Kubernetes API. Once Kraftlet registers itself as a node, Kubernetes can schedule Pods onto it. Any Pod scheduled to the Kraftlet node won't run as a container within the cluster. Instead, it will run a highly optimized microVM on Unikraft Cloud. Kraftlet will manage the Pod lifecycle to make sure the apps are up and running. ## Getting started Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. Grab the metro name and your API token from the dashboard and set them as environment variables: ```bash title="" # Set Unikraft Cloud access token export UKC_TOKEN=token # Set metro to Frankfurt, DE export UKC_METRO=fra ``` You can install Kraftlet into a Kubernetes cluster using its Helm chart: ```bash helm install kraftlet \ --namespace kraftlet \ --create-namespace \ --set ukc.metro=${UKC_METRO} \ --set ukc.token=${UKC_TOKEN} \ oci://ghcr.io/unikraft-cloud/helm-charts/kraftlet ``` The chart deploys Kraftlet as a StatefulSet. You can check if Kraftlet is running by checking its pods: ```bash kubectl get pods -n kraftlet ``` Which should return a single pod running: ```ansi title="" NAME READY STATUS RESTARTS AGE kraftlet-0 1/1 Running 0 38s ``` You can also check if the kraftlet successfully registered as a node: ```bash kubectl get nodes ``` Which should, among other nodes, return Kraftlet: ```ansi title="" NAME STATUS ROLES AGE VERSION kraftlet Ready agent 58s v0.6.0 ``` ### Node taint and labels Kraftlet taints its node so that nothing lands on Unikraft Cloud by accident. The default taint is `virtual-kubelet.io/provider=ukc` with the `NoSchedule` effect, so every Pod you want on Unikraft Cloud needs a matching toleration and a node selector. Turn the taint off with `--set node.taint.enable=false`, or change it through the other `node.taint.*` values. Kraftlet advertises the following labels on its node, which you can select on: | Label | Value | |---|---| | `kubernetes.io/hostname` | The node name, `kraftlet` by default. | | `kubernetes.io/role` | `agent` | | `kubernetes.io/os` | `linux` | | `kubernetes.io/arch` | The value of `node.architecture`, `amd64` by default. | | `type` | `kubelet` | | `unikraft.com/virtual-kubelet` | `true` | Anything you add under `node.labels` joins this set. :::note With `kraftlet.replication.enabled=true`, each replica registers a node named after its own Pod, such as `kraftlet-0` and `kraftlet-1`. Select on `unikraft.com/virtual-kubelet` rather than on `kubernetes.io/hostname` to spread Pods across every replica. ::: ## Autoscaling the cluster The chart above registers one node per Kraftlet, against capacity that already exists. To let the cluster create that capacity on demand, add the [Karpenter provider for Unikraft](/integrations/karpenter). It launches a machine when a Pod has nowhere to run, installs Kraftlet against it, joins it as a node, and removes both again once the capacity goes unused. ## Examples Below are examples of Kubernetes configurations that define Unikraft Cloud apps through Kubernetes concepts. You will notice that each workload object defines `tolerations` and `nodeSelector` so Pods get scheduled on the Kraftlet node. :::warning Make sure Kraftlet is up and running before trying out examples below. ::: ### Simple app The configuration below defines an app with three replicas running the NGINX image and a single Kubernetes service that exposes port `443`. For each service backed by a Pod scheduled to the Kraftlet node, Kraftlet will create a corresponding service. In this case, Kraftlet will create three NGINX instances and a single service that exposes port 443. ```yaml title="simple.yaml" # simple.yaml apiVersion: apps/v1 kind: Deployment metadata: name: my-app spec: replicas: 3 selector: matchLabels: app: my-app template: metadata: labels: app: my-app spec: tolerations: - key: "virtual-kubelet.io/provider" operator: "Equal" value: "ukc" effect: "NoSchedule" nodeSelector: kubernetes.io/hostname: kraftlet containers: - name: nginx image: nginx:latest ports: - containerPort: 8080 --- apiVersion: v1 kind: Service metadata: name: my-service spec: selector: app: my-app ports: - port: 443 targetPort: 8080 protocol: TCP ``` You can apply the configuration with: ```bash kubectl apply -f simple.yaml ``` Once applied, you can check the status of your newly created pods in the Kubernetes cluster: ```bash kubectl get pods ``` ```ansi title="" NAME READY STATUS RESTARTS AGE my-app-78c766fb67-k2dgk 1/1 Running 0 104s my-app-78c766fb67-mfp77 1/1 Running 0 104s my-app-78c766fb67-tkkxq 1/1 Running 0 104s ``` Your app is now managed from the Kubernetes cluster, but is actually running on Unikraft Cloud. To check the instances, run: ```bash unikraft instances list ``` Which will return a list of instances created from pods above: ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra pfCUcp2sg0toDx3N60IerjgiXdqj8BqTzRPfgIX7M8R running /nginx 128MiB 1 MFyBmK4Hm5BTcjtMvVTYs2RXn3BzCzSPToyTBvjdVIO-orjlyrac.fra.unikraft.app 2 minutes ago fra N1z7fNY4CmhOP6UhqIQsbeRmu8a5Ti8zzhk20okCTwW running /nginx 128MiB 1 MFyBmK4Hm5BTcjtMvVTYs2RXn3BzCzSPToyTBvjdVIO-orjlyrac.fra.unikraft.app 2 minutes ago fra O830nhB9LeT1sLGh3SiqbjiTmM3u26gOZcvA2eKtOVt running /nginx 128MiB 1 MFyBmK4Hm5BTcjtMvVTYs2RXn3BzCzSPToyTBvjdVIO-orjlyrac.fra.unikraft.app 2 minutes ago ``` Kraftlet derives every Unikraft Cloud resource name from a hash, so the names carry no trace of the Pod they belong to. Use the [tags](#tags) Kraftlet attaches instead to find the instance behind a Pod: ```bash unikraft instances list --filter 'tags.*==k8s.io:pod=my-app-78c766fb67-k2dgk' ``` As you can see, all instances have the same FQDN. This is because Kraftlet created a corresponding Unikraft Cloud service for the Kubernetes service defined in YAML above. You can check the created service with the following command: ```bash unikraft services list ``` ```ansi METRO NAME AUTOSCALE CREATED FQDN SOURCE DESTINATION HANDLERS fra MFyBmK4Hm5BTcjtMvVTYs2RXn3BzCzSPToyTBvjdVIO false 6 minutes ago MFyBmK4Hm5BTcjtMvVTYs2RXn3BzCzSPToyTBvjdVIO-orjlyrac.fra.unikraft.app 443 8080 ["tls", "http"] ``` :::tip The generated FQDN follows the hashed service name. Annotate the Kubernetes Service with `cloud.unikraft.v1.services/domain` to pick a readable hostname instead, as described under [Service annotations](#service-annotations). ::: You can now manage your app running in Unikraft Cloud via Kubernetes resources! ### Stateful apps The example below deploys a stateful app on Unikraft Cloud that has access to a volume. To support provisioning Unikraft Cloud volumes through Kubernetes, Kraftlet watches [PersistentVolumeClaim](https://kubernetes.io/docs/concepts/storage/persistent-volumes/) (PVC) objects with storage class `ukc-volume`. Creating a new PVC object with that storage class triggers Kraftlet to create a Unikraft Cloud volume and a PersistentVolume object that marks the PVC as `Bound`. :::note This watcher is off by default. Install the chart with `--set kraftlet.enablePvcWatcher=true` to let Kraftlet manage `ukc-volume` claims. ::: Below is an example PVC with the Unikraft Cloud storage class you can apply to your cluster. ```yaml title="pvc.yaml" apiVersion: v1 kind: PersistentVolumeClaim metadata: name: my-claim spec: accessModes: - ReadWriteOnce storageClassName: ukc-volume resources: requests: storage: 10Mi ``` Once applied, you can check the created PVC status: ```bash kubectl get pvc ``` ```ansi title="" NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE my-claim Bound pv-7ab06383-ac03-4a81-968a-1b0cff03c23a 10Mi RWO ukc-volume 4s ``` You can also check the volume on Unikraft Cloud: ```bash unikraft volumes list ``` At this point, the volume exists but no instance mounts it, so it reports the `available` state. To create an instance that uses the volume, create a Kubernetes Pod that references the PVC: ```yaml title="pod.yaml" apiVersion: v1 kind: Pod metadata: name: nginx-pod spec: tolerations: - key: "virtual-kubelet.io/provider" operator: "Equal" value: "ukc" effect: "NoSchedule" nodeSelector: kubernetes.io/hostname: kraftlet containers: - name: nginx image: nginx:latest ports: - containerPort: 8080 volumeMounts: - mountPath: /usr/share/nginx/html name: nginx-storage volumes: - name: nginx-storage persistentVolumeClaim: claimName: my-claim ``` Once the Pod runs, the same volume reports the `mounted` state and names the instance that attached it. Every Pod that references the same `ukc-volume` claim mounts the same Unikraft Cloud volume. ### Running commands with `kubectl exec` Kraftlet serves `kubectl exec` through the [sandbox plugin](/features/plugins#sandbox-plugin), which runs inside the instance next to your app. Exec needs chart version 0.10.0 or later, and stays off until you give Kraftlet the plugin image. Set it by upgrading your existing Kraftlet release: ```bash helm upgrade kraftlet \ --namespace kraftlet \ --reuse-values \ --set kraftlet.env.KRAFTLET_EXEC_PLUGIN_IMAGE=plugins/sandbox:latest \ oci://ghcr.io/unikraft-cloud/helm-charts/kraftlet ``` On a fresh install, add the same `--set` flag to the [`helm install`](#getting-started) command instead. Exec needs Kraftlet to authenticate requests to its kubelet API. The chart sets this up by default with the cluster CA. Without it, Kraftlet refuses to start with exec on. Each Pod opts in with the `cloud.unikraft.v1.instances/enable-exec` annotation. Kraftlet adds the plugin only to instances whose Pod carries it: ```yaml title="exec.yaml" apiVersion: v1 kind: Pod metadata: name: nginx-exec annotations: cloud.unikraft.v1.instances/enable-exec: "true" spec: tolerations: - key: "virtual-kubelet.io/provider" operator: "Equal" value: "ukc" effect: "NoSchedule" nodeSelector: kubernetes.io/hostname: kraftlet containers: - name: nginx image: nginx:latest ports: - containerPort: 8080 ``` Apply it, and once the Pod runs, run a command inside its instance: ```bash kubectl apply -f exec.yaml kubectl exec nginx-exec -- nginx -T ``` The command runs inside the instance, and `kubectl` prints its output and returns its exit code. Kraftlet passes the arguments straight to the plugin without a shell, so the program must exist in the image. To use pipes or globs, call a shell the image ships, for example `kubectl exec nginx-exec -- sh -c 'ls /etc | wc -l'`. A few details differ from a regular kubelet: * Commands run without a real terminal. `kubectl exec -it` still forwards your input, but full-screen programs and line editing don't work. * Kraftlet collects output by polling the plugin, so each line shows up after a short delay. Set `KRAFTLET_EXEC_POLL_INTERVAL` through `kraftlet.env` to change that delay from its `250ms` default. * An instance created before you enabled exec has no plugin. Recreate its Pod to add one. ### Egress filtering with a network shield A [network shield](/features/network-shield) is an egress firewall for sandboxes. It starts closed and only lets out what a policy allows, by host, network, port, or protocol. It can also intercept TLS, rate-limit requests, and inject secrets into outgoing headers. Kraftlet runs the shield as a sidecar: the container of the Pod that `cloud.unikraft.v1.instances/relay-container` names. The shield container runs the shield image, with the `netshield` plugin attached through `cloud.unikraft.v1.instances/plugins.`. The `CA_CERT` and `CA_KEY` environment variables hold the certificate authority it signs intercepted connections with, here from a Secret: ```yaml apiVersion: v1 kind: Pod metadata: name: sandbox annotations: cloud.unikraft.v1.instances/relay-container: shield cloud.unikraft.v1.instances/plugins.shield: netshield-plugins spec: containers: - name: app image: my-app:latest - name: shield image: /netshield:latest envFrom: - secretRef: name: shield-ca ``` Kraftlet creates the shield instance first, then relays every other instance of the Pod through it, init containers too. DNS requests go through the shield as well. It publishes the address of each plugin on the shield as `cloud.unikraft.v1.instances/plugins...url` on the Pod, which is where you send the policy. Note the following: - The shield restarts whenever it stops. Stateless scale-to-zero would wipe its in-memory state, so Kraftlet turns scale-to-zero off for the shield. Scale-to-zero annotations on the Pod override that, and apply to the shield as well. - The Pod isn't `Running` until the shield runs. Services don't route to the shield, and the Pod IP is that of the other container. - The Pod-wide `cloud.unikraft.v1.instances/template` annotation skips the shield. Give the shield a template of its own with `cloud.unikraft.v1.instances/template.`. - Kraftlet deletes the shield with the Pod, after the other instances. - Every sandbox behind a shield has to trust its certificate authority, or intercepted connections fail. - A shield whose policy fails to apply stays closed, so replace the Pod. - Inbound policies aren't supported, and a Pod with a shield can't use CNI. ## Kraftlet internals This section describes how Kraftlet translates Kubernetes objects into Unikraft Cloud resources. ### Resource names Kraftlet names every Unikraft Cloud resource after a base62-encoded hash of the Kubernetes object that owns it. Hashing makes the names opaque, so map a resource back to its Kubernetes owner through the tags below rather than through the name. ### Tags Kraftlet [tags](/platform/tagging) every instance and volume it creates with the Kubernetes object it belongs to: | Tag | Instances | Volumes | |---|---|---| | `kraftlet:node=` | Yes | Yes | | `k8s.io:namespace=` | Yes | Yes | | `k8s.io:pod=` | Yes | No | | `k8s.io:container=` | Yes | No | | `k8s.io:pvc=` | No | Yes | Kraftlet replaces characters outside `A-Za-z0-9-+_.:=` with `_` and truncates any tag longer than 256 bytes. Filter on these tags to find the resources behind a Kubernetes object: ```bash unikraft instances list --filter 'tags.*==kraftlet:node=kraftlet' unikraft volumes list --filter 'tags.*==k8s.io:pvc=my-claim' ``` ### Ports and handlers When Kraftlet maps a Kubernetes Service port to a Unikraft Cloud service, it derives the [handler](/platform/services#handlers) from the port number automatically: | Port | Handler applied | |------|-----------------| | `80` | `http` | | `443` | `tls + http` | | Any other port | `tls` | This is why the example above produces `443:8080/tls+http` in the service list. Kraftlet infers `tls+http` from port 443. A Service port only maps to a container when the port's `targetPort` matches a port the container declares, either by number or by name. Kraftlet skips container ports without such a match, so declare every port you expose in the container specification. A Pod that backs no Service, or whose Service ports match no container port, still reaches the network as long as it declares exactly one container port. In that case Kraftlet creates a service on port 443 with the `tls` and `http` handlers, and the platform assigns a generated FQDN. A Pod that backs no Service and declares more than one container port fails, and so does a Pod whose labels match more than one Kubernetes Service. ### Multi-container pods Kraftlet maps each container in a pod to a **separate Unikraft Cloud instance**. When a Pod has a single container, the Unikraft Cloud service covers the whole Kubernetes Service. When a Pod has more than one container, each container gets its own Unikraft Cloud service derived from the Service name and the container name. Kraftlet supports init containers. Kraftlet schedules both regular containers and init containers as Unikraft Cloud instances, and deletes them together when you delete the Pod. Some containers only make sense inside a cluster, such as a log shipper or a service mesh sidecar that a Unikraft Cloud instance never needs. List those in the `cloud.unikraft.v1.instances/ignore` annotation and Kraftlet skips them. An ignored container gets no instance and no service, and Kraftlet reports it as running so the Pod still becomes ready. ### Compute resources Kraftlet sizes each instance from the container resource block, preferring limits over requests: | Instance property | Source | Default | |---|---|---| | Memory | `limits.memory`, otherwise `requests.memory`, rounded up to whole MiB | `128` MiB | | vCPUs | `limits.cpu`, otherwise `requests.cpu`, rounded up to whole CPUs | `1` | A request such as `cpu: 500m` yields a single vCPU, and `cpu: 2` yields two. Kraftlet passes the rest of the container specification through as well: | Pod or container field | Unikraft Cloud instance property | |---|---| | `image` | Image, prefixed with `oci://` when Kraftlet resolves pull credentials | | `imagePullPolicy` | Pull policy | | `command` and `args` | Instance arguments, concatenated in that order | | `env` | Instance environment | | `spec.restartPolicy` | Restart policy: `Always`, `OnFailure` or `Never` | The `cloud.unikraft.v1.instances/template` annotation changes this mapping. Kraftlet then creates the instance from the named [instance template](/platform/instances#instance-templates). The template supplies the image, resources, arguments, environment and volumes, and Kraftlet adds only the service, ROMs, plugins, scale-to-zero settings and tags. ### Environment variables Kraftlet resolves the container environment in the cluster before it creates the instance, the same way a kubelet does. It supports `envFrom` with a ConfigMap or Secret reference, `valueFrom` with `configMapKeyRef`, `secretKeyRef` or `fieldRef`, `$(VAR)` expansion between variables, and the service link variables Kubernetes injects for Services in the same namespace. It marks optional references that go missing with a Pod event instead of failing. It doesn't support `resourceFieldRef`. ### Files from ConfigMaps, Secrets and images Kraftlet turns file-shaped volume mounts into [ROMs](/features/roms), one ROM per container, and mount. The mount path becomes the ROM mountpoint, and every key becomes a file inside it: | Pod volume source | ROM content | |---|---| | `configMap` | One file per key, or one file per entry under `items` | | `secret` | One file per key, or one file per entry under `items` | | `downwardAPI` | One file per entry, holding the referenced Pod field | | `projected` | The merged content of its ConfigMap, Secret, downward API and service account token sources | | `image` | The referenced image, attached directly as a ROM image | Kraftlet requests service account tokens from the API server through the TokenRequest API, so projected tokens carry the expiry the Pod asks for. It honors `optional: true` on ConfigMap and Secret sources and skips whatever it can't find. :::note Kraftlet resolves ROM content when it creates the instance. Later edits to a ConfigMap or Secret don't reach an instance that already runs, so restart the Pod to pick them up. ::: ### Volumes Kraftlet maps the remaining volume types onto Unikraft Cloud storage: | Pod volume | Unikraft Cloud resource | |---|---| | Claim with the `ukc-volume` storage class | One [volume](/platform/volumes) per claim, shared by every Pod that mounts it | | Claim with any other CSI storage class | One volume per Pod, staged through the CSI driver | | `emptyDir` | One volume per Pod and mount, sized from `sizeLimit` and defaulting to 100 MiB | | `emptyDir` with `medium: Memory` | Nothing, Kraftlet skips the mount | | `hostPath` | One [managed volume](/features/managed-volumes) per Pod and mount | Kraftlet rounds an `emptyDir` size limit up to whole MiB. It deletes the volumes it created for `emptyDir` and `hostPath` mounts together with the Pod, while a `ukc-volume` claim keeps its volume until you delete the claim. `hostPath` support stays off unless you set `KRAFTLET_ENABLE_HOST_PATH_VOLUMES=true` through `kraftlet.env`. #### Third-party CSI drivers Kraftlet can serve claims that belong to another storage system, such as a cloud block store, by driving that system through its CSI driver. Register each driver under `csi.plugins` as a `driverName: host:port` pair, or as a path to its socket. ```yaml title="values.yaml" csi: plugins: ebs.csi.aws.com: "ebs-csi-node.kube-system.svc:9000" ``` For a Pod that mounts such a claim, Kraftlet waits for the volume attachment when the driver needs one, then calls the driver to stage the volume under `csi.stagingBasePath`. It then creates a managed volume that points at the staging path. It health-checks every registered driver on the `csi.healthCheckInterval` and refuses to stage through a driver that reports unhealthy. Setting any `csi` value also makes the chart advertise the `volumes.kubernetes.io/controller-managed-attach-detach` node annotation. :::note The platform resolves a managed volume path on the machine that runs the instance. This flow expects Kraftlet to stage volumes on that same machine, which holds for on-prem and bring-your-own-cloud installations. ::: ### Init containers Kraftlet runs init containers as ordinary instances, one after another, before it creates the instances for the regular containers. Each init instance starts with autostart off, restarts off and scale-to-zero off. Kraftlet starts it, waits for it to stop, and treats a non-zero exit code as a failure. A Pod with the `Always` or `OnFailure` restart policy makes Kraftlet retry a failed init instance with an exponential backoff that grows from one second to five minutes. With `Never`, the first failure stops the Pod from starting. ### Private registries Kraftlet reads the Secrets listed under `spec.imagePullSecrets` and passes the matching credentials to the platform with the image. It accepts both the `kubernetes.io/dockerconfigjson` and the `kubernetes.io/dockercfg` Secret types, and matches an entry to the image by registry host. An image without a registry host, such as `nginx:latest`, and an image on `docker.io` both match the `index.docker.io` entry. ### Pod status Kraftlet subscribes to the platform's [event stream](/platform/audit-events) and updates a Pod as soon as its instance stops, fails, or errors. It also keeps refreshing the status of every Pod it manages on the `kraftlet.podStatusUpdateInterval`, which defaults to 15 seconds, to catch changes the stream misses. When the stream drops events, it re-syncs every Pod on the node. A move into `running` or `standby` triggers no refresh, so scale-to-zero churn doesn't flood the Kubernetes API. Set `KRAFTLET_ENABLE_INSTANCE_EVENTS=false` through `kraftlet.env` to go back to polling only. Kraftlet reads instance state through an instance cache instead of asking the platform once per Pod. The cache merges the lookups that arrive together into shared requests, and remembers newly created instances so their first status updates need no extra request. As a result, the time from creating a Pod to seeing it `Running` stays flat as the node grows. It derives the Pod phase from the state of the backing instances: | Instance state | Pod phase | |---|---| | `starting` | `Pending` | | `running`, `draining`, `stopping` | `Running` | | `standby` | `Running`, or `Pending` while the platform reports a failure | | `stopped` after a clean shutdown | `Succeeded` | | `stopped` after a fault or a failed image pull | `Failed` | | Any state, once the platform stopped the instance for insufficient quota | `Failed` | The container status carries the detail behind a failure. An instance the platform stopped for running out of memory surfaces as `OOMKilled` with exit code 137. A failed image pull surfaces as `ErrImagePull`, a stop for insufficient quota surfaces as `QuotaExceeded` with exit code 1, and any other platform-side stop surfaces as `PlatformError`. Note that the running quota is dynamic, so on the next request, an instance which failed to start with `QuotaExceeded` might succeed. An instance with exactly one network interface also contributes its private IP as the Pod IP. Kraftlet records what it does on the Pod as events: ```ansi title="" LAST SEEN TYPE REASON OBJECT MESSAGE 12s Normal CreateInstanceSuccess pod/my-app-78c766fb67-k2dgk started instances ``` `CreateInstanceFailed` and `PodCreateServiceFailed` carry the platform error that blocked the Pod. `kubectl logs` works against a Pod on the Kraftlet node. Kraftlet serves it from the instance console and returns the last 4096 bytes. The `--tail` and `--limit-bytes` flags move that window, and Kraftlet counts both in bytes rather than in lines. ### Node capacity and conditions Kraftlet reports the node capacity from your Unikraft Cloud [quotas](/platform/quotas), so the Kubernetes scheduler stops placing Pods once you run out of headroom: | Node resource | Quota | |---|---| | `cpu` | Live vCPU quota | | `memory` | Live memory quota | | `pods` | Instance quota | Kraftlet reports allocatable capacity equal to capacity, and refreshes both on every node status interval. It also maps quota exhaustion and platform health onto node conditions: | Condition | Kraftlet sets it when | |---|---| | `Ready` | The platform answers its health check | | `MemoryPressure` | The workloads use up the live memory quota | | `DiskPressure` | The volumes use up the storage quota | | `PIDPressure` | The instances use up the live instance quota | | `UnikraftPlatformHealthy` | The platform health endpoint reports a healthy state | An unreachable platform turns `Ready` to false, so the scheduler stops placing new Pods on the node until the platform answers again. ### Pod rescheduling Idle instances go to `standby` and hold no live memory, which lets a node carry far more Pods than its live memory quota can run at once. Resuming one can fail once the quota is full, and the platform then leaves the instance in a stopped or scaled-to-zero state rather than starting it. Kraftlet reports such a Pod as `Failed` with the reason `QuotaExceeded`, and then deletes the Pod so the controller that owns it schedules a replacement. The node reports `MemoryPressure` as soon as its next quota refresh finds the live memory quota exhausted. That condition becomes a `node.kubernetes.io/memory-pressure:NoSchedule` taint, which your Pods don't tolerate, so the replacement stays off that node until live memory frees up. On a cluster that [autoscales with Karpenter](/integrations/karpenter), the replacement lands on another node, or on a node the autoscaler creates for it. Keep these in mind when running your workloads: * Run Pods under a Deployment, ReplicaSet, StatefulSet or Job, since Kraftlet reclaims only a Pod that has a controller behind it. It leaves a bare Pod `Failed` in place, because deleting that Pod would leave nothing behind to replace it. * Reclaiming a Pod runs the same teardown as a delete, so its instances and Pod-scoped volumes (including `emptyDir` and `hostPath` volumes) go with it. Volumes behind PersistentVolumeClaims remain until you delete their claims. Kraftlet reclaims failed Pods by default. Set `kraftlet.enableFailedPodReclaim=false` to leave them in place instead. ### Resource lifecycle Kraftlet adds the `cloud.unikraft.v1/resources` finalizer to every Pod it accepts, so a delete only completes once Kraftlet removes the Unikraft Cloud resources. When you delete a Kubernetes object, Kraftlet deletes the corresponding Unikraft Cloud resources: | Kubernetes object deleted | Unikraft Cloud resources deleted | |---|---| | Pod or Deployment replica | Instances for its containers and init containers, the service once no instance uses it, the volumes for its `emptyDir` and `hostPath` mounts, and any certificate the service held | | PersistentVolumeClaim with the `ukc-volume` class | The volume behind the claim | Kraftlet also cleans up a Pod that finishes on its own. Once every container reaches a successful stop and the Pod phase becomes `Succeeded`, Kraftlet deletes its instances and volumes. :::caution Turn off finalizers by setting `KRAFTLET_POD_FINALIZER=""`, but be careful-this risks leaking resources on the Unikraft platform when the Kraftlet node is draining. ::: ## Platform features Kraftlet supports the following Unikraft Cloud platform features on Kraftlet-managed resources: * [**Instance templates**](/platform/instances#instance-templates) Frequently deployed workloads can go into instance templates. Templates pre-warm the snapshot, reducing cold-start latency for every new instance created from the template. Point a Pod at one with the `cloud.unikraft.v1.instances/template` annotation. * [**Scale-to-zero**](/features/scale-to-zero) Instances that back a service or carry plugins suspend automatically when idle. Scale-to-zero runs by default for those instances and you can configure it through Pod annotations. Kraftlet turns it off for an instance with neither a service nor a plugin, since nothing would wake it again. * [**ROMs**](/features/roms) Kraftlet ships ConfigMaps, Secrets, downward API fields and image volumes to instances as ROMs. * [**Plugins**](/features/plugins) A Pod can attach plugins to its instances through a ConfigMap, and Kraftlet annotates the Pod with the endpoint of each running plugin. * [**Managed volumes**](/features/managed-volumes) `hostPath` mounts and volumes staged through third-party CSI drivers become managed volumes. * [**Tagging**](/platform/tagging) Every instance and volume Kraftlet creates carries the identity of its Kubernetes owner. * [**Custom network configuration**](/features/custom-network-configuration) Instances can carry addresses and TAP devices that you choose instead of ones from the platform pool. Kraftlet drives this through CNI plugins. * [**Annotations**](/features/annotations) Instance metadata that also reaches the guest. Kraftlet writes the [CNI result](/features/annotations#guest-network-configuration-with-cni) that a guest configures its own interfaces from. * [**Network shield**](/features/network-shield) A sidecar container of the Pod filters its egress traffic. See [egress filtering with a network shield](#egress-filtering-with-a-network-shield). ## Custom networking with CNI {/* vale off */} :::caution[**Limited Access**] CNI-based networking for Kraftlet is available as part of enterprise plans. To enable it for your account, reach out to the [Unikraft Cloud Discord](https://unikraft.com/discord) or send an email to [support@unikraft.com](mailto:support@unikraft.com). ::: {/* vale on */} The [Container Network Interface](https://www.cni.dev/) (CNI) configures the networking stack of a workload through plugins. This split lets vendors such as AWS, Tigera and Isovalent each ship their own plugin for configuring networking. Kubernetes builds on the same interface, and the kubelet invokes the installed CNI plugins on every node of the cluster. Kraftlet takes that role for Pods on its node that opt in with the `cloud.unikraft.v1.instances/cni-enabled` annotation. It invokes the CNI plugins of your cluster and passes the resulting interfaces and addresses to Unikraft Cloud. The platform then creates the instance with [custom network interfaces](/features/custom-network-configuration) rather than with interfaces from its own address pool. Instances join the same networks as native Pods and follow the same address management as the rest of the cluster. A companion component, `remote-cni`, exposes the plugins over gRPC for clusters where Kraftlet runs apart from the machine that hosts the instances. An instance's `network_interfaces` only accept IPv4 addresses. Therefore, the Kraftlet hands the rest of a plugin result—IPv6 addresses, in practice—to the guest through the [`unikraft.com/cni` annotation](/features/annotations#guest-network-configuration-with-cni), and the guest configures them on its own interfaces. Kraftlet does this for the Pod's default network, a secondary network contributes its IPv4 addresses alone. You can write the same annotation yourself on an instance you create through the API, for an [interface without an address](/features/custom-network-configuration#interfaces-without-an-address) whose addressing an IPAM system owns. A CNI Pod can bring its own resolvers. Kraftlet passes the Pod's `spec.dnsConfig`, its nameservers, search domains, and options, to the instance, and falls back to the DNS from the CNI result when the Pod sets none: ```yaml apiVersion: v1 kind: Pod metadata: name: my-pod annotations: cloud.unikraft.v1.instances/cni-enabled: "true" spec: dnsPolicy: None dnsConfig: nameservers: ["2001:4860:4860::8888"] searches: ["svc.example.internal"] ``` `dnsPolicy` itself has no effect, and a Pod without CNI keeps the platform's resolver. ## Annotations Kraftlet reads the following annotations from Pod and Service objects to configure Unikraft Cloud resources. ### Pod annotations | Annotation | Type | Default | Description | |---|---|---|---| | `cloud.unikraft.v1.instances/autostart` | boolean | `true` | Whether the instance starts automatically when Kraftlet schedules the Pod. | | `cloud.unikraft.v1.instances/cni-enabled` | boolean | `false` | Configures the Pod's networking through the CNI plugins of the cluster, as [custom networking with CNI](#custom-networking-with-cni) describes. | | `cloud.unikraft.v1.instances/template` | string | — | Name of a pre-existing Unikraft Cloud instance template to use instead of the container image. | | `cloud.unikraft.v1.instances/template.` | string | — | Same as above, for a single container, where an empty value keeps that container on its image. | | `cloud.unikraft.v1.instances/ignore` | string | — | Comma-separated container names that Kraftlet leaves out of Unikraft Cloud. | | `cloud.unikraft.v1.instances/enable-exec` | boolean | `false` | Adds the sandbox plugin so [`kubectl exec`](#running-commands-with-kubectl-exec) reaches the instance. Needs exec turned on in Kraftlet. | | `cloud.unikraft.v1.instances/plugins` | string | — | ConfigMap holding the plugin list, written as `` or `/`. | | `cloud.unikraft.v1.instances/plugins.` | string | — | Same as above, for a single container of a multi-container Pod. | | `cloud.unikraft.v1.instances/relay-container` | string | — | Container of the Pod that runs as its [network shield](#egress-filtering-with-a-network-shield), with every other instance of the Pod relaying through it. | | `cloud.unikraft.v1.instances/scale_to_zero.policy` | `on` \| `off` \| `idle` | `on` | Enables or disables scale-to-zero for instances that back a service or carry plugins. | | `cloud.unikraft.v1.instances/scale_to_zero.stateful` | boolean | `false` | When `true`, Kraftlet retains the instance state when scaling to zero. | | `cloud.unikraft.v1.instances/scale_to_zero.cooldown_time_ms` | integer | `1000` | Idle time in milliseconds before Kraftlet suspends the instance. | The plugin ConfigMap holds a JSON array of plugin objects under the `plugins.json` key, or under the key you name in the annotation. Kraftlet also accepts a ConfigMap with a single key of any name. ```yaml title="plugins.yaml" apiVersion: v1 kind: ConfigMap metadata: name: my-plugins data: plugins.json: | [ {"name": "metrics", "rom": "my-org/metrics:latest", "config": {"interval": 5}} ] ``` ### Service annotations | Annotation | Type | Default | Description | |---|---|---|---| | `cloud.unikraft.v1.services/domain` | string | — | Custom [domain](/platform/domains) for the Unikraft Cloud service. | | `cloud.unikraft.v1.services/domain.` | string | — | Per-container domain for a multi-container Pod. | A bare label such as `my-app` becomes a subdomain of the metro, giving `my-app.fra.unikraft.app`. A fully qualified name such as `app.example.com` makes the platform request a [certificate](/platform/certificates) for it. For a multi-container Pod, the global annotation applies to every container as `-`, and the per-container form overrides it. ### Status annotations Kraftlet writes a few annotations back onto the Pod as it reports status: | Annotation | Description | |---|---| | `cloud.unikraft.v1.instances/plugins..url` | Address of a running plugin, prefixed with the container name for multi-container Pods and for the network shield. | | `cloud.unikraft.v1.instances/fqdns` | JSON object mapping each container to the private and service FQDN of its instance. | The FQDN annotation stays off until you install the chart with `--set kraftlet.enableInstanceFqdnAnnotations=true`. ## Helm chart values The values below cover the settings most deployments touch. | Value | Default | Description | |---|---|---| | `ukc.metro` | — | Metro name such as `fra`, or a full API endpoint address. | | `ukc.token` | — | Unikraft Cloud token, which the chart stores in a Secret. | | `image.name` and `image.tag` | `ghcr.io/unikraft-cloud/kraftlet` and `latest` | Kraftlet image. | | `node.name` | `kraftlet` | Name Kraftlet registers with the control plane. | | `node.architecture` | `amd64` | Architecture the node advertises. | | `node.taint.enable` | `true` | Whether Kraftlet taints its node. | | `node.taint.key`, `node.taint.value`, `node.taint.effect` | `virtual-kubelet.io/provider`, `ukc`, `NoSchedule` | The taint Kraftlet applies. | | `node.labels` and `node.annotations` | — | Extra labels and annotations for the node object. | | `node.providerId` | — | Provider ID of the machine backing the node. | | `kraftlet.replication.enabled` and `kraftlet.replication.replicas` | `false` and `1` | Run more than one Kraftlet, each registering its own node. | | `kraftlet.enablePvcWatcher` | `false` | Manage the lifecycle of `ukc-volume` claims. | | `kraftlet.enableInstanceFqdnAnnotations` | `false` | Annotate Pods with the FQDNs of their instances. | | `kraftlet.enableFailedPodReclaim` | `true` | Delete a Pod that fails because the live memory quota is full, so its controller replaces it. | | `kraftlet.podStatusUpdateInterval` | `15s` | How often Kraftlet refreshes Pod status from the platform, alongside the event stream. | | `kraftlet.podSyncWorkers` | `1` | Number of Pod reconcile workers. | | `kraftlet.logLevel` and `kraftlet.logType` | `info` and `json` | Log verbosity and format. | | `kraftlet.port` | `10250` | Port for the kubelet API that serves logs and Pod listings. | | `kraftlet.k8s.qps` and `kraftlet.k8s.burst` | — | Rate limits for the API server client. | | `kraftlet.env` | — | Extra environment variables for the Kraftlet container. | | `csi.plugins` | — | CSI drivers Kraftlet calls, as `driverName: host:port` pairs. | | `csi.stagingBasePath` | `/var/lib/kubelet/plugins/kubernetes.io/csi/staging` | Where Kraftlet stages CSI volumes. | | `csi.healthCheckInterval` | `10s` | How often Kraftlet health-checks each CSI driver. | | `tls.secretName` and `tls.secretKeys` | — | Serving certificate for the kubelet API. | | `resources` | — | Resource requests and limits for the Kraftlet pod. | | `priorityClassName` | — | PriorityClass for the Kraftlet pod, which keeps its node registered under pressure. | ## Current limitations A Kraftlet node is a virtual kubelet in front of a remote platform, so a few kubelet behaviors have no counterpart: * `kubectl attach` doesn't reach an instance. Use [`kubectl exec`](#running-commands-with-kubectl-exec) to run commands inside it instead. * `kubectl top pod` returns nothing, since Kraftlet serves no stats summary. Read [instance metrics](/platform/instances#instance-metrics) instead. * Liveness, readiness, and startup probes never run. Kraftlet reports readiness from the instance state. * Kraftlet applies no in-place updates to a Pod specification. Recreate the Pod to change the instance behind it. * A Pod may back at most one Kubernetes Service. * `subPath` on a volume mount has no effect. * An `emptyDir` with `medium: Memory` gets no backing volume. --- ## Document: Karpenter URL: /docs/integrations/karpenter # Karpenter {/* vale off */} :::caution[**Limited Access**] The Karpenter provider for Unikraft is a preview feature available as part of enterprise plans. The behavior described here reflects the current implementation and may change before general availability. To try the preview, reach out to the [Unikraft Cloud Discord](https://unikraft.com/discord) or send an email to [support@unikraft.com](mailto:support@unikraft.com). ::: {/* vale on */} [Kraftlet](/integrations/kubernetes) lets a Kubernetes cluster run Pods on Unikraft Cloud, but the cluster still needs nodes to schedule those Pods onto. The Karpenter provider for Unikraft creates them on demand. When a Pod has nowhere to run, the provider launches a machine running the Unikraft Cloud platform and installs Kraftlet against that machine. Then the provider joins it to the cluster as a node. Once the capacity goes unused, the provider removes the node and the machine again. The provider builds on [Karpenter](https://karpenter.sh), the Kubernetes node autoscaler, so the objects and the workflow are the ones Karpenter users already know. ## How a node comes up {/* vale off */}
An unschedulable Pod triggers a NodeClaim, the Karpenter provider reads the nodeclass and creates a machine through the cloud API, then installs Kraftlet, which connects to the machine and registers it as a node that runs the Pod. {/* Cluster boundary */} Kubernetes cluster (EKS, GKE, AKS) {/* Control plane */} Control plane etcd Kubernetes API {/* Kubernetes objects */} NodeClaim Pod (unschedulable) {/* Unikraft components */} Karpenter provider for Unikraft Kraftlet {/* Outside the cluster */} Cloud API (AWS, GCP) Unikraft Cloud machine {/* Steps */} 1. Triggers a NodeClaim 2. Reads the UnikraftNodeClass 3. Creates a machine 4. Starts the machine 5. Installs Kraftlet with its Helm chart 6. Connects to the platform API 8. Runs the Pod 7. Registers as a node
Karpenter turns unschedulable Pods into machines, and Kraftlet turns each machine into a node.
{/* vale on */} No kubelet runs on the machine. Kraftlet takes that role from inside your cluster and reaches the machine over the network, so each machine needs an endpoint your cluster can reach. A node joins the cluster only once the platform on its machine reports healthy. ## What it's good for Without it, a Kraftlet cluster scales to a limited, static capacity. The provider makes Unikraft Cloud capacity elastic from inside Kubernetes, driven by the autoscaler Kubernetes users already run. Nodes arrive when Pods need them and go away once they empty, so a deployment that swings between nothing and thousands of instances holds nodes only while it runs. It also keeps a full node from stalling the cluster. A Pod that no longer fits goes back to its controller and lands on a node with room, or on a new one, as [Pod rescheduling](/integrations/kubernetes#pod-rescheduling) describes. ## What a setup looks like The provider needs the following objects in the cluster: 1. **A `UnikraftNodeClass`** describes the machine behind a node: its image, where it runs, its DNS and certificate setup, and the Kraftlet release that fronts it. 2. **A `NodePool`** specifies which machine sizes to pick, how many nodes to allow, and when to remove the ones that empty. 3. **Your workloads** reach the pool through a toleration and a node selector, the same way they reach a fixed Kraftlet node. Everything the [Kubernetes integration](/integrations/kubernetes) describes then applies to the Pods that land there, including services, volumes, ROMs, plugins, and annotations. Unikraft provides the machine images, and the setup steps for your cluster. The enterprise documentation covers the configuration in full. ## Requirements and limitations * The provider needs a Kubernetes cluster and a cloud account of your own. At the moment, the provider can provision machines on AWS and GCP. * Machines launch as on-demand instances, so a NodePool that asks for `karpenter.sh/capacity-type: spot` gets none. * A node advertises the platform quotas of its machine rather than the size of the machine underneath. You can cap the total vCPUs, memory, and volume storage a node advertises, and [scale-to-zero](/features/scale-to-zero) lets you overcommit those caps by a wide margin. * A nodeclass with `spec.box.dns.skip: true` creates no DNS records, and the cluster reaches each machine at the internal hostname of the cloud provider instead. Those machines serve a self-signed certificate, so the controller needs `controller.allowInsecure` and Kraftlet needs `KRAFTLET_ALLOW_INSECURE`, and both have to run inside the machines' VPC. * Changing a nodeclass replaces the machines made from it rather than reconfiguring them in place. The exception is `dns.skip`, which can't change after creation. --- ## Document: unikraft The Unikraft Command-Line Interface. URL: /docs/cli/unikraft # unikraft The Unikraft Command-Line Interface. ``` unikraft [flags] ``` ## Examples Login to Unikraft Cloud: ```bash unikraft login ``` List instances across metros: ```bash unikraft instances list ``` Build and publish an image from a Kraftfile: ```bash unikraft image build . --output my-org/my-app:latest ``` Deploy a new instance from an image: ```bash unikraft run --metro=sfo --image=nginx:latest --autostart -p 443:8080/http+tls --scale-to-zero policy=on ``` Switch to a different profile: ```bash unikraft profile use my-other-profile ``` ## Options ``` --config file Path to the configuration file. --log-level level Set the logging level. (default info) --log-type type Set the log type. (default text) --profile name Set the current profile. --telemetry Toggle usage analytics. (default true) --timeout duration Set a deadline for the command (e.g. 30s, 5m, 1h). ``` ## See Also * [`unikraft run`](unikraft/run.mdx): Run an image as an instance. * [`unikraft build`](unikraft/build.mdx): Build a Unikraft project into a container image. * [`unikraft tui`](unikraft/tui.mdx): Browse resources in a TUI. * [`unikraft metros`](unikraft/metros.mdx): Manage Unikraft Cloud metros. * [`unikraft quotas`](unikraft/quotas.mdx): Display quota usage for metros. * [`unikraft instances`](unikraft/instances.mdx): Manage Unikraft Cloud instances. * [`unikraft volumes`](unikraft/volumes.mdx): Manage Unikraft Cloud volumes. * [`unikraft services`](unikraft/services.mdx): Manage Unikraft Cloud services. * [`unikraft certificates`](unikraft/certificates.mdx): Manage Unikraft Cloud certificates. * [`unikraft images`](unikraft/images.mdx): Manage Unikraft Cloud images. * [`unikraft api`](unikraft/api.mdx): Make an authenticated HTTP request to the Unikraft Cloud API. * [`unikraft login`](unikraft/login.mdx): Login to Unikraft Cloud. * [`unikraft logout`](unikraft/logout.mdx): Logout from Unikraft Cloud. * [`unikraft profile`](unikraft/profile.mdx): Manage Unikraft Cloud profiles. * [`unikraft config`](unikraft/config.mdx): Manage CLI configuration. * [`unikraft completion`](unikraft/completion.mdx): Outputs shell code for initialising tab completions. * [`unikraft version`](unikraft/version.mdx): Show version information. * [`unikraft upgrade`](unikraft/upgrade.mdx): Upgrade the Unikraft CLI to the latest version. --- ## Document: Registries URL: /docs/cli/registries # Registries import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" Unikraft Cloud uses a central OCI registry at `index.unikraft.io` to store images. Every image you build and push lands in this registry under your account's namespace. When you start an instance, the platform pulls the image from this registry automatically. Each node has a local registry also running for directly pushing to a specific node. These registries have the address formatted as `index..unikraft.cloud`. The main advantage of pushing to a node's registry is that it avoids the round trip to the central registry. This speeds up deployments and reduces bandwidth usage. Directly pushing to a node's registry or using the central registry is interchangeable. The platform handles synchronization between them. :::caution[**Limited Access**] The local registry is only available in BYOC or on-prem Unikraft Cloud installations. ::: {/* vale off */} :::caution[**Temporary storage**] Do not treat a node's local registry as permanent storage. The platform cleans up its images over time, so push anything you need to keep to the central registry at `index.unikraft.io`. ::: {/* vale on */} ## Image naming The platform addresses images using the standard OCI reference format: ```text title="" index.unikraft.io//: index..unikraft.cloud//: ``` For example: ```text title="central registry" index.unikraft.io/alice/http-python312:latest index.unikraft.io/alice/http-python312@sha256:278cb8b1... ``` ```text title="local registry" index.node1.unikraft.cloud/alice/http-python312:latest index.node1.unikraft.cloud/alice/http-python312@sha256:278cb8b1... ``` The `` segment is your Unikraft Cloud organization. Tags are arbitrary strings. The platform defaults to `latest` when you don't specify a tag. You can also refer to images by digest to pin to an exact version. ### Images served over HTTP The platform can also run an image from an [OCI image layout](https://github.com/opencontainers/image-spec/blob/main/image-layout.md) that a plain HTTP or HTTPS server serves as a tarball, without a registry. Such a reference works anywhere the CLI takes an image: ```text title="" http+oci://// https+oci://// https+oci:////@ ``` ```bash unikraft run --metro fra --image https+oci://cdn.example.com/my-org/my-app/latest ``` The server has to answer with the layout tarball and a `Content-Type` of `application/vnd.oci.layout.v1+tar`. `unikraft image inspect` and `unikraft image copy` don't support these references yet. ## Authentication Registry authentication uses the same API token as the rest of the platform. The `unikraft` CLI uses [profiles](/tutorials/kraftkit-to-unikraft#profile-management) to manage the token. ```bash unikraft login ``` When using external registries (e.g. `ghcr.io`), authenticate explicitly: ```bash docker login ghcr.io -u -p ``` You don't need to login to the central or node registries. ## Pushing images ### Via `unikraft build` The simplest way to push an image is to use the CLI to build, package, push, and start an instance in one flow: ```bash title="central registry" # the index.unikraft.io domain can be omitted as it's the default unikraft build . --output index.unikraft.io//:latest unikraft run --metro fra \ --name \ -m 256M \ -p 443:8080/http+tls \ --image index.unikraft.io//:latest ``` ```bash title="local registry" unikraft build . --output index..unikraft.cloud//:latest unikraft run --metro fra \ --name \ -m 256M \ -p 443:8080/http+tls \ --image index..unikraft.cloud//:latest ``` ### External registries `unikraft build` can also push to an external OCI registry, meaning registries other than `index.unikraft.io` or a node's local registry. {/* vale off */} :::caution[**Limited Access**] External registries are only available in BYOC or on-prem Unikraft Cloud installations. Each host supports one external registry at a time through host-level configuration. To explore such a deal - reach out to the [Unikraft Cloud Discord](https://unikraft.com/discord) or send an email to [support@unikraft.com](mailto:support@unikraft.com). ::: {/* vale on */} To work with Unikraft Cloud, a registry must: * Support the [Docker Registry HTTP API V2](https://distribution.github.io/distribution/spec/api/). * Support bearer token authentication. * Accept custom fields in the OCI manifest, which Unikraft Cloud uses to store image metadata. Unikraft Cloud works with the following popular registries: * Docker Hub * GitHub Container Registry * Amazon ECR * Google Cloud Artifact Registry * DigitalOcean Container Registry * CNCF Distribution Registries based on [CNCF Distribution](https://distribution.github.io/distribution/), such as Harbor, also work. If you run into issues with a registry or want support for one not listed here, contact the Unikraft Cloud team. Log in to the registry first, as shown in [Authentication](#authentication), then pass the full image reference. For example, with GitHub Container Registry and a personal access token that has the `write:packages` scope: ```bash title="" echo | docker login ghcr.io -u --password-stdin unikraft build . --output ghcr.io//my-app:latest ``` ## Listing images ```bash unikraft images list ``` ```ansi title="central registry" NAME VERSION SIZE alice/http-python312 latest 77 MB alice/my-app latest 42 MB ``` ```ansi title="local registry" NAME VERSION SIZE alice/http-python312 latest 77 MB index..unikraft.cloud/alice/my-app latest 42 MB ``` :::note There may be a delay of a few minutes between pushing or removing an image and the image list reflecting the change. ::: ## Removing images Remove an image by its tag: ```bash unikraft images delete /: ``` If you omit the tag, the CLI deletes `/:latest`. If the image has no `latest` tag, the command deletes nothing and fails with `references not found`. Each reference deletes one image, so pass every tag you want to remove, for example `unikraft images delete /:v1 /:v2`. ## Kernel layer deduplication An image typically has two OCI layers: a kernel layer and a rootfs layer. When you push an update to an existing image, the CLI checks whether the kernel layer is already present in the registry and skips re-uploading it. Only the rootfs layer travels over the wire. This makes iterative pushes faster and cheaper on bandwidth. [ROMs](/features/roms) follow the same packaging system but go out without a kernel layer. They only have a rootfs. They appear in the image list alongside regular images. Images that boot on the [platform kernel](/platform/images#platform-kernel) carry a rootfs layer alone in the same way. ## Instance creation When creating an instance directly from an existing image, reference it by tag or digest: ```bash # By tag unikraft run --metro fra \ -m 256M \ -p 443:8080/http+tls \ --image index.unikraft.io/alice/my-app:latest # By digest (exact version pinning) unikraft run --metro fra \ -m 256M \ -p 443:8080/http+tls \ --image index.unikraft.io/alice/my-app@sha256:278cb8b1... ``` ## Learn more * [Images](/platform/images): build, package, and deploy workflow. * The [CLI reference](/docs/cli/unikraft). * Unikraft Cloud's [REST API reference](/api/platform/v1). --- ## Document: Unikraft CLI Command-line interfaces for Unikraft Cloud URL: /docs/cli/overview # Unikraft CLI The **unikraft** CLI is the new, official command-line interface for [Unikraft Cloud](https://unikraft.cloud). It provides a modern, streamlined experience for deploying and managing unikernels globally. ## Features - **Deploy Instantly** - Run unikernel images as serverless instances with `unikraft run` - **Global Infrastructure** - Deploy across supported metros with automatic multi-region support - **Resource Management** - Create, read, update, and delete instances, volumes, services, certificates, and images - **Scale-to-Zero** - Built-in support for scale-to-zero policies to optimize costs - **Output Formats** - Machine-readable JSON or human-friendly text output - **Profile Management** - Switch between accounts and configurations - **Shell Completions** - Tab completion for Bash, Zsh, Fish, and PowerShell ## Installation ```bash title="1-liner (macOS & Linux)" curl --proto '=https' --tlsv1.2 -fsSL https://unikraft.com/cli/install.sh | sh ``` ```bash title="macOS" brew install unikraft/cli/unikraft ``` ```powershell title="Windows (PowerShell)" irm https://unikraft.com/cli/install.ps1 | iex ``` ```bash title="Debian/Ubuntu" # Update and install dependencies sudo apt update sudo apt install ca-certificates curl # Download and add the GPG key sudo install -d -m 0755 /etc/apt/keyrings sudo curl -fsSL \ -o /etc/apt/keyrings/unikraft-cli.gpg \ https://pkg.unikraft.com/debian/cli-apt/keys/cli-apt.gpg sudo tee /etc/apt/sources.list.d/unikraft-cli.sources < On Windows, the installer puts `unikraft.exe` into `%LOCALAPPDATA%\Programs\Unikraft\bin` and adds that directory to your user `PATH`. Set `UNIKRAFT_CLI_INSTALL_BIN_DIR` to install somewhere else, or download the Windows zip from the [releases page](https://github.com/unikraft-cloud/cli/releases). `unikraft upgrade` keeps the Windows build current like any other. A few limitations apply on Windows: - `unikraft build` needs a BuildKit that builds Linux images, either Docker Desktop's or one you name with `BUILDKIT_HOST`. See [debugging the build and push steps](/platform/troubleshooting#debugging-the-build-and-push-steps). - There's no Scoop or winget package yet. See [alternative installation instructions](https://github.com/unikraft-cloud/cli/?tab=readme-ov-file#installation) for other platforms. ## Quick start ```sh # Login to Unikraft Cloud unikraft login # Deploy an instance unikraft run --metro fra -p 443:8080/http+tls --image nginx:latest # List instances unikraft instances list ``` [View unikraft CLI documentation](/docs/cli/unikraft) {/* vale off */} :::warning The `kraft cloud` CLI is deprecated. To move your scripts to the `unikraft` CLI, see the [migration guide](/docs/tutorials/kraftkit-to-unikraft). ::: {/* vale on */} --- ## Document: Fields Display, sort, filter, and edit resource fields in the unikraft CLI URL: /docs/cli/fields # Fields Every resource in the `unikraft` CLI (instances, images, services, volumes, and more) has a set of **fields**. Fields are the named pieces of data the platform stores about a resource, such as its `name`, `state`, `image`, or memory `resources`. The same fields power table output, sorting, filtering, and editing, so once you learn a resource's field names you can use them everywhere. This page covers what fields are, how to control which fields appear, how to sort and filter on them, and how to edit them with `edit`. ## The field model To see all fields in the model, run `unikraft instances list --help` or `unikraft instances get --help`. To view instances' values for all these fields, use `unikraft instances list -fall` or `unikraft instances get -fall`. To view the default set of fields and their current values for an instance, inspect it with `get`. The output is a tree of fields, where some fields hold scalars and others hold nested objects or lists. ```ansi $ unikraft instances get httpserver-rust175-tokio metro: fra name: httpserver-rust175-tokio uuid: 78ad8c05-ed7a-4886-abd3-07613dce7c28 state: standby image: /httpserver-rust175-tokio resources: memory: 256MiB vcpus: 1 service: name: nameless-silence-donj1yl2 uuid: cd2910d2-b709-410c-9e04-f5539a7d020a domains: - fqdn: nameless-silence-donj1yl2.fra.unikraft.app networks: - uuid: d8700d21-63c0-448a-806d-54014584b082 private-ip: 10.0.0.25 mac: 12:b0:0a:00:00:19 timestamps: created: 1 day ago scale-to-zero: enabled: true policy: on cooldown-time: 1s stop: reason: platform stop exit-code: 1 ``` ### Field paths Use dot-separated paths to address nested fields. A path walks down the tree, one field name per segment. ```bash name # top-level field resources.memory # nested field service.name # nested field timestamps.created # nested field networks.0.private-ip # array element by zero-based index ``` The `--field`, `--sort`, `--filter` flags, and the `edit` command, all use these same paths (described below). ## Choosing which fields to display Listing resources prints a default set of columns. Use `--field` (short form `-f`) to select which fields to display. When you pass a parent field, the CLI expands it into a column for each of its child fields. ```ansi $ unikraft instances list --field service NAME UUID FQDN falling-sky-k20dl7a7 32b81f2f-8767-4961-a55f-520e7462395b falling-sky-k20dl7a7.fra.unikraft.app nameless-silence-donj1yl2 cd2910d2-b709-410c-9e04-f5539a7d020a nameless-silence-donj1yl2.fra.unikraft.app ``` For a single instance, `get` with `--field` shows the same nested structure: ```ansi $ unikraft instances get httpserver-rust175-tokio --field service service: name: nameless-silence-donj1yl2 uuid: cd2910d2-b709-410c-9e04-f5539a7d020a domains: - fqdn: nameless-silence-donj1yl2.fra.unikraft.app certificate: name: uuid: ``` Pass a specific field path to display only that value. ```ansi $ unikraft instances list --field service.name NAME falling-sky-k20dl7a7 nameless-silence-donj1yl2 ``` ```ansi $ unikraft instances get httpserver-rust175-tokio --field service.name service: name: nameless-silence-donj1yl2 ``` You can pass fields as a comma-separated list, or repeat `--field` several times. ```ansi $ unikraft instances list --field service.name,resources.memory NAME MEMORY falling-sky-k20dl7a7 256MiB nameless-silence-donj1yl2 256MiB ``` ```ansi $ unikraft instances get httpserver-rust175-tokio --field service.name,resources.memory service: name: nameless-silence-donj1yl2 resources: memory: 256MiB ``` ### Adding and removing from the default field set Prefix a field name with `+` to add it to the default set, or `-` to remove it. This lets you fine-tune the output without listing every field you want. ```ansi $ unikraft instances get httpserver-rust175-tokio --field +timing,-networks metro: fra name: httpserver-rust175-tokio uuid: 78ad8c05-ed7a-4886-abd3-07613dce7c28 state: standby image: /httpserver-rust175-tokio resources: memory: 256MiB vcpus: 1 service: name: nameless-silence-donj1yl2 uuid: cd2910d2-b709-410c-9e04-f5539a7d020a domains: - fqdn: nameless-silence-donj1yl2.fra.unikraft.app timestamps: created: 1 day ago scale-to-zero: enabled: true policy: on cooldown-time: 1s stop: reason: platform stop exit-code: 1 timing: uptime: 4m22.163s boot-time: 78.247ms net-time: 85.486ms ``` The `networks` field is absent because `-networks` removed it, and `timing` appears at the end because `+timing` added it. Besides `uptime`, `boot-time`, and `net-time`, `timing` holds the whole start sequence of the instance, from the start prerequisites to ready. Checkpoints and templates carry their own `timing` field. The same syntax works with `list`: ```ansi $ unikraft instances list --field +timing,-networks METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED UPTIME BOOT-TIME NET-TIME fra httpserver-rust175-tokio standby /httpserver-rust175-tokio 256MiB 1 nameless-silence-donj1yl2.fra.unikraft.app 1 day ago 4m22.163s 78.247ms 85.486ms fra nginx-fj704 running /nginx 256MiB 1 falling-sky-k20dl7a7.fra.unikraft.app 1 hour ago 12m5.002s 45.123ms 52.897ms ``` ## Sorting Use `--sort` with a field path to order the output by that field. ```ansi $ unikraft instances list --sort timestamps.created METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra httpserver-rust175-tokio standby /httpserver-rust175-tokio 256MiB 2 nameless-silence-donj1yl2.fra.unikraft.app 1 day ago fra nginx-fj704 running /nginx 256MiB 1 falling-sky-k20dl7a7.fra.unikraft.app 1 hour ago ``` ## Filtering Use `--filter` with a field path and an operator to show only the resources that match. ```ansi $ unikraft instances list --filter state==standby METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra httpserver-rust175-tokio standby /httpserver-rust175-tokio 256MiB 2 nameless-silence-donj1yl2.fra.unikraft.app 1 day ago ``` `--filter` is available on the `list` and `delete` subcommands throughout the `unikraft` CLI. The filter expression language supports nested paths, wildcards, regular expressions, and combining expressions. See the [filter expression reference](#filter-expression-reference) for the full grammar, operators, and examples. ### The `--until` flag The `--until` flag uses the same expression syntax as `--filter`. `--until` is available on the `wait` subcommand. The command polls until every targeted resource matches the expression. ```bash unikraft instances wait my-instance --until state==running ``` ## Editing fields The `edit` command modifies a resource's fields in place, and the `create` command uses the same flags when creating a new resource. `edit` prints a diff of the change, where `+` lines are additions and `-`/`+` pairs show a replaced value. ### Setting a field with `--set` Use `--set =` to set or replace a field. Setting a field that doesn't exist yet creates it, including any parent fields. Many common fields also have dedicated shorthand flags—for example, `--memory` instead of `--set resources.memory=`. These are shortcuts for `--set` and are a bit easier to remember and type. You don't need to use field-based editing for fields that have a dedicated flag. ```ansi $ unikraft instances edit httpserver-rust175-tokio --set runtime.env=MYSECRET=secret metro: fra name: httpserver-rust175-tokio uuid: 78ad8c05-ed7a-4886-abd3-07613dce7c28 state: standby image: /httpserver-rust175-tokio + runtime: + env: + MYSECRET: * resources: memory: 256MiB vcpus: 1  ... ``` ### Adding and removing with `--add` and `--del` Use `--add =` to append to a list or map field, and `--del ` to remove an entry from one. For example, remove an environment variable: ```ansi $ unikraft instances edit httpserver-rust175-tokio --del runtime.env=MYSECRET metro: fra name: httpserver-rust175-tokio uuid: 78ad8c05-ed7a-4886-abd3-07613dce7c28 state: standby image: /httpserver-rust175-tokio - runtime: - env: - MYSECRET: * ``` The `--add` flag works the same way, appending the given value instead of removing it. ```ansi $ unikraft instances edit httpserver-rust175-tokio --add runtime.env=MYSECRET=secret metro: fra name: httpserver-rust175-tokio uuid: 78ad8c05-ed7a-4886-abd3-07613dce7c28 state: standby image: /httpserver-rust175-tokio + runtime: + env: + MYSECRET: * resources: memory: 256MiB vcpus: 1  ... ``` You can combine `--set`, `--add`, and `--del` flags in a single `edit` invocation. ### List and map fields Each flag carries exactly one element, so a field holding a list or a map takes one flag per entry. The CLI never splits a value on commas—a comma inside a value is part of that value. ```bash unikraft instances edit httpserver-rust175-tokio \ --set tags=production \ --set tags=customer_A ``` To pass a whole list or map in a single flag, write it as JSON instead. ```bash unikraft instances edit httpserver-rust175-tokio --set tags='["production","customer_A"]' ``` ## Visual editing Pass `--visual` to open the resource in your editor and edit its fields interactively. The CLI launches the program named by the `$VISUAL` or `$EDITOR` environment variable. ```bash export EDITOR=vim unikraft instances edit httpserver-rust175-tokio --visual ``` If neither variable has a value, the command fails with `no editor set: please set $VISUAL or $EDITOR`. When you save and close the editor, the CLI applies your changes and prints the resulting diff. ```ansi $ unikraft instances edit httpserver-rust175-tokio --visual metro: fra name: httpserver-rust175-tokio uuid: 78ad8c05-ed7a-4886-abd3-07613dce7c28 state: standby image: demo/httpserver-rust175-tokio runtime: env: MYSECRET: * resources: memory: 256MiB - vcpus: 1 + vcpus: 2  ... ``` ## Dry-run Pass `--dry-run` to any `edit` or `create` command to print the diff of what *would* change without actually applying it. This is useful for previewing changes before committing them, and pairs especially well with `--visual`. ```ansi $ unikraft instances edit httpserver-rust175-tokio --set resources.vcpus=2 --dry-run resources.vcpus := 2 ``` ## Filter expression reference The `--filter` and `--until` flags accept filter expressions that match resources based on their field values. The syntax extends the [containerd filter grammar](https://github.com/containerd/containerd/blob/main/pkg/filters/filter.go) with negated regex matching, wildcards, and alternative quoting. ### Grammar ``` expression := selector ( "," selector )* selector := fieldpath [ operator value ] | fieldpath? "*" ( "." selector | operator value )? fieldpath := field ( "." field )* field := quoted-string | identifier | index identifier := [A-Za-z] [A-Za-z0-9_-]* index := [0-9]+ operator := "=" | "==" | "!=" | "!==" | "~=" | "!~=" | ">" | "<" | ">=" | "<=" value := quoted-string | regex-string | unquoted-string ``` A **quoted-string** uses Go string syntax (double quotes with standard backslash escapes: `\n`, `\t`, `\\`, `\"`, `\xNN`, `\uNNNN`, `\UNNNNNNNN`, octal `\NNN`). A **regex-string** may only appear after `~=` or `!~=`. You can delimit it with `/` or `|` instead of `"`, which is convenient when the pattern contains double-quotes or backslashes (for example, `name~=/[abc]{0,2}/` or `path~=|foo/bar|`). An **unquoted-string** is any run of non-whitespace, non-comma characters. ### Operators | Operator | Alias | Description | Example | | -------- | ----- | --------------------------------- | -------------------- | | `==` | `=` | Equals | `state==running` | | `!=` | `!==` | Not equals | `state!=stopped` | | `~=` | | Matches regular expression | `name~="^web-.*"` | | `!~=` | | Doesn't match regular expression | `name!~="^test-"` | | `>` | | Greater than | `resources.vcpus>2` | | `<` | | Less than | `resources.vcpus<4` | | `>=` | | Greater than or equal | `resources.vcpus>=2` | | `<=` | | Less than or equal | `resources.vcpus<=4` | | _(none)_ | | Field is present and non-empty | `description` | Regular expressions use Go `regexp` syntax. ### Comparisons `>`, `<`, `>=`, and `<=` order a field's value against the given value. They work on numeric fields (`resources.vcpus>2`) and on timestamp fields (`timestamps.created<2024-01-01T00:00:00Z`), but not on strings or booleans—those only support `==` and `!=`. ```bash # Instances with more than 2 vCPUs unikraft instances list --filter 'resources.vcpus>2' # Instances with less than 512MiB of memory unikraft instances list --filter 'resources.memory<512MiB' ``` #### Absolute time Timestamp fields accept most common formats for an absolute point in time, so you don't need to look up or match one specific layout. For example: - RFC3339, with or without fractional seconds: `2024-01-01T00:00:00Z` - A date and time without a zone offset (interpreted as UTC): `2024-01-01T00:00:00` or `"2024-01-01 00:00:00"` (quote values containing a space) - A bare date (midnight UTC): `2024-01-01` - A Unix timestamp: `1704067200` ```bash # Instances created before 2024 unikraft instances list --filter 'timestamps.created<2024-01-01' ``` #### Relative time Timestamp fields also accept a relative duration on the right-hand side of `>`, `<`, `>=`, or `<=`, which is compared against the field's age (how long ago it occurred), rather than the timestamp itself. So `timestamps.created<7d` matches resources created less than 7 days ago, and `timestamps.created>30d` matches resources older than 30 days. A duration is a sequence of numbers with a unit, and units can be combined (for example `1d12h`): | Unit | Meaning | | ---- | ------- | | `s` | seconds | | `m` | minutes | | `h` | hours | | `d` | days | | `w` | weeks | | `y` | years | ```bash # Instances created in the last 7 days unikraft instances list --filter 'timestamps.created<7d' # Instances older than 30 days unikraft instances list --filter 'timestamps.created>30d' # Instances created between 1 and 2 weeks ago unikraft instances list --filter 'timestamps.created>1w,timestamps.created<2w' ``` ### Wildcards A `*` in a field path iterates over all entries of a map or array field. The wildcard can appear at any level, and you can chain wildcards. ```bash labels.* # true if any label entry exists labels.*==value # true if any label entry equals "value" authors.*.email==a@b.com # true if any author's email matches nested.*.*==true # double wildcard ``` **Negation semantics with wildcards:** positive operators (`==`, `~=`) match if **any** entry meets the condition. Negated operators (`!=`, `!~=`) match only if **all** entries meet the condition (that is, no entry matches the negated pattern). An empty map or array has no entries, so a wildcard selector, negated or not, doesn't match it: `tags.*!=temporary` excludes instances without tags. ### Indexed access You can also access array elements by zero-based numeric index. ```bash authors.0.name==Alice # first author's name authors.1.email==b@b.com # second author's email ``` ### Combining expressions #### And (comma-separated within one flag) Comma-separated selectors within a single `--filter` value must **all** match for the filter to include a resource. ```bash --filter 'state==running,name~="^web"' ``` #### Or (repeated flags) When you specify `--filter` several times, the filter includes a resource if it matches **any** of the expressions. ```bash --filter 'state==running' --filter 'state==stopped' ``` #### Combining both operators The two mechanisms compose: ```bash --filter 'state==running,metro==fra' --filter 'state==stopped,metro==was' ``` This matches resources that are (running in fra) **or** (stopped in was). ### Special fields The `metro` field receives special handling: when present in a filter, it restricts which metros the CLI queries rather than filtering results after the fact. This means `--filter metro==fra` avoids fetching data from other metros entirely. ### Examples ```bash # List only running instances unikraft instances list --filter state==running # List instances NOT in the "stopped" state unikraft instances list --filter 'state!=stopped' # List images whose ref contains "nginx" unikraft image list --filter 'ref~="/nginx"' # Exclude images matching a pattern unikraft image list --filter 'ref!~="test"' # Regex with slash delimiters (avoids escaping issues) unikraft image list --filter 'ref~=/nginx[0-9]{1,3}/' # Wait for an instance to reach the running state unikraft instances wait my-instance --until state==running # Delete all stopped instances (with confirmation) unikraft instances delete --filter state==stopped # Combine filters: running instances in fra, OR stopped in was unikraft instances list --filter 'state==running,metro==fra' \ --filter 'state==stopped,metro==was' # Filter on nested fields unikraft instances list --filter 'scale-to-zero.enabled==true' # Filter using array wildcard unikraft instances list --filter 'volumes.*.name==data' # Check that no tag equals a value (all must satisfy !=; instances without tags don't match) # `tags` isn't in the default table, so add the column with --field +tags unikraft instances list --field +tags --filter 'tags.*!=temporary' # Instances with more than 2 vCPUs unikraft instances list --filter 'resources.vcpus>2' # Instances created in the last 7 days unikraft instances list --filter 'timestamps.created<7d' ``` --- ## Document: Vertical scaling URL: /docs/features/vertical-scaling # Vertical scaling > **Availability** > > Available on dedicated hosts, BYOC and on-prem. Coming soon on Unikraft Cloud. Status: Preview. > > Exposed by the API. Not exposed by the CLI and the SDKs yet. > > - Unikraft Cloud: Vertical scaling is coming soon to Unikraft Cloud. Without it, an instance with a range boots and runs at its initial size. [Contact support to try it](mailto:support@unikraft.com) > - CLI: The CLI and the SDKs do not expose the range fields yet. Use the API, for example through the unikraft api command. [unikraft api](https://unikraft.com/docs/cli/unikraft/api) > > See [Hosting options](https://unikraft.com/docs/platform/hosting-options). Vertical scaling lets an instance grow and shrink its memory and its number of vCPUs while it runs. Where [autoscale](/features/autoscale) changes the *number of instances* behind a service, vertical scaling changes the *size of a single instance*. The platform watches the instance's memory utilization and CPU pressure and adjusts its resources within a range you define. You don't have to size the instance for its peak load up front. ## Setting a range An instance opts in with a range at creation time. The guest kernel must support vertical scaling, or the instance won't start (see [Requirements and limitations](#requirements-and-limitations)). In the [create request](/api/platform/v1/instances#create-instance), set `min_memory_mb` and `max_memory_mb` for memory, or `min_vcpus` and `max_vcpus` for vCPUs. Vertical scaling is active only for a resource with at least one bound. An instance without a range keeps a fixed size. {/* vale off */} :::caution The `unikraft` CLI and the SDKs don't expose the range fields yet. In the meantime, set them through the [API](/api/platform/v1/instances#create-instance), which you can invoke with `curl` or the [`unikraft api`](/cli/unikraft/api) command. ::: {/* vale on */} ```bash title="POST /instances" unikraft api /v1/instances --metro=fra \ '{ "name": "my-instance", "image": "nginx:latest", "memory_mb": 512, "min_memory_mb": 256, "max_memory_mb": 2048, "vcpus": 1, "min_vcpus": 1, "max_vcpus": 2, "autostart": true }' ``` The instance boots with the minimum of each resource. If you also set `memory_mb` or `vcpus`, the platform grows the instance to that amount right after boot, so the value must lie inside the range. The maximum must exceed the minimum by at least one guest memory block size. The platform rounds the maximum down to the nearest guest memory block size (128M by default). If you set only one bound of a resource, the platform fills in the other: | You set | Minimum | Maximum | |---------|---------|---------| | Only the minimum | The value you set | Your per-instance limit, `limits.max_memory_mb` or `limits.max_vcpus` in your [quotas](/platform/quotas) | | Only the maximum | `memory_mb` or `vcpus`, or the platform default when unset | The value you set | ## How the platform scales an instance ### Memory The platform samples the memory utilization of each scaling instance at a fixed interval and adjusts the memory in fixed-size blocks: | Condition | Action | |-----------|--------| | Utilization stays at or above the scale-up threshold, averaged over recent samples | The platform adds memory until utilization drops a margin below the threshold. | | Utilization reaches the urgent threshold | The platform adds memory at once, based on the current utilization rather than the average, and the guest pauses its user space until the memory arrives. | | Utilization stays below the scale-down threshold | The platform removes memory until utilization rises a margin above the threshold, and no sooner than a cooldown after the last addition. | The memory always stays within the instance's range. ### vCPUs The guest itself decides when to scale its vCPUs. It requests another vCPU when its processes spend a notable share of a short window waiting for CPU time. It checks its load average at a longer interval and gives vCPUs back when the load is low, starting no sooner than a cooldown after the last addition. The thresholds, intervals, and cooldowns for both memory and vCPU scaling are configurable by the platform operator. ## Quotas Added memory and vCPUs count against your account's live limits, `hard.live_memory_mb` and `hard.live_vcpus` in your [quotas](/platform/quotas). When that budget runs out, the instance doesn't grow further. Once the instance reaches its maximum memory or the budget runs out, the guest's out-of-memory handling takes over, as on an instance of fixed size. The range itself must fit the per-instance limits in your quotas: `limits.min_memory_mb` to `limits.max_memory_mb` for memory, and `limits.min_vcpus` to `limits.max_vcpus` for vCPUs. ## Inspecting a scaling instance The instance status reports the current size in `memory_mb` and `vcpus`. For each resource with a range, it also reports the size the instance booted with in `boot_memory_mb` and `boot_vcpus`, and the range in `min_memory_mb`, `max_memory_mb`, `min_vcpus`, and `max_vcpus`. ```bash unikraft instance get my-instance ``` ## Requirements and limitations - Only the default `micro` [instance type](/platform/instances#instance-types) supports vertical scaling, so a request that sets a range for a full VM fails. - The guest kernel must support vertical scaling. The default guest kernel configuration and the platform's guest kernel support it. If the image lacks that support for a resource that has a range (for vCPUs, this includes ACPI), the create or start request fails with `the image does not support vertical scaling of memory` (or `of vCPUs`), and the stop cause is `no-mem-vscaling` or `no-cpu-vscaling`. - You can't change the range after you create the instance. - An instance created from an instance template that has a snapshot can't set its own range, because it inherits the range of its template. - Instance templates and [autoscale](/features/autoscale) templates accept the same fields and pass the range on to their clones, so for [on-demand templates](/features/on-demand-templates) you set the range inside `create_args`. - An instance that resumes from a snapshot, for example after [scale-to-zero](/features/scale-to-zero), keeps its current size. ## Learn more * Unikraft Cloud's [REST API reference](/api/platform/v1), in particular the [create instance endpoint](/api/platform/v1/instances#create-instance). * [Quotas](/platform/quotas), for the limits a range has to fit in. --- ## Document: Snapshots URL: /docs/features/snapshots # Snapshots > **Availability** > > Available on Unikraft Cloud, dedicated hosts, BYOC and on-prem. > > Exposed by the API, the CLI and the SDKs. > > See [Hosting options](https://unikraft.com/docs/platform/hosting-options). Unikraft Cloud supports the ability to deploy **stateful** apps. These apps can keep state across different [scale-to-zero](/features/scale-to-zero) incarnations. This mechanism also reduces app init time. Heavy-weight apps take a while to get started when cold started (for example, Spring Boot, Ruby on Rails, etc.). You can reduce this initialization time by taking a snapshot of the app memory and restoring it when the app starts again. ## Setting it up The quickest way to set this up is when creating an instance, via the CLI: ```bash unikraft run --metro fra \ -p 443:8080/http+tls \ --scale-to-zero policy=on,stateful=true \ ... ``` ## How it works When you enable stateful mode and first deploy your app, Unikraft Cloud brings it up normally and lets it fully initialize. This may be fast or take seconds or minutes, depending on your app's init time. Unikraft Cloud waits until the app's exposed port becomes available, and then starts the scale-to-zero process. This assumes no traffic is coming to that port. At this point, Unikraft Cloud saves the state of your app and sets the app to `standby` (consuming no resources). Next, when traffic arrives, Unikraft Cloud brings up the app including its saved state. This ensures statefulness across scale-to-zero and scale-to-one cycles. It also eliminates long initialization times from heavyweight apps. ## Learn more * The [CLI reference](/docs/cli/unikraft). * Unikraft Cloud's [REST API reference](/api/platform/v1). * [Instances](/platform/instances) documentation. * [Service groups](/platform/services) documentation. --- ## Document: Scale-to-Zero URL: /docs/features/scale-to-zero # Scale-to-Zero > **Availability** > > Available on Unikraft Cloud, dedicated hosts, BYOC and on-prem. > > Exposed by the API, the CLI and the SDKs. > > See [Hosting options](https://unikraft.com/docs/platform/hosting-options). With conventional cloud platforms you need to keep at least one instance running always to be able to respond to incoming requests. Performing a just-in-time cold boot is too time-consuming and would create a response latency of many seconds or worse. This isn't the case with Unikraft Cloud. Unikraft Cloud is lightweight and based on Unikraft technology. Instances on Unikraft Cloud can cold boot within milliseconds, while providing the same strong, hardware-level isolation afforded by virtual machines. Millisecond cold boots allow performing low-latency scale-to-zero. As long as no traffic is flowing through your instance, it consumes **no** resources. When the next connection arrives, Unikraft Cloud takes care of transparently cold booting (is it cold booting if it's milliseconds?) your instance and replying. All within a negligible amount of time compared to Internet round-trip times, and so unbeknownst to your end users. By default, Unikraft Cloud reduces network and cloud stack cold start time to a minimum. Some apps take a while to initialize (for example, Spring Boot, Puppeteer, etc). If you need to deploy such an app and still want millisecond cold starts, Unikraft Cloud offers a [*stateful*](/features/scale-to-zero#stateful-scale-to-zero) feature. :::tip If you add instances to a [service](/platform/services), the service will load balance traffic across them and Unikraft Cloud will wake up each instance as needed. This differs from [autoscale](/features/autoscale), in which you *don't* specify the number of instances. The platform does this for you based on traffic load. ::: ## Policies With the scale-to-zero policy you define under which circumstances Unikraft Cloud should put your instance into standby. Unikraft Cloud currently supports the following scale-to-zero policies: | Policy | Description | |--------|-------------| | `off` | Disables scale-to-zero. The instance keeps on running until manually stopped. | | `on` | Enables scale-to-zero. When there are no TCP connections or HTTP requests during the cooldown time, the instance goes into standby. | | `idle` | Same as `on`, but also puts the instance into standby when there are TCP connections established that have been inactive during the cooldown time. The connections remain established and incoming packets wake up the instance. Scale-to-zero doesn't happen while there are active HTTP requests (that is, traffic on ports which use the `http` [handler](/platform/services#handlers)). | :::info Unikraft Cloud supports scale-to-zero for traffic going through the proxy, as well as [internal traffic](/platform/networking#internal-communication) via Private FQDNs. ::: ## Application support for scale-to-zero Most apps can use scale-to-zero without any special support. Cases exist where making your app aware of scale-to-zero makes sense. - **Background Jobs**: For example, you want to run background jobs after your app has responded to a request (for example, send trace information to a logging server). In this case, you may want to temporarily disable scale-to-zero to ensure your instance isn't put to sleep while still performing work. - **Long Request Processing**: The same is true if your app can have long request processing times. Consider a setup where you use the `idle` policy with plain TCP connections and configure a cooldown time of 10s. Sometimes if it takes your app 15s to process a request until the first response data gets sent, Unikraft Cloud will prematurely scale your instance to zero[^1]. Configuring a longer cooldown time can be a simple solution sometimes. But this isn't possible if the max duration of background jobs or request processing phases is unknown. It also means that you have to compromise between cost efficiency and reliability of your service. Unikraft Cloud allows your app to temporarily disable scale-to-zero. You can have both a short cooldown phase and reliable operation no matter how long things may take. To control scale-to-zero from within your app, instances on Unikraft Cloud provide a special file-based interface: | Path | Description | |------|-------------| |`/uk/libukp/scale_to_zero_disable` | Allows to temporarily disable scale-to-zero. | The `scale_to_zero_disable` pseudo file keeps track of the count of concurrent disable requests. If the count reaches `0`, scale-to-zero remains active[^2]. Any number larger than `0` means scale-to-zero is temporarily inactive. Using a count instead of a boolean value is helpful when you have many independent workers. Your app workers can disable scale-to-zero individually by incrementing and decrementing the count without having to synchronize. Reading the file returns the current count. The system prefixes the value with an equals sign (that is, `=X` with `X` being the current count). Writing to the file modifies the count. You can use the following strings: | String | Description | |------|---------------| | `+` | Increment the current count by one. | | `-` | Decrement the current count by one. | | `=X` | Set the count to `X`. | | `+X` | Increment the current count by `X`. | | `-X` | Decrement the current count by `X`. | Any attempt to write an invalid string to the file returns an `EINVAL` error. Any attempt to set a count less than `0` or larger than `2^64` returns an `ERANGE` error. :::caution Writing to the file to disable scale-to-zero doesn't atomically disable scale-to-zero. There can be a short delay (a few milliseconds) until the Unikraft Cloud controller receives the changed value and makes scale-to-zero decisions accordingly. Ensure you configure a cooldown time large enough to accommodate for potential signal delay. ::: ## Notification signal Before the platform suspends an instance, it can send a notification signal to the guest, giving it time to finish in-flight work before suspension begins. The platform delivers the notification over [vsock](https://man7.org/linux/man-pages/man7/vsock.7.html) on port `138`. It sends the literal string `scaletozero` to the guest at the configured lead time before suspension. Your app can listen on vsock port `138` and use the signal to: - Drain in-flight requests - Flush write buffers - Update any external state ### Configuring the notification lead time When creating or updating an instance, you can specify the number of milliseconds before the scaletozero event when the platform should send the notification signal: ```bash title="unikraft" unikraft run --metro fra \ -p 443:8080/http+tls \ --scale-to-zero policy=on,cooldown-time=2000,notify-time=1000 \ ... ``` ```bash title="API" curl -X POST \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Content-Type: application/json" \ "https://api.fra.unikraft.cloud/v1/instances" \ -d '{ "service_group": { "services": [ { "port": 443, "destination_port": 8080, "handlers": [ "tls", "http" ] } ] }, "scale_to_zero": { "policy": "on", "cooldown_time_ms": 2000, "notify_time_ms": 1000 }, ... }' ``` This sends the notification 1 second before the cooldown expires and suspension begins. :::note `notify_time_ms` must be less than `cooldown_time_ms`. If `notify_time_ms` is `0` (the default), the platform skips the notification and suspends the instance immediately when the cooldown expires, without informing the guest. ::: ## Stateful scale-to-zero Some apps have a long cold boot phase or experience large first response latency. For example, to run just-in-time compilation and fill caches. Using stateful scale-to-zero can dramatically reduce the response time of your service. With stateful scale-to-zero Unikraft Cloud takes a [snapshot](/features/snapshots) of the instance state before putting it into standby. When incoming network traffic triggers a wake-up, Unikraft Cloud loads the snapshot in milliseconds. The instance resumes execution where it left off—with caches already warm. Stateful scale-to-zero can also enable scale-to-zero for apps that need to keep state for functional correctness. This works even if cold boot times are no concern. If your app doesn't require maintaining state and has a satisfactorily short boot time, you might prefer to disable stateful scale-to-zero to save disk space. :::info The time to load an instance snapshot is constant and in the order of a few milliseconds. This is what the boot time metrics report. The system loads the actual memory contents from the snapshot only at first access. This reduces response latency by loading only what's necessary to process the request. This means that the first few requests might take longer until all required memory comes back. The time to take an instance snapshot during scale-to-zero depends on the amount of memory touched by the app since the last start. ::: ## Best practices ### Cooldown configuration Choosing the optimal cooldown period depends on your app's start-up time and traffic patterns. The default cooldown is 1 second, which works well for simple apps. For larger apps with longer boot times, start with a conservative cooldown (for example, 5-10 seconds) and gradually lower it while monitoring behavior. If your app takes a long time to initialize, you might encounter issues where it scales down before serving the first request if the cooldown is too short. Strategies to mitigate this include: 1. **Stateful Scale-to-Zero**: [Enabling stateful mode](#stateful-scale-to-zero) saves memory to disk, reducing wake-up time for complex apps. 2. **Startup Protection**: Use the [manual disable mechanism](#application-support-for-scale-to-zero) to temporarily turn off scale-to-zero during your app's boot sequence, then re-enable it once ready to serve. ### Continuous connections Apps using long-lived connections with frequent keep-alive messages (for example, WebSockets, HTTP/2, HMR) may prevent the instance from scaling to zero. To allow the instance to sleep: * Increase the interval between keep-alive messages. * Use a more aggressive cooldown period if appropriate. * If requiring continuous connectivity, consider disabling scale-to-zero for these workloads. ## Example You can enable millisecond scale-to-zero with the `--scale-to-zero` flag in relevant CLI subcommands. ```bash git clone https://github.com/unikraft-cloud/examples cd examples/nginx/ unikraft build . --output /nginx:latest unikraft run --metro fra \ -m 256M \ -p 443:8080/http+tls \ --scale-to-zero policy=on,cooldown-time=1000 \ --image /nginx:latest ``` This command will create the NGINX instance with scale-to-zero enabled, with a cooldown time of 1 second: ```ansi metro: fra name: nginx-67zbu uuid: 8a8bc1b9-0af6-420e-a426-190dc2da9eaa state: starting image: /nginx resources:  memory: 256MiB  vcpus: 1 service:  uuid: a942b9b5-ad17-3ffe-dcd2-ef4331f9087a  name: nameless-fog-0tvh1uov  domains:  - fqdn: nameless-fog-0tvh1uov.fra.unikraft.app networks: - uuid: 62d9bbf0-aec8-61f6-7bdb-86edf63dd068  private-ip: 10.0.3.3  mac: 12:b0:c6:23:ed:15 timestamps:  created: just now scale-to-zero:  enabled: true  policy: on  cooldown-time: 1s ``` Note that at first the system starts the instance as usual. Check the instance's status: ```bash unikraft instances list ``` You should see output like: ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra nginx-67zbu standby /nginx 256MiB 1 nameless-fog-0tvh1uov.fra.unikraft.app 2 minutes ago ``` Notice the state is now set to `standby`? At first, `unikraft run` sets the state to `running`. Unikraft Cloud then puts the instance to sleep (it stops it, but keeps state to start it again when needed). You can also check that scale-to-zero gets enabled by grabbing the instance details ```bash unikraft instance get nginx-67zbu ``` which outputs: ```ansi metro: fra name: nginx-67zbu uuid: 8a8bc1b9-0af6-420e-a426-190dc2da9eaa state: standby image: /nginx resources:  memory: 256MiB  vcpus: 1 service:  uuid: a942b9b5-ad17-3ffe-dcd2-ef4331f9087a  name: nameless-fog-0tvh1uov  domains:  - fqdn: nameless-fog-0tvh1uov.fra.unikraft.app networks: - uuid: 62d9bbf0-aec8-61f6-7bdb-86edf63dd068  private-ip: 10.0.3.3  mac: 12:b0:c6:23:ed:15 timestamps:  created: just now scale-to-zero:  enabled: true  policy: on  cooldown-time: 1s stop:  reason: platform stop  exit-code: 0 ``` Try using `curl` or your browser to see scale-to-zero (well, *scale-to-one* in this case!) in action: ```bash curl https://nameless-fog-0tvh1uov.fra.unikraft.app ``` You should get an NGINX response with no noticeable delay. For fun, try to use the following command to see if you can catch the instance's `STATE` field changing from `standby` to `running`: ```bash unikraft instances list --watch=0.5s ``` If you `curl` enough, you should see the `STATE` turn to a green `running` from time to time. :::note You can use scale-to-zero *in conjunction with* [autoscale](/features/autoscale). This ensures that as you scale back down, if traffic dies, your last instance gets removed. You're not charged for the service in this state. ::: ## Learn more * The [CLI reference](/docs/cli/unikraft). * Unikraft Cloud's [REST API reference](/api/platform/v1). * Instance [snapshots](/features/snapshots). * Service [autoscaling](/features/autoscale). [^1]: This is never the case for ports of your service that have the `http` [handler](/platform/services#handlers) set. [^2]: If it's actually enabled, depends on the instance configuration. --- ## Document: ROMs URL: /docs/features/roms # ROMs > **Availability** > > Available on Unikraft Cloud, dedicated hosts, BYOC and on-prem. > > Exposed by the API, the CLI and the SDKs. > > See [Hosting options](https://unikraft.com/docs/platform/hosting-options). import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" 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.). 1. **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. :::tip 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_`, where `` 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](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash 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 Kraftfile server.py wrapper.sh ```dockerfile title="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 ``` ```yaml title="Kraftfile" spec: v0.7 runtime: base-compat:latest targets: - kraftcloud/x86_64 rootfs: source: ./Dockerfile format: erofs cmd: [ "/usr/bin/python3", "/src/server.py" ] ``` ```python title="server.py" import argparse import importlib.util import sys from http.server import HTTPServer, BaseHTTPRequestHandler rom_module = None def load_rom_module(): global rom_module module_name = 'rom' module_path = '/rom/rom.py' spec = importlib.util.spec_from_file_location(module_name, module_path) rom_module = importlib.util.module_from_spec(spec) sys.modules[module_name] = rom_module spec.loader.exec_module(rom_module) class MyServer(BaseHTTPRequestHandler): global rom_module def do_GET(self): self.send_response(200) self.send_header("Content-type", "text/html") self.end_headers() msg = rom_module.function() self.wfile.write(bytes(msg, "utf-8")) def main(args): load_rom_module() server = HTTPServer((args.host, args.port), MyServer) print("starting server at %s:%s" % (args.host, args.port)) try: server.serve_forever() except KeyboardInterrupt: pass print("server stopped") def parse_args(): parser = argparse.ArgumentParser() parser.add_argument("--host", type=str, default="0.0.0.0") parser.add_argument("--port", type=int, default=8080) return parser.parse_args() if __name__ == "__main__": main(parse_args()) ``` ```bash title="wrapper.sh" #!/usr/bin/sh # Unused in this example, but left here for reference on how to mount ROMs manually if not using automounting. /usr/bin/mkdir -p /rom/ /usr/bin/mount -t erofs /dev/ukp_rom_python_function /rom/ exec "$@" ``` :::tip 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`. ::: :::note 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](/platform/images#architectures) for more detail. ::: Package and push the base image: ```bash unikraft build . --output /http-python:latest ``` Wait a few seconds for propagation and check that the image is present: ```bash 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: ```bash title="" mkdir -p rom1/fs rom2/fs ``` ```python title="rom1/fs/rom.py" def function(): return "Hi from 1st ROM!\n" ``` ```yaml title="rom1/Kraftfile" spec: v0.7 targets: - kraftcloud/x86_64 roms: - ./fs ``` ```python title="rom2/fs/rom.py" def function(): return "Hi from 2nd ROM!\n" ``` ```yaml title="rom2/Kraftfile" spec: v0.7 targets: - kraftcloud/x86_64 roms: - ./fs ``` :::caution 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: ```bash unikraft build rom1/ --output /my-rom1:latest unikraft build rom2/ --output /my-rom2:latest ``` Wait a few seconds for upload and check that the ROMs are present: ```bash unikraft images list ``` ### Instances Create two instances with the same base image and attach different ROMs: ```bash title="unikraft" 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-rom1:latest,at=/rom \ --image /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-rom2:latest,at=/rom \ --image /http-python:latest ``` ```bash # Set Unikraft Cloud access token export UKC_TOKEN=token ``` ```bash title="API" curl -X POST \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Content-Type: application/json" \ "https://api.fra.unikraft.cloud/v1/instances" \ -d '[ { "name": "test-http-python-rom1", "image": "/http-python:latest", "service_group": { "services": [ { "port": 443, "destination_port": 8080, "handlers": ["tls", "http"] } ], "domains": [ { "name": "test-http-python-rom1" } ] }, "memory_mb": 512, "scale_to_zero": { "policy": "on", "stateful": false, "cooldown_time_ms": 1000 }, "autostart": true, "roms": [ { "name": "python_function", "image": "/my-rom1:latest", "at": "/rom" } ] }, { "name": "test-http-python-rom2", "image": "/http-python:latest", "service_group": { "services": [ { "port": 443, "destination_port": 8080, "handlers": ["tls", "http"] } ], "domains": [ { "name": "test-http-python-rom2" } ] }, "memory_mb": 512, "scale_to_zero": { "policy": "on", "stateful": false, "cooldown_time_ms": 1000 }, "autostart": true, "roms": [ { "name": "python_function", "image": "/my-rom2:latest", "at": "/rom" } ] } ]' ``` :::note 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](#rom-names-and-mount-points) for the rules that both fields follow. ::: Check that the instances are up: ```bash 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: ```bash curl https://test-http-python-rom1.fra.unikraft.app curl https://test-http-python-rom2.fra.unikraft.app ``` ```text Hi from 1st ROM! Hi from 2nd ROM! ``` ### Cleanup When done, remove the instances: ```bash 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](/platform/instances#instance-templates) where the snapshot locks `ENV` and `ARGS`, but you still need to inject instance-specific data. {/* vale off */} :::note An inline ROM is ephemeral: it's automatically deleted once the last reference to it is closed. ::: {/* vale on */} To use inline ROMs with `unikraft`, specify a local directory as source: ```bash 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 /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: ```bash title="POST /instances" curl -X POST \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Content-Type: application/json" \ "https://api.fra.unikraft.cloud/v1/instances" \ -d '{ "image": "/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: | Field | Description | |-------|-------------| | `path` | File path inside the ROM filesystem. The platform creates directories automatically. | | `encoding` | Either `text` (UTF-8 string) or `base64` (base64-encoded binary data). | | `data` | The 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: ```bash title="POST /instances" curl -X POST \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Content-Type: application/json" \ "https://api.fra.unikraft.cloud/v1/instances" \ -d '{ "image": "/my-base:latest", "roms": [ { "name": "app-code", "image": "/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: | Limit | Default | |-------|---------| | ROMs per instance | 8 (inline and image-based combined) | | Files per inline ROM | 16 | | Total size per inline ROM | 1 MB (across all files) | | Max API request body | 8 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_`, 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](/features/plugins#plugin-names): - 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](/features/plugins) 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](/features/plugins#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](/features/plugins#how-it-works), so a ROM can't mount there. The same rule applies to the mount points of [volumes](/platform/volumes). Two ROMs of the same instance also can't mount at the same path. | Error message | Cause | |---------------|-------| | `ROM name must be between 1 and 63 characters long` | The `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 it` | The `at` path is `/uk` or a path below it. | | `Failed to configure roms: EINVAL` or `Failed to configure plugins: EINVAL` | A ROM and a plugin share a name, or two ROMs share a mount point. | | `Failed to update roms: EINVAL` or `Failed to update plugins: EINVAL` | A `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: ```bash unikraft --help ``` Or visit the [CLI Reference](/docs/cli/unikraft). For more information on the features used in this use case and how to use them, check out the following documentation: * Unikraft Cloud's [REST API reference](/api/platform/v1), and in particular the [instances API](/api/platform/v1/instances). * [Tutorial](/tutorials/rootfs-volumes-roms) explaining the difference of storage media on Unikraft Cloud. * Running [serverless functions](/use-cases/serverless-functions) packaged as ROMs on Unikraft Cloud. * Running and customizing [game servers](/use-cases/game-servers) using ROMs on Unikraft Cloud. --- ## Document: Plugins URL: /docs/features/plugins # Plugins > **Availability** > > Available on Unikraft Cloud (enterprise plan), dedicated hosts, BYOC and on-prem. > > Exposed by the API, the CLI and the SDKs. > > - Plugins are part of enterprise plans. Contact support to enable them for your account. [support@unikraft.com](mailto:support@unikraft.com) > > See [Hosting options](https://unikraft.com/docs/platform/hosting-options). Plugins let you attach small helper programs to an instance and reach each one over a direct, authenticated HTTP endpoint. A plugin runs inside the instance next to your main app, loads from its own image, and answers requests that the Unikraft Cloud API forwards to it. Unikraft Cloud built plugins to provide a native sandbox experience. In the sandbox case, a plugin runs a small HTTP server that accepts commands to run inside the instance, exposes filesystem services, and more. The same mechanism fits any helper you want to reach over a per-instance, authenticated endpoint. ## Official plugins ### Sandbox plugin The sandbox plugin lets you run commands and manipulate the filesystem inside an instance, over the plugin endpoint: - **Commands**: start a shell command inside the instance, inspect it, read its output, feed its standard input, wait for it, and signal it. - **Filesystem**: create directories, and read, write, and upload files. Load the sandbox plugin with `--plugin` when you create, run, or [edit](#changing-plugins-on-an-existing-instance) an instance: ```bash unikraft instance run \ --metro fra \ --image official/alpine:latest \ --volume :/data:size=1GiB \ --plugin 'name=sandbox,image=plugins/sandbox:latest,config={"persist_path":"/data"}' ``` An instance with the sandbox plugin loaded accepts five `unikraft` commands: | Command | Description | |---------|-------------| | [`unikraft instance exec`](/cli/unikraft/instances/exec) | Runs a command on the instance. `--dir` sets the working directory, `--env` sets the environment, and the command's exit status becomes the CLI's. | | [`unikraft instance shell`](/cli/unikraft/instances/shell) | Opens an interactive shell. The shell runs on your machine and sends each command to the instance, so paths resolve against the instance. | | [`unikraft instance copy`](/cli/unikraft/instances/copy) | Copies a single file in either direction. Write a path on an instance as `:`, or `/:` to pick the metro. The alias is `cp`. | | [`unikraft instance write`](/cli/unikraft/instances/write) | Uploads a single local file to the instance. `--parents` creates missing directories, and `--append` adds to the remote file instead of replacing it. | | [`unikraft instance read`](/cli/unikraft/instances/read) | Downloads a single file from the instance. The local path is optional and defaults to the remote file's base name. | ```bash unikraft instance exec my-instance -- ls -la /var/log unikraft instance shell my-instance unikraft instance copy ./config.json my-instance:/etc/app.json unikraft instance write my-instance ./data.bin /var/lib/app.bin --parents unikraft instance read my-instance /var/log/app.log ``` The commands use the plugin named `sandbox` by default. Pass `--plugin-name ` to use a sandbox plugin that you loaded under a different name. If the instance has no sandbox plugin, the commands offer to attach one and restart the instance, and `--plugin-image` picks the image to attach. The offer only appears in an interactive terminal. Elsewhere, the error says how to attach the plugin. #### Shell builtins Inside `unikraft instance shell`, a line that starts with `:` runs in the CLI instead of on the instance, so you can manage the instance you're in without leaving it: ```bash $ :mount data /var/lib/app --readonly $ :restart ``` The builtins are `:get`, `:volumes`, `:edit`, `:mount`, `:unmount`, `:start`, `:stop`, `:restart`, and `:suspend`, and `:help` lists them. The same builtins work in the instance terminal of the dashboard. A few limitations apply: - The instance must run on the [`base-compat` runtime](/platform/images#dockerfile-kraftfile-and-runtime). Use an image that ships the standard Linux utilities, such as `official/alpine:latest` or `official/debian-slim:latest`, so that more commands work in the sandbox. - The shell is experimental and has no pseudo-terminal, so full-screen and interactive programs such as `vim`, `top`, `less`, or a password prompt don't work. It has no job control either, so `ctrl-z`, `bg`, and `fg` don't work. - `copy`, `read`, and `write` carry one regular file at a time, and don't carry file permissions across. - `exec` forwards standard input only when you redirect it from a file or a pipe. - The shell isn't available on Windows yet. Run it from [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) instead. ## How it works Each plugin loads from its own [ROM image](/features/roms). When the instance boots, the platform mounts every plugin at `/uk/plugins/` and starts its `init` program. The platform hands each plugin a socket to accept connections on, so every plugin has its own private channel. You reach a plugin through the instance's API endpoint: ``` https://api..unikraft.cloud/v1/instances//plugins// ``` The platform forwards everything after `/` to the plugin as the request path. A request to `.../plugins/my-plugin/files/list` reaches the plugin with the path `/files/list`. This design has a few notable properties: - **Authorized before it reaches the plugin.** By default the platform checks the request against your account and confirms your access to the target instance. A plugin can carry [a token of its own](#authorization) instead, which lets callers without a platform account in. Either method settles the request before the plugin sees it, which sets plugins apart from running an HTTP server as a regular [service](/platform/services). - **A direct line to one instance.** You talk to a single instance, so no [load balancing](/features/load-balancing) or [autoscale](/features/autoscale) sits in the path, and the instance needs no [service group](/platform/services). - **Works with scale-to-zero.** When [scale-to-zero](/features/scale-to-zero) has put the instance to sleep, the platform wakes it to serve the request and keeps it up for the duration, the same way a normal request does. ## The plugin image A plugin ships as a standard ROM image with an executable named `init` in its root. The platform loads the image, mounts it at `/uk/plugins/`, and runs `init` when the plugin starts. `init` receives two things from the platform: - **Configuration on standard input.** Whatever you pass in the plugin's `config` field arrives on `init`'s `STDIN` as JSON. A plain value such as `"config": "my-string"` counts as valid JSON, the same as `"config": 232` or `"config": { ... }`. - **A socket file descriptor.** The platform passes `init` an `--api_fd ` argument that holds the file descriptor the plugin accepts connections on. The plugin serves API traffic on that socket. ### Assigning ports The controller assigns each plugin's socket a port from a configurable range. It walks the range from the start and gives each plugin the next free port, skipping any port that the guest already uses. Operators set the range with the node-level `--vmm-plugin-port-start` (default `20000`) and `--vmm-plugin-port-end` (default `29999`) flags. ## Adding plugins when you create an instance Pass `--plugin` to [`unikraft instance create`](/cli/unikraft/instances/create) or [`unikraft instance run`](/cli/unikraft/instances/run), once for each plugin: ```bash unikraft instance create \ --name my-instance \ --metro fra \ --image my-app:latest \ --plugin 'name=my-plugin,image=user/myplugin:latest,config={"workdir":"/tmp"}' ``` The flag takes `name`, `image`, and an optional `config`, which must be valid JSON. The CLI sends these in the `plugins` field of a [`POST /instances`](/api/platform/v1/instances#create-instance) request. In the API, each entry in the `plugins` array accepts these fields. The `--plugin` column shows which of them the CLI flag can set: | Field | Required | `--plugin` | Description | |-------|----------|------------|-------------| | `name` | Yes | Yes | The plugin name. It becomes the `` segment in the plugin endpoint. See [Plugin names](#plugin-names) for the allowed format. | | `image` | Yes | Reference string only | The plugin's ROM image, given as an image reference string such as `user/myplugin:latest`, or as an image object with a `url` and optional `headers` and `pull_policy`, the same as elsewhere in the API. The [deprecated `rom` field](#the-deprecated-rom-field) still works in its place. | | `config` | No | Yes | Arbitrary JSON that the platform passes to the plugin's `init` on `STDIN`. Any JSON value works, including a string, a number, or an object. | | `authorization` | No | No | How the platform authorizes requests to this plugin, as described under [authorization](#authorization). | You can attach up to 8 plugins to an instance. ### The deprecated `rom` field The API used to call the `image` field `rom`. It still accepts `rom`, but a response to a request that uses it carries a deprecation warning: ```json title="Response" { "status": "success", ... "warnings": [ { "type": "deprecated_migration", "message": "The plugin `rom` field is deprecated. Use the `image` field instead" } ] } ``` A plugin entry takes only one of the two fields, and a request that sets both fails with `Only one of 'rom' and 'image' may be set`. The `--plugin` flag still accepts `rom=` as an alias for `image=`. ## Authorization Every request to a plugin endpoint carries a token in an `Authorization: Bearer ` header. The platform authorizes the request before it forwards anything, and strips that header on the way, so the plugin never sees the token. In the API, a plugin entry picks how the platform checks the token with an optional `authorization` object. The `--plugin` flag can't set this object, so a plugin that you load with the CLI uses the `userdb` method. | Field | Required | Description | |-------|----------|-------------| | `type` | Yes | Either `userdb` or `bearer`. | | `token` | For `bearer` | The plugin's own token, at most 256 characters, which only a `bearer` plugin may carry. | ### Platform users A plugin without an `authorization` object uses the `userdb` method. The token has to belong to a platform user with access to the instance, so the token you use for the rest of the API reaches the plugin as well. ### A token of the plugin's own With `"type": "bearer"` the platform compares the supplied token against the plugin's own token and consults no user account. Send the request with [`unikraft api`](/cli/unikraft/api), because the `--plugin` flag can't set `authorization`: ```bash unikraft api /v1/instances --metro fra \ '{ "name": "my-instance", "image": "official/alpine:latest", "plugins": [ { "name": "my-plugin", "image": "user/myplugin:latest", "authorization": { "type": "bearer", "token": "s3cr3t" } } ] }' ``` A caller then reaches the plugin with that token alone: ```bash curl -H "Authorization: Bearer s3cr3t" \ https://api.fra.unikraft.cloud/v1/instances//plugins/my-plugin/files/list ``` This suits callers with no platform account and no business holding one: a sidecar, your own control plane, or a webhook from a third-party service. The token opens that one plugin on that one instance, and nothing else of the API. The instance status in the API echoes a non-default `type` back in the plugin's `authorization` object, and never echoes the token. A plugin on the default method reports no `authorization` object at all. `unikraft instance get` doesn't show `authorization`, so read it from the API with `unikraft api /v1/instances/ --metro fra`. Two consequences follow from `bearer` consulting no user account: - Your platform token stops working on that plugin, so the plugin's token becomes the only way in. - Changing the token takes the same [PATCH path](#changing-plugins-on-an-existing-instance) as other plugin changes, so it lands the next time the instance stops. Send it with `unikraft api`, because the CLI flags can't set `authorization`. ## Plugin names A plugin name has a maximum length of 63 characters and contains only these characters: - Lowercase and uppercase letters (`a`–`z`, `A`–`Z`) - Digits (`0`–`9`) - Hyphen (`-`) and underscore (`_`) A plugin can't share its name with a [ROM](/features/roms#rom-names-and-mount-points) of the same instance. ## Changing plugins on an existing instance Use [`unikraft instance edit`](/cli/unikraft/instances/edit) to change the plugins of an existing instance. `--plugin` replaces the instance's plugin list, `--add plugins=...` appends to it, and `--del plugins=` removes one plugin: ```bash # Replace the plugin list unikraft instance edit my-instance \ --plugin name=my-plugin,image=user/myplugin:latest # Append to the plugin list unikraft instance edit my-instance \ --add plugins=name=my-other-plugin,image=user/myotherplugin:latest # Remove a plugin by name unikraft instance edit my-instance \ --del plugins=my-other-plugin ``` Each flag sends a [`PATCH /instances`](/api/platform/v1/instances) request with `prop` set to `plugins`. `--plugin` sends the `set` op, `--add` sends `add`, and `--del` sends `del`. The platform handles these requests the same way whether they come from the CLI or the API. A plugin that a `set` leaves out of the new list unloads, the same as with `del`. The platform applies the change while the instance sits in the `stopped` state. On a running instance, the platform queues the change and applies it the next time the instance stops. A restart and a scale-to-zero both count as a stop. Until then, the instance keeps its current plugins, and a removed plugin still answers requests. :::caution The `--plugin` flag doesn't carry `authorization`, so `unikraft instance edit --plugin` turns every `bearer` plugin on the instance back into a `userdb` plugin. `--add` leaves the existing plugins as they are. To replace the list and keep a `bearer` plugin, send the `set` request with [`unikraft api`](/cli/unikraft/api). ::: ## Reloading across the instance lifecycle The platform treats plugins like [ROMs](/features/roms) across state changes. It reloads them whenever the instance goes through a scale-to-zero, suspend, or restart cycle, so a plugin returns with the instance each time it comes back up. ## Inherited when you clone an instance Plugins belong to the instance definition, so an instance that comes from another one keeps the same plugins. This applies when you use [branching](/features/branching), restore a [checkpoint](/features/checkpoints), [fork](/features/forking) an instance, or start from an [on-demand template](/features/on-demand-templates). ## Use cases The first use case for plugins is a native sandbox: a plugin runs a small HTTP server that accepts commands to run inside the instance and exposes filesystem services, all reachable over the authenticated plugin endpoint. Plugins fit other patterns too. A few ideas: - **Admin and debug endpoints** that stay separate from your app's public [services](/platform/services) and stay reachable only through the authenticated API. - **Health and inspection probes** that read state from inside the instance on demand. - **Sidecar utilities** such as a metrics collector, a log tailer, or a configuration reloader. - **Per-instance tooling** that you attach to one instance without a change to its base image. ## Limitations - You can attach at most 8 plugins to an instance. - A plugin name has a maximum length of 63 characters and uses only letters, digits, `-`, and `_`. - A `bearer` token has a maximum length of 256 characters. ## Learn more * [Plugin SDK](/sdks/plugin): build a plugin in Go without hand-rolling the platform contract. * [Sandboxes](/use-cases/sandboxes): run untrusted code in a hardware-isolated microVM. * [ROMs](/features/roms): the image format that plugins build on. * [Instances](/platform/instances): create and manage the instances that host plugins. * [Scale-to-zero](/features/scale-to-zero): how an idle instance wakes to serve a plugin request. * [Services](/platform/services): the regular way to expose an HTTP server, for contrast with the authenticated plugin endpoint. * Unikraft Cloud's [REST API reference](/api/platform/v1), in particular the [create instance endpoint](/api/platform/v1/instances#create-instance). --- ## Document: On-Demand Templates URL: /docs/features/on-demand-templates # On-Demand Templates > **Availability** > > Available on Unikraft Cloud, dedicated hosts, BYOC and on-prem. > > Exposed by the API and the SDKs. Not exposed by the CLI yet. > > - CLI: The CLI does not support on-demand template creation yet. Call the API, for example with the unikraft api command. [unikraft api](https://unikraft.com/docs/cli/unikraft/api) > > See [Hosting options](https://unikraft.com/docs/platform/hosting-options). [Instance templates](/platform/instances#instance-templates) let you pay an expensive initialization cost once, snapshot the warmed-up instance, and then stamp out new instances from that snapshot in milliseconds. The straightforward way to use them is to manage the template explicitly: create the template first, then create instances that reference it by name. *On-demand template creation* removes that explicit step. Instead of managing templates yourself, you embed the template's specification directly in your instance-create call. If the template doesn't exist yet, the platform creates it on the fly. If it already exists, the platform reuses it. This means you issue the same instance create operation every time and template management happens transparently in the background. This works for any instance-create call—not only one-off instances. In particular, you can create the template of an [autoscaled service](/features/autoscale) this way, so the platform prepares and manages the template autoscale clones from, without a separate template step. ## On-demand template creation To create a template on demand, add `prepare: true` and a `create_args` object to the `template` block of a normal instance-create request: ```bash title="POST /instances" unikraft api /v1/instances --metro=fra \ '{ "name": "my-instance", "template": { "name": "my-template", "prepare": true, "create_args": { "image": "/my-image:v1.0", "memory_mb": 512, "vcpus": 1 } }, "service_group": { "services": [ { "port": 443, "destination_port": 8080, "handlers": ["tls", "http"] } ] }, "autostart": true }' ``` The platform processes the `template` block first: - If a template named `my-template` already exists, the platform uses it directly. - If it doesn't exist, the platform creates it from `create_args`, then creates your instance from the resulting snapshot. Because the call carries everything needed to build the template, you can issue the *exact same* request, irrespective of whether the template already exists. The action is idempotent. ### How `prepare` works When you set `prepare: true` and the template doesn't yet exist, the platform: 1. Starts an instance from `create_args`. 2. Waits for the guest to convert itself into a template by writing to `/uk/libukp/template_instance` (see [Instance templates](/platform/instances#instance-templates)). 3. Takes a snapshot once the guest signals readiness—this snapshot becomes the template. 4. Starts your actual instance from that snapshot. If the template already exists, the platform skips steps 1–3 and your instance starts right away. The guest controls the exact moment of snapshotting. It does heavy, instance-independent initialization up front, then writes to `/uk/libukp/template_instance` at the point where the state is worth capturing. :::caution The image you specify in `create_args` **must** write to `/uk/libukp/template_instance` at some point during its execution. If it never does, the platform waits indefinitely: the instance stays in the `running` state and is never snapshotted, so your actual instance never starts. Make sure the image you use for template preparation includes this write. ::: ### The `create_args` object `create_args` accepts the same fields as a top-level instance-create body—`image`, `memory_mb`, `vcpus`, `restart_policy`, `scale_to_zero`, `roms`, `autokill`, and more. In other words, it fully describes the instance that becomes the template. This symmetry is what makes the feature composable: anything you can specify when creating an instance, you can specify for the instance that becomes your template—including another `template` block (see [Nested templates](#nested-templates) below). ## Injecting per-instance data A template snapshot locks in the environment and arguments captured at snapshot time, so every instance cloned from it starts from identical state. To pass data that differs per instance—a session ID, a config file, a function payload—attach an [inline ROM](/features/roms#inline-roms) to the instance-create call: ```bash title="POST /instances" unikraft api /v1/instances --metro=fra \ '{ "name": "my-instance", "template": { "name": "my-template", "prepare": true, "create_args": { "image": "/my-image:v1.0", "memory_mb": 512 } }, "roms": [ { "name": "session", "at": "/mnt/session", "files": [ { "path": "session.txt", "encoding": "text", "data": "sessionID=XYZ" } ] } ], "service_group": { "services": [ { "port": 443, "destination_port": 8080, "handlers": ["tls", "http"] } ] }, "autostart": true }' ``` The ROM attaches to the new instance, not to the template. This means each instance can carry different data while sharing the same warm snapshot. The write to `/uk/libukp/template_instance` blocks until the platform clones the instance from the template. This means the guest can then write to the file and read its instance-specific data from the ROM once the write returns. ## Nested templates Because `create_args` mirrors a top-level instance-create body, it can itself contain a `template` block with its own `create_args`. This lets you nest layers of templates—and the final instance—in a single API call. A common shape is a generic worker template, a more specific function template built on top of it, and finally the concrete instance: ```bash title="POST /instances" unikraft api /v1/instances --metro=fra \ '{ "name": "my-instance", "template": { "name": "my-function-template", "prepare": true, "create_args": { "template": { "name": "my-node-worker", "prepare": true, "create_args": { "image": "/my-node-worker:v1.0", "memory_mb": 1024 } }, "roms": [ { "name": "function", "at": "/mnt/function", "image": "/my-function:v1.0" } ] } }, "service_group": { "services": [ { "port": 443, "destination_port": 8080, "handlers": ["tls", "http"] } ] }, "autostart": true }' ``` The platform resolves the levels from the innermost outward. The platform reuses any level whose template already exists. It creates the rest on demand. This means a single call can create the worker template, the function template, and the instance—or reuse whichever levels already exist. ## Automatic cleanup with autokill Named templates persist on the machine once created, holding onto the storage their snapshot occupies. To release that space automatically, give the template an [autokill](/features/autokill#instance-template-autokill) policy inside its `template` block: ```bash title="POST /instances" unikraft api /v1/instances --metro=fra \ '{ "name": "my-instance", "template": { "name": "my-template", "prepare": true, "autokill": { "time_ms": 60000 }, "create_args": { "image": "/my-image:v1.0", "memory_mb": 512 } }, "service_group": { "services": [ { "port": 443, "destination_port": 8080, "handlers": ["tls", "http"] } ] }, "autostart": true }' ``` Template autokill deletes the template when nothing clones it for the configured time (measured from the last clone). Because your instance create call still contains all the information needed to rebuild it, the template is transparently recreated the next time a request references it. You pay the preparation cost again only after autokill removes the template and you need it once more. ### Hash-based template names Combine autokill with a naming convention to remove explicit template management entirely: instead of a fixed name like `my-template`, use a hash of the `create_args` object as the template `name`. ```json title="POST /instances" { "name": "my-instance", "template": { "name": "93ab8429c863343d", "prepare": true, "autokill": { "time_ms": 60000 }, "create_args": { "image": "/my-image:v1.0", "memory_mb": 512 } }, "service_group": { "services": [ { "port": 443, "destination_port": 8080, "handlers": ["tls", "http"] } ] } } ``` With this convention: - Whenever `create_args` changes, its hash changes, so the template `name` changes. The platform automatically creates the new template and uses it for this and all later instances that hash to the same name. - The old template is no longer referenced, so its autokill timer eventually removes it. Template management collapses into your standard instance create calls: to switch templates, you change `create_args` and the platform creates, reuses, or cleans up the right template. :::note Use real version tags (for example `v1.0`), not `latest`, in `create_args`. If you push a new image under the same `latest` tag, `create_args` doesn't change, so the hash and template name stay the same and the template is never updated. Bumping the tag to `v2.0` changes `create_args`, which produces a new hash, a new template name, and a freshly prepared template. ::: ## Learn more {/* vale off */} * [Instance templates](/platform/instances#instance-templates)—how templates work, the guest-side `template_instance` write, and template hierarchies. * [ROMs](/features/roms) and [Inline ROMs](/features/roms#inline-roms)—injecting per-instance data into instances cloned from a template. * [Autokill](/features/autokill#instance-template-autokill)—automatically removing templates the platform hasn't cloned recently. * [Autoscale](/features/autoscale)—using on-demand templates as the source for autoscaled instances. {/* vale on */} * Unikraft Cloud's [REST API reference](/api/platform/v1), in particular the section on [instances](/api/platform/v1/instances). --- ## Document: Network Shield and Relay URL: /docs/features/network-shield # Network Shield and Relay > **Availability** > > Available on Unikraft Cloud (enterprise plan), dedicated hosts, BYOC and on-prem. > > Exposed by the API. Coming soon in the CLI. Not exposed by the SDKs yet. > > - Network shield and relay are available to enterprise customers. Contact support to try them. [support@unikraft.com](mailto:support@unikraft.com) > - CLI: Dedicated CLI subcommands are coming soon. Until then, configure the shield and the relay through the API, for example with the unikraft api command. [Create instance API](https://unikraft.com/docs/api/platform/v1/instances#create-instance) > > See [Hosting options](https://unikraft.com/docs/platform/hosting-options). A relay redirects all network traffic to and from a network interface toward another network interface. You can set up a relay VM and configure its interface as the relay for another VM's interface, so that all its traffic transparently passes through the relay. ## The network shield The **network shield** is an example of a relay: an instance inserted between a microVM and the public internet. It sits directly in the path, as a bump on the wire, so every packet the workload sends or receives goes through it first. Since many instances can specify the same microVM as their relay, many sandboxes can share the same network shield. A microVM every packet passes through, which filters traffic and injects secrets Third-party APIs, package registries The shield provides filtering capabilities but also the possibility to inject credentials/secrets for agentic or other use cases. This is useful in sandbox environments, as it means the sandbox won't need unfiltered network access or access to secrets. Each shield runs as its own microVM. This means each shield: - Can be [scaled to zero](/features/scale-to-zero) and woken up when it receives traffic. - Is strongly isolated from the microVMs running the workloads, and also from other network shields. - Is fully flexible and extensible in the capabilities it provides. The image it uses determines its behavior. The official shield runs the `netshield` plugin, which serves an API for its policies. A policy allows traffic by host (wildcards included), network, port, or protocol. It can also intercept TLS, rate-limit requests and connections, and inject secrets into outgoing headers, so a sandbox sends `{{GITHUB_TOKEN}}` and never sees the real value. The shield needs a certificate authority in `CA_CERT` and `CA_KEY` for TLS interception, which every sandbox behind it has to trust. It starts closed, and a shield whose policy fails to apply stays closed, so replace it. On Kubernetes, Kraftlet [runs a shield as a sidecar container](/integrations/kubernetes#egress-filtering-with-a-network-shield) of a Pod. ## Relay To use a relay, reference another interface by name inside a `relay` object: {/* TODO: Update this example to use the official network shield image once it becomes available. */} ```bash title="Relay instance" unikraft api /v1/instances --metro fra \ '{ "name": "my-relay", "image": "/my-relay:latest", "network_interfaces": [ { "name": "my-relay-iface" } ] }' ``` ```bash title="Sandbox instance (relay client)" unikraft api /v1/instances --metro fra \ '{ "name": "my-sandbox", "image": "official/alpine:latest", "network_interfaces": [ { "relay": { "name": "my-relay-iface", "relay_dns": true } } ] }' ``` Relay builds on [custom network configuration](/features/custom-network-configuration): the `relay` object is a field of a `network_interfaces` entry, so the same permission and limitations apply. The `relay` object accepts the following fields: | Field | Required | Description | |-------|----------|-------------| | `name` | Yes | The name of the interface to relay traffic through, which must belong to another instance in your account. | | `relay_dns` | No | Whether the relay forwards DNS requests, `true` by default, with `false` leaving them to the default DNS server. | ### Relay status The instance status shows the relay of each interface that uses one, and the instance that the relay interface belongs to. `unikraft instance get` doesn't show relays, so read the status from the API: ```bash unikraft api /v1/instances/ --metro fra ``` ```json title="Response (excerpt)" { "status": "success", "data": { "instances": [ { "name": "my-sandbox", ... "network_interfaces": [ { "uuid": "", "name": "", ... "relay": { "uuid": "", "name": "my-relay-iface", "instance": { "uuid": "", "name": "my-relay" }, "relay_dns": true } } ] } ] } } ``` ### Relay notes - All network traffic passes over the relay. Many VMs can share a single relay. - Both ends of a relay have to be interfaces that Unikraft Cloud allocates, meaning entries where you set no `ip`, `mac`, or `tap_name` of your own. - You don't need to enable VM-to-VM communication to use this feature (which you don't want in a sandbox environment). - DNS requests can be either passed to the relay or the default DNS server. This is to simplify setups where security isn't the focus as you don't need to run a DNS server in the relay VM. - The guest acting as a relay VM should have net.ipv4.ip_forward=1 and rp_filter=0 set. ## Error handling | Error message | Cause | |---------------|-------| | `Unknown member 'network_interfaces'` | Your account lacks the `net_manager` permission required for this feature. | | `Invalid relay ''. Please make sure the relay exists and does not create a circular dependency` | The interface you named doesn't exist, already relays through another interface, or is itself a custom interface. | | `Network interface without an IP address cannot use a relay` | You configured a `relay` on an interface without an `ip`, and the relay datapath keys on the client interface's address. | ## Limitations - The feature requires the `net_manager` permission (see the note at the top of this page). - Creating a [template](/platform/instances#instance-templates) from an instance **drops** its network configuration, since binding a template to a specific TAP device or IP defeats the purpose of cloning many instances from it. - The create call sets the configuration once, and **no** PATCH support exists yet. - Both ends of a relay have to be interfaces that Unikraft Cloud allocates, meaning entries where you set no `ip`, `mac`, or `tap_name` of your own. - Neither direction of a relay chain works. An interface that already relays can't serve as a relay, and one serving as a relay for others can't take a relay itself. - Every [custom network configuration](/features/custom-network-configuration#limitations) limitation applies here too, since relay is part of that feature. ## Learn more * [Custom network configuration](/features/custom-network-configuration): the interface, gateway, and nameserver settings that relay builds on. * [Instances](/platform/instances): how instances work, from their lifecycle to their configuration. * [Sandboxes](/use-cases/sandboxes): run untrusted code in a hardware-isolated microVM. * [Kubernetes](/integrations/kubernetes#egress-filtering-with-a-network-shield): a shield per Pod on a Kraftlet node. * Unikraft Cloud's [REST API reference](/api/platform/v1), in particular the section on [instances](/api/platform/v1/instances). --- ## Document: Managed Volumes URL: /docs/features/managed-volumes # Managed Volumes > **Availability** > > Available on Unikraft Cloud (enterprise plan), dedicated hosts, BYOC and on-prem. > > Exposed by the API. Not exposed by the CLI and the SDKs yet. > > - Managed volumes are part of enterprise plans. Contact support to enable them for your account. [support@unikraft.com](mailto:support@unikraft.com) > - CLI: There is no dedicated CLI subcommand yet. Drive managed volumes through the API, for example with the unikraft api command. [unikraft api](https://unikraft.com/docs/cli/unikraft/api) > > See [Hosting options](https://unikraft.com/docs/platform/hosting-options). Managed volumes give your instances **permanent storage** backed by a directory on the host machine. Unlike regular [volumes](/platform/volumes), which have a fixed size, a managed volume exposes an existing host directory to the guest over [virtiofs](https://virtio-fs.gitlab.io/). It has no fixed size limit (only the host's disk bounds it), and the data outlives any individual instance. This makes managed volumes a good fit for large or shared datasets that you keep on the host and surface inside one or more instances. ## How it works A managed volume maps a host directory (`host_path`) to a mountpoint (`at`) inside the guest. The platform mounts the directory over virtiofs when the instance starts, so the guest reads and writes the host directory directly. Changes the guest makes appear on the host immediately, and changes on the host appear inside the guest. Because the data lives on the host rather than in a dedicated block device, a managed volume has no fixed size. The platform treats its quota policy as `dynamic`, and counts it toward the number of volumes in your quota but not toward your total volume size quota. :::warning Total volume size quotas aren't yet enforced by the platform for managed volumes, so use them with caution to avoid filling up your host's disk. ::: {/* vale off */} :::tip On BYOC or on-prem Unikraft Cloud installations, managed volumes can also use custom filesystems. Custom filesystem drivers can define lifecycle hooks for `format`, `fscheck`, `mount`, `unmount`, and `delete`, and can accept backend-specific arguments when you create the volume. This makes it possible to present shared object storage as a regular filesystem inside instances, for example by using a POSIX layer like JuiceFS on top of S3. ::: {/* vale on */} ## Creating an instance with a managed volume To attach a managed volume, add an entry with a `host_path` to the `volumes` array when you create an instance. Currently, you drive managed volumes through the [API](/api/platform/v1/instances#create-instance), which you can call with [`unikraft api`](/cli/unikraft/api) or `curl`. ```bash title="unikraft" unikraft api /v1/instances -X POST --metro fra \ '{ "image": "...", "memory_mb": 1024, "autostart": true, "volumes": [ { "at": "/data", "host_path": "/srv/shared-data", "uid": 1001, "gid": 1001 } ] }' ``` ```bash title="API" curl -X POST \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Content-Type: application/json" \ "https://api.fra.unikraft.cloud/v1/instances" \ -d '{ "image": "...", "memory_mb": 1024, "autostart": true, "volumes": [ { "at": "/data", "host_path": "/srv/shared-data", "uid": 1001, "gid": 1001 } ] }' ``` The fields that describe a managed volume are: | Field | Description | |-------|-------------| | `at` | The mountpoint inside the guest. | | `host_path` | The directory on the host to mount into the guest. | | `uid` | The host user ID that the guest's `root` user maps to. | | `gid` | The host group ID that the guest's `root` user maps to. | After the instance starts, the contents of `host_path` show up at `at` inside the guest. ## Ownership and permissions A managed volume maps the guest's `root:root` ownership onto the host `uid:gid` you supply. Every access the guest makes to `host_path` follows the permissions of that `uid:gid` on the host. For example, with `uid` and `gid` set to `1001`, the guest acts as host user and group `1001` whenever it touches the directory. To give the guest read-only access, make `host_path` read-only for that `uid:gid` on the host. {/* vale off */} :::caution Setting both `uid` and `gid` to `0` gives the guest **root** access to `host_path` on the host. Choose these values carefully in production, and prefer a restricted host user where possible. ::: {/* vale on */} ## Limitations - Managed volumes don't accept the `size_mb` and `quota_policy` fields. The size has no fixed limit (only host disk space bounds it), and the platform applies a `dynamic` quota policy. - The `uid:gid` must be able to access every parent directory of `host_path`, since the guest traverses them to reach it. - Mounting the same host directory as more than one volume can cause undesirable side effects. Mount each host directory once. - Managed volumes count toward the number of volumes in your [quota](/platform/quotas), but their size doesn't count toward your volume size quota. ## Learn more * [Volumes](/platform/volumes): fixed-size, block-device volumes managed entirely by the platform. * Unikraft Cloud's [REST API reference](/api/platform/v1), in particular the section on [instances](/api/platform/v1/instances). --- ## Document: Load Balancing URL: /docs/features/load-balancing # Load Balancing > **Availability** > > Available on Unikraft Cloud, dedicated hosts, BYOC and on-prem. > > Exposed by the API, the CLI and the SDKs. > > See [Hosting options](https://unikraft.com/docs/platform/hosting-options). Load balancing in Unikraft Cloud is easy: as soon as you attach more than one instance to a [service](/platform/services), Unikraft Cloud will automatically start balancing traffic between many instances. The load balancing happens based on the number of connections (in TCP mode) or requests (in HTTP mode). More on that below. Because of load balancing, instances in a service must be of the same kind (for example, expose the same port). You can remove instances from a service at any time, and, when you do, Unikraft Cloud will immediately take the instance out of the load balancing service. ## Soft and hard limits [Services](/platform/services) have soft and hard limits for the number of concurrent requests and connections. The limits apply **per instance**. For HTTP services (that is, using the `http` [handler](/platform/services#handlers)), the system checks each individual in-flight request against the limit, but not the underlying TCP connection. For TCP services, the system counts the individual open connections. In the following, the term request *refers* to both requests and connections. The load balancer uses the soft limit to decide when to wake up another [`standby`](/platform/instances#instance-states) instance. For example, if you set the soft limit to 5 and the service consists of 2 standby instances, one of the instances can receive up to 5 concurrent requests. The 6th parallel request wakes up the second instance. If there are no more standby instances to wake up, the number of requests assigned to each instance will exceed the soft limit. The load balancer ensures that when the number of in-flight requests goes down again, instances go into standby as fast as possible. The hard limit defines the max number of concurrent requests that an instance can handle. The load balancer will never assign more requests to a single instance. In case there are no other instances available, the load balancer blocks excess requests (that is, they're blocked and **not queued**). ## Setup To set load balancing up, first use `unikraft run` with the `-p` flag to create the service as part of the instance creation. For example, use NGINX as the app: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/nginx/ unikraft build . --output /nginx:latest unikraft run --metro fra \ -m 256M \ -p 443:8080/http+tls \ --image /nginx:latest ``` This single command will (a) create a service via the `-p` flag and (b) start an NGINX instance: ```ansi metro: fra name: nginx-67zbu uuid: 8a8bc1b9-0af6-420e-a426-190dc2da9eaa state: starting image: /nginx resources:  memory: 256MiB  vcpus: 1 service:  uuid: a942b9b5-ad17-3ffe-dcd2-ef4331f9087a  name: nameless-fog-0tvh1uov  domains:  - fqdn: nameless-fog-0tvh1uov.fra.unikraft.app networks: - uuid: 62d9bbf0-aec8-61f6-7bdb-86edf63dd068  private-ip: 10.0.3.3  mac: 12:b0:c6:23:ed:15 timestamps:  created: just now ``` With this in place, it's now time to start a second instance and attach it to the created service (in this case, named `nameless-fog-0tvh1uov`): ```bash unikraft run --metro fra \ -m 256M \ --service nameless-fog-0tvh1uov \ --image /nginx:latest ``` :::tip If a prompt appears saying *"deployment already exists: what would you like to do with the 1 existing instances"*, choose `keep`. ::: You should see another instance starting up, with the same service and domain as the first instance: ```ansi metro: fra name: nginx-0s295 uuid: 9b1c8e5c-7a0d-4f1e-9c3a-2b8d9f0e5a1f state: starting image: /nginx resources:  memory: 256MiB  vcpus: 1 service:  uuid: a942b9b5-ad17-3ffe-dcd2-ef4331f9087a  name: nameless-fog-0tvh1uov  domains:  - fqdn: nameless-fog-0tvh1uov.fra.unikraft.app networks: - uuid: adf1f17d-bad1-49b2-a126-ed730e06379a  private-ip: 10.0.1.4  mac: 12:b0:0a:00:01:05 timestamps:  created: just now ``` To check that it worked, run the following command: ```bash unikraft services get nameless-fog-0tvh1uov ``` You should see output such as: ```ansi metro: fra name: nameless-fog-0tvh1uov uuid: a942b9b5-ad17-3ffe-dcd2-ef4331f9087a persistent: true limits:  soft: 1  hard: 65535 timestamps:  created: just now domains: - fqdn: nameless-fog-0tvh1uov.fra.unikraft.app instances: - name: nginx-0s295  uuid: 9b1c8e5c-7a0d-4f1e-9c3a-2b8d9f0e5a1f - name: nginx-67zbu  uuid: 8a8bc1b9-0af6-420e-a426-190dc2da9eaa services: - source: 443  destination: 8080  handlers: ["tls", "http"] ``` Note the two instances under the `instances` field. You're now load balancing across 2 NGINX instances! :::tip To ensure that stopping an instance doesn't drop existing connections, use Unikraft Cloud's draining feature by stopping an instance with a drain timeout: ```bash unikraft instance stop --drain-timeout 5s my-instance ``` ::: ## Load balancing algorithm The load balancing algorithm is a variant of `least_conn`. For every instance, the system tracks the number of current in-flight TCP connections (if in `tcp` [mode](/platform/services#handlers)) or requests (if in `http` [mode](/platform/services#handlers)). To select an instance, the system goes over all instances in the service. It finds the set of instances with the least amount of in-flight requests/connections, and picks from that set. To illustrate, imagine this scenario: | Instance ID | # in-flight conns | | ----------- | ----------------- | | `i-0` | 4 | | `i-1` | 1 | | `i-2` | 2 | | `i-3` | 1 | In this case, the algorithm first chooses instances `i-1` and `i-3`, since they both have the least number of connections at the moment (only 1 each). After that, it chooses between these 2 instances and assigns the new connection to one of them. For example, if it chose `i-1`, the next new connection goes to `i-3`. This is because `i-3` now becomes the only instance with only 1 connection (assuming none of the connections that the other instances handle close). ## Learn more * The [CLI reference](/docs/cli/unikraft). * Unikraft Cloud's [REST API reference](/api/platform/v1), and in particular the section on [services](/api/platform/v1/servicegroups). * [Instances](/platform/instances) documentation. * [Service groups](/platform/services) documentation. --- ## Document: Live Migration Move an instance to another node without restarting it. URL: /docs/features/live-migration # Live Migration > **Availability** > > Available on dedicated hosts, BYOC and on-prem. Not available on Unikraft Cloud. > > Exposed by the API. Not exposed by the CLI and the SDKs yet. > > - Unikraft Cloud: Live migration needs control of the nodes, so it is not available on Unikraft Cloud. [Contact support](mailto:support@unikraft.com) > - CLI: There is no dedicated CLI subcommand yet. The examples on this page use the unikraft api command. [unikraft api](https://unikraft.com/docs/cli/unikraft/api) > > See [Hosting options](https://unikraft.com/docs/platform/hosting-options). **Live migration** moves an instance from one node to another. A running instance keeps running while it moves, so it keeps its memory and its state. Use it to: - Empty a node before maintenance. - Move load from a busy node to a quieter one. ## How it works A migration copies an instance from a source node to a destination node: - A running instance moves while it runs, and resumes on the destination with its memory and state. - A stopped instance moves without a snapshot, and an instance in `standby` moves with its snapshot, so it keeps its state. - The volumes of the instance move with it. For a running instance, the platform copies them while the instance runs, and then sends only the blocks that changed. On the destination, the instance keeps its UUID, name, configuration, and creation time. Its uptime, start counts, CPU time, and memory-time continue from their values on the source. Its volumes and its [service group](/platform/services) move with it, and keep their UUIDs and names. The instance gets new network interfaces on the destination. Their private IP addresses and MAC addresses come from that node, so look the instance up through its [private FQDN](/platform/networking#internal-communication) rather than keeping its old private IP address. Once the migration completes, the instance exists only on the destination. If a migration fails, or you abort it, the instance stays on the source node and resumes there. ## Requirements - Your account needs the `migration` permission on both nodes. Root users have it. Your operator grants it to other users, and should grant it only to trusted ones, because the destination creates the instance with the owner and configuration that the source sends. - The owner of the instance has to exist on the destination node, and the instance counts against the owner's quota there, as a new instance would. - The names of the instance, of its volumes, and of its service group have to be free on the destination. - Both nodes need a platform release with live migration, and your operator has to set up the destination to receive migrations. ## Migrating an instance A migration takes two requests: one to the destination node, which issues a token, and one to the source node, which sends the instance. There's no dedicated `unikraft` CLI subcommand yet, so the examples use the [`unikraft api`](/cli/unikraft/api) command. In them, `node-a` and `node-b` are metros of a custom [profile](/tutorials/kraftkit-to-unikraft#profile-management) that point at the API endpoints of the source and destination nodes. ### Issuing a token on the destination Ask the destination node for a token for the instance: ```bash title="unikraft" unikraft api /v1/instances/migrate/accept -X PUT --metro node-b \ '{"uuid": ""}' ``` ```json title="Response" { "status": "success", "data": { "instances": [ { "status": "success", "uuid": "", "url": "ukpmig://api.node-b.example.com:45235", "token": "", "ttl_s": 300 } ] } } ``` The response holds the migration address of the destination in `url`, and the token. A token admits one migration, and only of the instance whose UUID you passed. Leave out `uuid`, or send the request without a body, to get a token that admits any one instance. A token stays valid for `ttl_s` seconds until its first use. Set `ttl_s` in the request to choose a lifetime of up to `3600` seconds. Without it, the node's default applies, which is `300` seconds unless your operator changed it. Treat the token as a secret, since it lets the holder create an instance on the destination. ### Sending the instance from the source Pass the `url` and the `token` to the source node: ```bash title="unikraft" unikraft api /v1/instances/migrate -X PUT --metro node-a \ '{ "uuid": "", "destination": { "url": "ukpmig://api.node-b.example.com:45235", "token": "" } }' ``` You can name the instance by `name` instead of `uuid`, or use `/v1/instances//migrate`. To move more than one instance, send an array of such objects, each with a token of its own. The request returns as soon as the platform accepts the migration, with the [`migration` object](#following-a-migration) of the instance. The source contacts the destination only when the migration starts, so a wrong or expired token doesn't fail the request. To wait for the outcome instead, set `timeout_s`. It takes up to the API timeout of the node, which is `60` seconds by default, and `-1` waits for that maximum. When the timeout expires first, the request reports `Operation timed out` with the `migration` object, and the migration goes on. Without `timeout_s`, a failed migration reports no error: the `migration` object disappears, and the instance stays on the source. ## Following a migration While an instance migrates, its status on the source node has a `migration` object: ```json "migration": { "destination": "ukpmig://api.node-b.example.com:45235", "state": "copying", "total_bytes": 1073741824, "copied_bytes": 268435456 } ``` | Field | Meaning | |---|---| | `destination` | The migration address of the destination node. | | `state` | The phase of the migration. | | `total_bytes` | The size of the files that the migration copies, such as volumes and the snapshot, once the platform knows it. | | `copied_bytes` | How much of `total_bytes` the platform has copied so far. | The `state` takes these values: | State | Meaning | |---|---| | `waiting` | The migration waits for the instance to finish starting, stopping, or suspending. | | `queued` | The migration waits for a free slot, since a node copies the files of at most four migrations at the same time. | | `copying` | The platform copies the configuration and the files of the instance. | | `syncing` | The platform transfers the running instance. | | `committing` | The source hands the instance over to the destination. | | `aborting` | The platform rolls the migration back. | Read it with `unikraft api /v1/instances/ --metro node-a`. Only accounts with the `migration` permission see the `migration` object. During the migration, the destination lists its copy of the instance, with `"incoming": true` in place of `destination`. When the migration completes, the instance disappears from the source node. Queued migrations start in the order of their requests. While a migration waits, you can still start, stop, and change the instance. The platform checks the instance again when the migration starts, and fails the migration if the instance no longer qualifies. If the instance is only busy at that point, the migration keeps its place in the queue and tries again later. While the instance migrates, the platform refuses changes to it, to its service group, and to its volumes. It also refuses to suspend the instance, and to stop it without `force`. A forced stop or a delete of the instance aborts the migration, until the migration reaches `committing`. A stopped instance can still start until the destination has a complete copy, and the migration then moves it as a running instance. A restart of the platform controller doesn't cancel a migration. The migration picks up where it stopped. ## Aborting a migration Abort a migration on the source node: ```bash title="unikraft" unikraft api /v1/instances/migrate/abort -X PUT --metro node-a \ '{"uuid": ""}' ``` You can also use `/v1/instances//migrate/abort`. The request returns at once, and the platform rolls the migration back in the background. The instance stays on the source node and resumes there. You can abort a migration in every state before `committing`. ## Error handling | Error message | Cause | |---------------|-------| | `Failed to migrate instances: insufficient permissions` | Your account lacks the `migration` permission on the node, which reports `Failed to accept migrations` or `Failed to abort migrations` for the other requests. | | `Failed to accept migration: this node accepts no migrations` | The destination node isn't set up to receive migrations. | | `Failed to accept migration: too many migrations accepted` | The destination holds too many tokens, so wait until migrations use them or they expire. | | `Failed to accept migration: too many unused tokens of this user` | You hold 1024 unused tokens on the destination, so use them or wait for them to expire. | | `The destination URL is invalid` | The `url` is empty or too long. | | `The destination token is invalid` | The `token` doesn't have the format of a token. | | `Failed to start migration: EOPNOTSUPP` | The instance, or one of its volumes, can't migrate yet (see [Limitations](#limitations)). | | `Failed to start migration: EBUSY` | The instance or one of its volumes is busy, for example with a snapshot, a scheduled restart, a volume operation, a template that isn't ready, or another migration, so retry later. | | `Failed to start migration: EIO` | A volume of the instance is in the error state. | | `Failed to start migration: service group changed, retry` | The service group of the instance changed during the request, so send it again. | | `Failed to migrate: migration aborted` | An abort request, a forced stop, or a delete ended the migration while the request waited for it. | | `Failed to migrate: ` | The migration failed after it started, for example because the destination refused the token, and the instance stays on the source. | | `Failed to update instance: the instance is migrating` | You changed an instance during its migration, so wait until the migration ends, and its service group and volumes report similar messages. | | `Failed to abort migration: the instance is not migrating out` | The instance has no migration to abort on this node. | | `Failed to abort migration: the migration can no longer be aborted` | The source already committed the instance to the destination. | ## Limitations The platform doesn't migrate the following instances yet: - Instances created with `"type": "full"` (full VMs, see [instance types](/platform/instances#instance-types)). - Instances with [vertical scaling](/features/vertical-scaling). - Instances of [kernel-less images](/platform/images#platform-kernel). - Instances with [inline ROMs](/features/roms#inline-roms). - [Templates](/platform/instances#instance-templates), and instances that are becoming one. - [Autoscale](/features/autoscale) instances, and instances in a service group with autoscale. - Instances with [autokill](/features/autokill), and instances that the platform deletes when they stop. - Instances with dependencies, and instances that running instances depend on. - Instances with changes to volumes, ROMs, or dependencies that wait for the next restart. - Instances in a service group with custom certificates, or in a [persistent service group](/platform/services#persistent-service-groups). - Instances with a [custom TAP device](/features/custom-network-configuration), a [relay](/features/network-shield), or `autoconfig` turned off on an interface. - Instances with network interfaces on a node in [managed network mode](/features/custom-network-configuration#managed-network-mode), since each of those interfaces uses a custom TAP device. Instances cloned from a template can migrate once their template is ready. Every volume of the instance has to qualify, or the instance doesn't migrate. A volume qualifies when it has the same owner as the instance, no other instance has it attached, and it's neither a [managed volume](/features/managed-volumes) nor a [`virtiofs` volume](/platform/volumes#filesystems). ## Learn more * [Instances](/platform/instances): how instances work, their states, and their configuration. * [Networking](/platform/networking): reach an instance through its private FQDN. * [Services](/platform/services): how service groups route traffic to instances. * [Volumes](/platform/volumes): persistent storage that moves with an instance. * The [CLI reference](/docs/cli/unikraft). --- ## Document: Instance Forking URL: /docs/features/forking # Instance Forking > **Availability** > > Coming soon on Unikraft Cloud, dedicated hosts, BYOC and on-prem. Status: Preview. > > - Instance forking is a preview feature and is not on the stable release yet. The interface can change before general availability. [Contact support to try it](mailto:support@unikraft.com) > > See [Hosting options](https://unikraft.com/docs/platform/hosting-options). Instance forking lets a running instance create an independent copy of itself at a snapshot boundary. Both the original instance (the **parent**) and the copy (the **child**) resume execution from the same memory state. This works like [`fork(2)`](https://man7.org/linux/man-pages/man2/fork.2.html) in a POSIX process. ## Filesystem interface The fork interface uses a FUSE (Filesystem in Userspace) filesystem mounted at `/uk/libukp` inside every instance. Apps trigger forks and track child instances by reading and writing (ASCII) files in this filesystem. The filesystem exposes the following top-level entries: | Path | Read | Write | Description | |------|------|-------|-------------| | `/uk/libukp/fork` | Returns the child UUID after a fork (for the parent), or empty (for the child). | Triggers a new fork operation. | Main entry point for forking. | | `/uk/libukp/vm/` | Directory listing of all child instance UUIDs. | — | Each child gets a subdirectory named by its UUID. | ## Triggering a fork To fork the current instance, write any value to the `fork` file: ```bash echo "1" > /uk/libukp/fork ``` The write blocks until the platform completes the fork. When it returns, the parent can read the child's UUID from the same file: ```bash # Returns the UUID of the newly created child (parent only) # To support parallel forks, only the thread initiating the fork can read the return value from the file. cat /uk/libukp/fork ``` For the child instance, reading `/uk/libukp/fork` returns a zero UUID. This is how an app determines whether it's the parent or the child after a fork. ### Concurrent fork requests The platform serializes fork operations. Only one fork can run at a time. If a second fork request arrives while one runs, the platform queues it. If the queue fills up, the write returns `EBUSY`. The platform processes queued requests in order once the current fork completes. ## Tracking child instances After a fork, a new directory appears at `/uk/libukp/vm//` containing files that reflect the child's state: | File | Read | Write | Description | |------|------|-------|-------------| | `snapshot` | Snapshot UUID the child originates from. | — | Read-only. | | `creation_time` | Unix timestamp (seconds since epoch) of child creation. | — | Read-only. | | `status` | Current state: `Starting`, `Running`, `Draining`, `Stopping`, `Stopped`, or `Template`. | — | Read-only. | | `wait` | Exit code of the child (available once stopped). | Write any value to begin waiting for the child to stop. | Supports `poll()` for non-blocking use. Returns `POLLIN` when the exit code becomes available. | ### Waiting for a child to stop To block until a child instance exits: ```bash # Write to begin the wait, then read the exit code echo "1" > /uk/libukp/vm//wait cat /uk/libukp/vm//wait ``` The write to `wait` blocks until the child stops. After the write completes, reading `wait` returns the numeric exit code. If the child has already stopped by the time you write to `wait`, the write completes immediately. Many processes can wait on the same child concurrently. The platform notifies each one when the child exits. For non-blocking usage, `poll()` on the `wait` file descriptor returns `POLLIN` when the exit status becomes available. ### Checking child status To inspect a child's current state without blocking: ```bash cat /uk/libukp/vm//status # Output: Running ``` The platform updates the status file in real time. Possible states and their numeric values: | State | Description | |-------|-------------| | `Stopped` | Instance has exited. | | `Starting` | Instance is booting. | | `Running` | Instance is running. | | `Draining` | Instance is draining connections before stopping. | | `Stopping` | Instance is shutting down. | | `Template` | Instance has become a template. | ## Child lifecycle - Children have [`delete-on-stop`](/platform/instances#delete-on-stop) enabled automatically. The platform removes them when they stop. - The child's `/uk/libukp/vm/` directory starts empty. The child doesn't inherit any children from the parent—it only sees children from forks it starts itself. - A parent can have many children alive at the same time. - A child can itself fork, becoming a parent to its own children. ## Error handling | Error | Cause | |-------|-------| | `EBUSY` | Fork queue is full. | | `EIO` | Hardware signal to the platform failed, or the request belonged to a parent before a fork occurred. | | `ESTALE` | A child VM directory access occurred after the child's cleanup (for example, after the current instance became a child via fork). | ## Learn more * [Instance templates](/platform/instances#instance-templates): a related snapshot-based feature for creating instances from a stopped template. * Unikraft Cloud's [REST API reference](/api/platform/v1), in particular the section on [instances](/api/platform/v1/instances). --- ## Document: Custom Network Configuration URL: /docs/features/custom-network-configuration # Custom Network Configuration > **Availability** > > Available on dedicated hosts, BYOC and on-prem. Coming soon on Unikraft Cloud. > > Exposed by the API. Coming soon in the CLI. Not exposed by the SDKs yet. > > - Unikraft Cloud: Custom network configuration is coming soon to Unikraft Cloud. Managed network mode is only available on dedicated, BYOC, and on-prem installations. [Contact support to try it](mailto:support@unikraft.com) > - CLI: Dedicated CLI subcommands are coming soon. Until then, use the API, for example with the unikraft api command. [unikraft api](https://unikraft.com/docs/cli/unikraft/api) > > See [Hosting options](https://unikraft.com/docs/platform/hosting-options). By default, Unikraft Cloud gives every instance a single network interface, allocated from the platform's address and TAP device pool. Custom network configuration lets you take control of this setup. It allows you to attach up to four interfaces, supply your own IP addresses, MAC addresses, and TAP devices, or take an interface with no address at all. You can also override the gateway and nameserver that the guest uses. This is the building block for private networking between your instances, and for the [network shield and relay](/features/network-shield). ## How it works You describe an instance's networking when you [create it](/api/platform/v1/instances#create-instance), through three optional fields: - `network_interfaces`: a list of one to four interfaces to attach. - `gateway`: the default gateway to configure inside the guest. - `nameserver`: the DNS resolver to configure inside the guest. The first interface in the list is the **primary interface**. It has a special role: - The `private_ip` field reported for the instance always comes from the primary interface. - The proxy forwards incoming traffic to the IP of the primary interface. - The controller ignores any port announcements from the guest that aren't for the primary interface's IP. Leaving out `network_interfaces` is the same as requesting a single pool-allocated interface, so `"network_interfaces": [{}]` and omitting the field entirely behave identically. ## Creating an instance with custom interfaces Add a `network_interfaces` array to your [`POST /instances`](/api/platform/v1/instances#create-instance) request: ```bash unikraft api /v1/instances --metro fra \ '{ "name": "my-instance", "image": "nginx:latest", "network_interfaces": [ { "name": "my-instance-private" }, { "name": "my-instance-public", "ip": "192.168.0.1/30", "tap_name": "vpc0", "autoconfig": true } ], "gateway": "192.168.0.100", "nameserver": "1.2.3.4" }' ``` Each entry in `network_interfaces` accepts the following fields: | Field | Required | Description | |-------|----------|-------------| | `name` | No | The interface name. If omitted, Unikraft Cloud generates one as `-ethX`, falling back to `eth-` when the instance name is too long. | | `ip` | No | The interface IP address in CIDR notation. | | `mac` | No | The interface MAC address as `hh:hh:hh:hh:hh:hh`, see [custom MAC addresses](#custom-mac-addresses). | | `tap_name` | No | The TAP device to attach the interface to (15 characters or less), which on its own gives you [an interface without an address](#interfaces-without-an-address). | | `autoconfig` | No | Whether the platform configures the interface inside the guest, as described under [autoconfiguration](#autoconfiguration). | | `relay` | No | An object that routes all traffic through another interface, as described under [network shield and relay](/features/network-shield). | An `ip` and a `mac` each require a `tap_name`, while a `tap_name` on its own is enough. When you omit all three, Unikraft Cloud allocates the interface, its address, and its MAC from the platform's own pools. Each value you supply has to stay outside the matching pool: the platform's address ranges for an `ip`, its `12:b0:` prefix for a `mac`, and its TAP device names for a `tap_name`. The TAP device itself is yours to create: Unikraft Cloud attaches the interface to a device that already exists. Nothing caps a custom network at a `/30`, though a `/31` or a `/32` leaves the platform no address to derive a gateway or nameserver from. The top-level `gateway` and `nameserver` fields override the values configured inside the guest. If you omit them, Unikraft Cloud derives them from the last usable address in the IP network of the first pool-allocated interface. An instance whose interfaces are all custom gets neither, so supply both fields or configure them in the guest. Like other object names, an interface `name` is **global to your account**, not local to the instance. This lets you reference one instance's interface from another. ### Custom MAC addresses By default the platform sets an interface's MAC address. Pass the `mac` field to pick the address yourself, so a guest that derives its licensing, clustering, or peer identity from its MAC address stays recognizable across recreations of the instance: ```json title="POST /instances" { ... "network_interfaces": [ { "tap_name": "tap-custom0", "mac": "02:1a:2b:3c:4d:5e", "ip": "192.168.0.1/24" } ] } ``` Write the address as `hh:hh:hh:hh:hh:hh`, without shorthand groups or surrounding whitespace, and observe two rules: - The address has to be unicast, so its first octet has to be even. - The address has to sit outside the platform's own pool, so it must not start with `12:b0:`. A `mac` requires a `tap_name`. Whether you pick the address or leave it to the platform, [`GET /instances`](/api/platform/v1/instances#list-instances) reports the address each interface ended up with as the `mac` field of its entry in `network_interfaces`. ### Interfaces without an address An interface that carries a `tap_name` but no `ip` gives the guest a link on your TAP device with no address on it: ```json title="POST /instances" { ... "network_interfaces": [ { "tap_name": "tap-custom0", "mac": "02:1a:2b:3c:4d:5e", "autoconfig": false } ] } ``` This is what a fully customer-managed network needs—an external IPAM, a CNI plugin, or a virtual appliance that handles its own addressing. Your side provides the TAP device and decides the addressing on it, and the platform only attaches the guest's interface to that device. A CNI plugin can also supply the link's addressing and routes without requiring your app or image to configure them. Write the plugin's result into the instance's [`unikraft.com/cni` annotation](/features/annotations#guest-network-configuration-with-cni), and the guest matches the addresses in it to this interface by MAC address. It then configures those addresses and the result's [routes](/features/annotations#routes) itself. The guest doesn't use stateless address autoconfiguration, so IPv6 addresses on the link have to come from that document or from inside the guest. A router advertisement on the link can still set the IPv6 default router and the maximum transmission unit. Such an interface differs from an addressed one in three ways: - `autoconfig` has to be `false`, because the platform has no address to configure in the guest. - The interface reports no `private_ip`, and neither does the instance when the address-less interface is its primary one. - The platform keeps the interface out of the internal DNS and routes no service traffic to it. The platform also never derives a gateway or nameserver from a custom interface's network. Set the instance-level `nameserver` field, and either set `gateway` or list a default route in the [CNI annotation](/features/annotations#default-routes). Otherwise, configure both inside the guest. ### Autoconfiguration `autoconfig` decides whether the platform configures the interface inside the guest. With it on, the platform hands the guest the interface's address—together with the gateway, nameserver, and hostname—at boot, and the guest brings the interface up with them. With it off, the platform passes nothing and skips the interface, so your app or image sets up networking on its own. It defaults to on for the default pool-allocated interface and for any interface with an `ip`. An interface without one has to keep it off, because the platform then has no address to configure in the guest. ### Network shield and relay An interface can route all its traffic through another instance's interface, which is how the [network shield](/features/network-shield) filters a workload's traffic and injects secrets on its behalf. Configure it with the `relay` field of a `network_interfaces` entry. Both ends of a relay have to be interfaces that Unikraft Cloud allocates, meaning entries where you set no `ip`, `mac`, or `tap_name` of your own. ## Managed network mode {/* vale off */} :::caution[**Limited Access**] Managed network mode is only available in BYOC or on-prem Unikraft Cloud installations. Each host sets its network mode through host-level configuration. To explore such a deal - reach out to the [Unikraft Cloud Discord](https://unikraft.com/discord) or send an email to [support@unikraft.com](mailto:support@unikraft.com). ::: {/* vale on */} Operators can hand a node's networking over to their own infrastructure entirely with `managed` network mode. In this mode the platform creates no network devices and assigns no IP addresses on that node. Every interface of an instance attaches to a TAP device that you create on the host before the instance starts, so every `network_interfaces` entry needs a `tap_name`. The address, gateway, and nameserver come from the request or from a [`unikraft.com/cni` annotation](/features/annotations#guest-network-configuration-with-cni): ```json title="POST /instances" { ... "network_interfaces": [ { "tap_name": "tap-web0", "ip": "10.20.0.5/24" } ], "gateway": "10.20.0.1" } ``` Routing, NAT, and firewall rules are yours to provide too, for example through firewall hook scripts on the host. This lets you connect instances directly to networks that you already run, such as VPNs, overlay networks, or routed data-center networks. The platform's address pool and NAT stay out of the path. The default mode, `isolates`, keeps the behavior that the rest of this page describes. ## Error handling | Error message | Cause | |---------------|-------| | `Network interface IP and MAC address require a TAP device` | You set `ip` or `mac` without a `tap_name`, so either add the TAP device or drop the address. | | `Network interface IP address cannot be from the platform's address pool` | The `ip` overlaps the platform's managed address pool. Use an address outside it. | | `Network interface TAP device cannot be from the platform's device pool` | The `tap_name` refers to a platform-managed TAP device. Use one of your own. | | `Invalid MAC address ''` | The `mac` isn't a `hh:hh:hh:hh:hh:hh` address, or it's all-zero, which the platform reads as an omitted field. | | `Network interface MAC address must be a unicast address` | The `mac` has its multicast bit set, so choose an address whose first octet is even. | | `Network interface MAC address cannot be from the platform's address pool` | The `mac` starts with `12:b0:`, which the platform reserves for the addresses it derives itself. | | `Network interface without an IP address cannot be auto-configured in the guest` | You set `autoconfig` to `true` on an interface without an `ip`, which has to keep autoconfiguration off. | | `Network interface without an IP address cannot use a relay` | You configured a `relay` on an interface without an `ip`, and the relay datapath keys on the client interface's address. | | `Invalid relay ''. Please make sure the relay exists and does not create a circular dependency` | The interface named as the relay doesn't exist, already relays through another interface, or is itself a custom interface. | | `Unknown member 'network_interfaces'` | Your account lacks the `net_manager` permission required for this feature. | ## Limitations - An instance can have at most **four** network interfaces. - The feature requires the `net_manager` permission (see the note at the top of this page). - Creating a [template](/platform/instances#instance-templates) from an instance **drops** its network configuration, since binding a template to a specific TAP device or IP defeats the purpose of cloning many instances from it. - You currently **can't** [branch](/features/branching) or [checkpoint](/features/checkpoints) an instance with more than one interface. - The [relay](/features/network-shield) feature works between pool-allocated interfaces only, on both ends. - The create call sets the configuration once; **no** PATCH support exists yet. - [Managed network mode](#managed-network-mode) is only available on BYOC and on-prem installations, and requires the same permission as the rest of this feature. - On a node in managed mode, the TAP device of every interface has to exist before the instance starts. - On a node in managed mode, [autoscale](/features/autoscale), [relay](/features/network-shield) interfaces, internal [service groups](/platform/services), and the internal DNS for instance names aren't available. ## Learn more * [Network shield and relay](/features/network-shield): route an instance's traffic through another instance that filters it and injects credentials. * [Annotations](/features/annotations#guest-network-configuration-with-cni): hand a CNI result to the guest through the `unikraft.com/cni` annotation, and let it address its own interfaces. * [Kubernetes](/integrations/kubernetes#custom-networking-with-cni): how Kraftlet runs the CNI plugins of your cluster for Unikraft Cloud instances. * [Instances](/platform/instances): how instances work, including their lifecycle and configuration. * [Branching](/features/branching) and [Checkpoints](/features/checkpoints): create independent copies and reusable restore points of running instances. * [Instance templates](/platform/instances#instance-templates): reusable images that you clone into new instances. * Unikraft Cloud's [REST API reference](/api/platform/v1), in particular the section on [instances](/api/platform/v1/instances). --- ## Document: Cron Jobs / Scheduled Wake-ups URL: /docs/features/cron-jobs # Cron Jobs / Scheduled Wake-ups > **Availability** > > Available on Unikraft Cloud, dedicated hosts, BYOC and on-prem. > > Exposed by the API. Not exposed by the CLI and the SDKs yet. > > - CLI: There is no CLI subcommand for schedules yet. Manage them through the API, for example with the unikraft api command. [unikraft api](https://unikraft.com/docs/cli/unikraft/api) > > See [Hosting options](https://unikraft.com/docs/platform/hosting-options). Scheduled operations let you automatically start, stop, delete, checkpoint, or exec a command in instances on a calendar-based schedule. Each scheduled operation specifies a name, a calendar expression, and an action (`start`, `stop`, `delete`, `exec`, or `checkpoint`). :::info When you clone an instance from a [template](/platform/instances#instance-templates), the clone inherits its scheduled operations. ::: ## Calendar expression format Unikraft Cloud uses systemd calendar events (see [`systemd.time(7)`](https://www.man7.org/linux/man-pages/man7/systemd.time.7.html)). Calendar expressions have the following fields: ```text title="" [WEEKDAY] [[YEAR-]MONTH-DAY] [HOUR:MINUTE[:SECOND]] ``` The expression syntax supports ranges, steps, and comma-separated lists: | Syntax | Description | |--------|-------------| | `*` | Any value | | `5` | Exact value | | `1..5` | Range | | `1..5/2` | Range with step | | `1,2,5` | Comma-separated list | ## Setting a schedule at instance creation Pass `schedules` in the [create request](/api/platform/v1/instances#create-instance). For example, to start an instance every day at 09:00 UTC and stop it at 18:00 UTC: ```bash title="unikraft" unikraft api /v1/instances -X POST --metro fra \ '{ "image": "...", "schedules": [ { "name": "morning-start", "when": "*-*-* 09:00:00", "action": "start" }, { "name": "evening-stop", "when": "*-*-* 18:00:00", "action": "stop" } ] }' ``` ```bash title="API" curl -X POST \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Content-Type: application/json" \ "https://api.fra.unikraft.cloud/v1/instances" \ -d '{ "image": "...", "schedules": [ { "name": "morning-start", "when": "*-*-* 09:00:00", "action": "start" }, { "name": "evening-stop", "when": "*-*-* 18:00:00", "action": "stop" } ] }' ``` To run a command inside the instance every night at midnight, use the `exec` action with an `args` array: ```bash title="unikraft" unikraft api /v1/instances -X POST --metro fra \ '{ "image": "...", "schedules": [ { "name": "nightly-cleanup", "when": "*-*-* 00:00:00", "action": "exec", "args": ["/bin/sh", "-c", "rm -rf /tmp/*"] } ] }' ``` ```bash title="API" curl -X POST \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Content-Type: application/json" \ "https://api.fra.unikraft.cloud/v1/instances" \ -d '{ "image": "...", "schedules": [ { "name": "nightly-cleanup", "when": "*-*-* 00:00:00", "action": "exec", "args": ["/bin/sh", "-c", "rm -rf /tmp/*"] } ] }' ``` ## Updating schedules of an existing instance Add, set, or remove scheduled operations via [PATCH](/api/platform/v1/instances#update-instance-by-uuid): ```bash title="unikraft" unikraft api /v1/instances/ -X PATCH --metro fra \ '{ "prop": "schedules", "op": "add", "value": [ { "name": "weekend-stop", "when": "Sat,Sun *-*-* 20:00:00", "action": "stop" } ] }' ``` ```bash title="API" curl -X PATCH \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Content-Type: application/json" \ "https://api.fra.unikraft.cloud/v1/instances/" \ -d '{ "prop": "schedules", "op": "add", "value": [ { "name": "weekend-stop", "when": "Sat,Sun *-*-* 20:00:00", "action": "stop" } ] }' ``` Add an `exec` schedule the same way, but include `args` with the command to run: ```bash title="unikraft" unikraft api /v1/instances/ -X PATCH --metro fra \ '{ "prop": "schedules", "op": "add", "value": [ { "name": "hourly-healthcheck", "when": "*-*-* *:00:00", "action": "exec", "args": ["/usr/bin/healthcheck"] } ] }' ``` ```bash title="API" curl -X PATCH \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Content-Type: application/json" \ "https://api.fra.unikraft.cloud/v1/instances/" \ -d '{ "prop": "schedules", "op": "add", "value": [ { "name": "hourly-healthcheck", "when": "*-*-* *:00:00", "action": "exec", "args": ["/usr/bin/healthcheck"] } ] }' ```
```bash title="unikraft" unikraft api /v1/instances/ -X PATCH --metro fra \ '{ "prop": "schedules", "op": "del", "value": [ "morning-start" ] }' ``` ```bash title="API" curl -X PATCH \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Content-Type: application/json" \ "https://api.fra.unikraft.cloud/v1/instances/" \ -d '{ "prop": "schedules", "op": "del", "value": [ "morning-start" ] }' ``` ## Actions | Action | Description | |--------|-------------| | `start` | Start the instance at the scheduled time | | `stop` | Stop the instance at the scheduled time | | `delete` | Delete the instance at the scheduled time | | `exec` | Execute a command inside the instance at the scheduled time. Requires the `args` field. | | `checkpoint` | Create a [checkpoint](/features/checkpoints) of the instance at the scheduled time, as described under [scheduled checkpoints](#scheduled-checkpoints) | The `args` field is an array of strings specifying the command to run when using the `exec` action. The first element is the executable and the remaining elements are its arguments. ```json title="" { "name": "daily-report", "when": "*-*-* 06:00:00", "action": "exec", "args": ["/usr/bin/python3", "/app/report.py", "--daily"] } ``` No need to specify it for all other actions. ## Scheduled checkpoints The `checkpoint` action periodically creates restore points for an instance. Every occurrence creates a [checkpoint](/features/checkpoints) of the instance, the same way a call to [`POST /instances/checkpoints`](/api/platform/v1/instances) does. This builds a history of restore points without an external scheduler, but it doesn't rotate older checkpoints - they persist until explicitly deleted. To checkpoint an instance every night at 03:00 UTC: ```bash unikraft api /v1/instances -X POST --metro fra \ '{ "image": "...", "schedules": [ { "name": "nightly-checkpoint", "when": "*-*-* 03:00:00", "action": "checkpoint" } ] }' ``` The platform names each new checkpoint after the instance and appends a random suffix, which matches the name a checkpoint gets when you create one through the API without one. Find the resulting checkpoints in the instance's [checkpoint history](/features/checkpoints#checkpoint-history). Note the following: * Scheduled checkpoints only work with block-based volumes (for example, `ext4`) - occurrences fail for other volume types. * Full VMs running on QEMU (`"type": "full"`) don't support checkpointing yet, so the action fails on them. * A failing occurrence leaves the schedule in place and doesn't affect the other scheduled operations of the instance. Because the instance status reports only the next elapse of a schedule and not the result of past runs, use the checkpoint history to confirm that occurrences produce checkpoints. ## Learn more * [Checkpoints](/features/checkpoints): creating, listing, and loading the restore points a `checkpoint` schedule produces. * Unikraft Cloud's [REST API reference](/api/platform/v1), in particular the section on [instances](/api/platform/v1/instances). --- ## Document: Checkpoints URL: /docs/features/checkpoints # Checkpoints > **Availability** > > Available on Unikraft Cloud (enterprise plan), dedicated hosts, BYOC and on-prem. > > Exposed by the API, the CLI and the SDKs. > > - Checkpoints are part of enterprise plans. Contact support to enable them for your account. [support@unikraft.com](mailto:support@unikraft.com) > > See [Hosting options](https://unikraft.com/docs/platform/hosting-options). A **checkpoint** captures the full state of an instance—its memory and volume state—at any moment, so you can later start new instances that resume from exactly that state. Unlike a [branch](/features/branching), which creates a single independent copy right away, a checkpoint is a reusable, named restore point that you can load as many times as you like. You can take a checkpoint of a running instance without stopping it, keep a history of successive checkpoints, and create new instances from any checkpoint later on. ## How it works Checkpointing builds on the same machinery as [branching](/features/branching). When you create a checkpoint, Unikraft Cloud: 1. Branches the source instance using copy-on-write (CoW) asynchronous snapshotting. Because the snapshot is copy-on-write and taken asynchronously, the source instance only pauses for a few milliseconds before it resumes running. 2. Converts the resulting branch into an [instance template](/platform/instances#instance-templates) and marks it as a checkpoint. This means checkpoints reuse the entire template lifecycle—storage, cloning, autokill, tags, and delete locks—but with their own dedicated endpoints. A freshly created checkpoint starts in the `starting` state while the platform is still building its snapshot. Once the snapshot completes and the checkpoint becomes ready, it transitions to the `checkpoint` state. You can only [load](#loading-a-checkpoint) a checkpoint once it reaches the `checkpoint` state. ## Creating a checkpoint To create a checkpoint, pass the source instance to `unikraft instance checkpoint create`: ```bash unikraft instance checkpoint create my-instance ``` The CLI doesn't name the checkpoint, so the platform derives a name from the source instance's name plus a random suffix. A new checkpoint reports `starting` until its snapshot completes. `unikraft instance checkpoint create` waits for that, up to the platform's API timeout (60 seconds by default), and prints a warning if the checkpoint isn't ready yet when the wait ends. To wait for an existing checkpoint to become ready: ```bash unikraft instance checkpoint wait my-checkpoint --until state==checkpoint ``` To choose the name yourself, call [`POST /instances/checkpoints`](/api/platform/v1/instances) directly—the CLI doesn't cover it: ```bash title="POST /instances/checkpoints" unikraft api /v1/instances/checkpoints --metro fra \ '{ "name": "my-checkpoint", "from": { "name": "my-instance" } }' ``` To take checkpoints on a recurring schedule rather than on demand, give the instance a `checkpoint` [scheduled operation](/features/cron-jobs#scheduled-checkpoints). ## Checkpoint history Every instance keeps an ordered history of its checkpoints, spanning the full lineage rather than only the checkpoints taken directly from that instance. When you create an instance from a checkpoint and then take further checkpoints from that instance, its history includes the new checkpoints and the ones inherited from the parent checkpoint. For example, given this sequence: 1. Create `inst1`. 2. Create `chk1` from `inst1`. 3. Create `inst2` from `chk1`. 4. Create `chk2` from `inst2`. The history of `inst2` lists both `chk2` (taken directly from `inst2`) and `chk1` (inherited from the checkpoint `inst2` was loaded from). To query this history, run `unikraft instance history`: ```bash unikraft instance history my-instance ``` ```text title="output" METRO TARGET NAME CREATED fra fra/my-instance my-other-checkpoint 2 minutes ago fra fra/my-instance my-checkpoint 4 minutes ago ``` The checkpoints themselves also carry this history of their lineage. You can query it directly with `unikraft instance checkpoint history`, which returns the same shape: ```bash unikraft instance checkpoint history my-checkpoint ``` ## Checkpoint information To retrieve detailed information about a checkpoint, including its instance configuration, and volumes, query it by name or UUID: ```bash unikraft instance checkpoint get my-checkpoint ``` List all available checkpoints by omitting the identifier: ```bash unikraft instance checkpoint list ``` ```text title="output" METRO NAME STATE IMAGE ARGS MEMORY VCPUS CREATED fra my-checkpoint checkpoint nginx 256MiB 2 4 minutes ago ``` ## Loading a checkpoint You load a checkpoint by creating a new instance from it. Pass the checkpoint to `--checkpoint`: ```bash unikraft instance create --metro fra \ --name my-new-instance \ --checkpoint my-checkpoint ``` Use `unikraft run --checkpoint` instead to load the checkpoint and follow the new instance's logs in one step. The new instance inherits the image, vCPUs, memory, arguments, environment, and saved memory and volume state from the checkpoint. As with [branching](/features/branching) and [instance templates](/platform/instances#instance-templates), you can configure the remaining properties—the instance `name`, [volumes](/platform/volumes), [ROMs](/features/roms), and [services](/platform/services). The checkpoint must be ready (in the `checkpoint` state) before you can load it. Loading a checkpoint that's still in the `starting` state fails (see [Error handling](#error-handling)). ## Autokill Like instance templates, checkpoints persist on the machine once created, holding onto the storage their snapshot occupies. A checkpoint can carry an [autokill](/features/autokill) policy that removes it automatically when nothing loads it for a configured time, measured from the last load. Set the policy with `--autokill` when you create the checkpoint: ```bash unikraft instance checkpoint create my-instance --autokill time=1h ``` You can also set or update the policy on an existing checkpoint: ```bash unikraft instance checkpoint edit my-checkpoint --autokill time=1h ``` This example removes the checkpoint after 1 hour without a load. `unikraft instance checkpoint get` reports the policy as `autokill.time`. ## Editing a checkpoint Use `unikraft instance checkpoint edit` to update a checkpoint's tags, delete lock, and [autokill](#autokill) policy: ```bash # add a tag, keeping the existing ones unikraft instance checkpoint edit my-checkpoint --add tags=my-new-tag # replace the tags outright unikraft instance checkpoint edit my-checkpoint --tag my-only-tag # protect the checkpoint from deletion unikraft instance checkpoint edit my-checkpoint --delete-lock ``` ## Deleting a checkpoint Delete a checkpoint by name or UUID: ```bash unikraft instance checkpoint delete my-checkpoint ``` :::note If a checkpoint has a [delete lock](/platform/delete-locks) set, the delete request fails until you remove the lock: ```bash unikraft instance checkpoint edit my-checkpoint --set delete-lock=false ``` ::: Deleting the instance that owns a set of checkpoints doesn't remove the checkpoints. They remain available to load until you delete them explicitly or their [autokill](#autokill) policy removes them. ## Error handling The CLI rejects a checkpoint in a different metro from the new instance before it sends anything, with `cannot create instance: metro mismatch between checkpoint ("fra") and instance ("dal2")`. Otherwise, a checkpoint request can fail for the following reasons: | Error message | Cause | |---------------|-------| | `A checkpoint with the name '' already exists` | A checkpoint with the requested name already exists. Choose a different name. | | `Failed to create instance from checkpoint: ...` | The referenced checkpoint isn't ready yet. Wait until it reaches the `checkpoint` state before loading it. | | `Deletion protection enabled` | The checkpoint has a [delete lock](/platform/delete-locks) set. Remove the lock before deleting. | | `Failed to allocate checkpoint: ...` | The platform couldn't create the checkpoint (for example, it hit a quota limit). | ## Limitations - A checkpoint and the instances you load from it must stay in the same metro. The CLI catches a cross-metro checkpoint before it sends the request. - Checkpointing is only supported on `--type=micro` instances running on Firecracker (which is the default). Full VMs running on QEMU, which [GPU instances](/platform/instances#gpus) require, don't support checkpointing yet, so a create request can't combine `--type=full` with `--checkpoint`. - Checkpointing builds on [branching](/features/branching), so it only works with block-based volumes (for example, `ext4`). The checkpoint clones the source's volume state consistently with its memory snapshot, which isn't supported for other volume types. ## Learn more * [Branching](/features/branching): create a single independent copy of a running instance—the mechanism checkpoints build on. * [Instance templates](/platform/instances#instance-templates): how templates work, the lifecycle checkpoints reuse. * [Autokill](/features/autokill): automatically removing checkpoints and templates the platform hasn't loaded recently. * [Snapshots](/features/snapshots): the copy-on-write snapshotting that underpins checkpoints. * [Serverless databases](/use-cases/serverless-databases): a use case that checkpoints a running PostgreSQL instance to capture and restore its state. * [Cron jobs / scheduled wake-ups](/features/cron-jobs#scheduled-checkpoints): the `checkpoint` action periodically creates restore points for an instance. * Unikraft Cloud's [REST API reference](/api/platform/v1), in particular the section on [instances](/api/platform/v1/instances). --- ## Document: Branching URL: /docs/features/branching # Branching > **Availability** > > Available on Unikraft Cloud (enterprise plan), dedicated hosts, BYOC and on-prem. > > Exposed by the API, the CLI and the SDKs. > > - Branching is part of enterprise plans. Contact support to enable it for your account. [support@unikraft.com](mailto:support@unikraft.com) > > See [Hosting options](https://unikraft.com/docs/platform/hosting-options). Branching lets you create a clone (a **branch**) of an existing instance, including its memory and volume state. The branch is an identical but independent copy of the source. It starts from the same memory state instead of going through a cold boot, and from then on runs on its own without affecting the source. This resembles creating an instance from an [instance template](/platform/instances#instance-templates), but the source must be an ordinary instance, not a template. ## How it works Branching builds on copy-on-write (CoW) asynchronous snapshotting. When you create a branch, Unikraft Cloud snapshots the source instance's memory and volume state. The snapshot becomes the starting point for the new instance. Because the snapshot is copy-on-write and taken asynchronously, the source instance only pauses for a few milliseconds before it resumes running. The source instance's [state](/platform/instances#instance-states) determines how Unikraft Cloud populates the branch: | Source state | Behavior | |--------------|----------| | Running | Unikraft Cloud takes an asynchronous CoW snapshot of the source. The branch waits for that snapshot to complete and then starts from it. | | Stopped or Standby | Unikraft Cloud creates the branch right away from the source's existing snapshot, or from its image if the source has none. | ## Creating a branch To create a branch, pass the source instance to `--branch`: ```bash unikraft instance create --metro fra \ --name my-branch \ --branch my-instance ``` You can also reference the source by its UUID instead of its name. Use `unikraft run --branch` instead of `unikraft instance create --branch` to create the branch and follow its logs in one step. Apart from the branch source, a branch accepts the same options as a regular instance creation. You can attach [ROMs](/features/roms) and [volumes](/platform/volumes), configure a [service](/platform/services), and set other options as when you create an instance from a template. ## What the branch inherits A branch derives the following from its source, so you **can't** set these fields in the create request: - The image (`--image`) - The number of vCPUs (`--vcpus`) - The memory size (`--memory`) - The app arguments (`--args`) - The environment variables (`--env`) - The memory and volume state at the time of the snapshot Setting any of these alongside a branch source makes the request fail (see [Error handling](#error-handling)). You can still give the branch its own configuration, as when instantiating a template, including a `name`, [ROMs](/features/roms), [volumes](/platform/volumes), and a [service](/platform/services). ## Error handling The CLI rejects `--branch` alongside `--image`, `--template`, or `--checkpoint` before it sends anything, with `only one of --image, --template, --branch, or --checkpoint may be specified`. It also rejects a source in a different metro from the new instance, with `cannot create instance: metro mismatch between branch ("fra") and instance ("dal2")`. Otherwise, a branch request can fail for the following reasons: | Error message | Cause | |---------------|-------| | `Cannot specify 'image' when 'branch_from' is set` | The request sets both `image` and `branch_from`. A branch derives its image from the source. | | `Cannot specify 'branch_from' when creating an instance from a template` | The request sets both `template` and `branch_from`. Choose one creation source, not both. | | `Cannot specify '' when branching from an existing instance` | The request sets a field the branch inherits from its source (`args`, `env`, `memory_mb`, or `vcpus`). | | `Cannot branch from VM with pending snapshot` | The source already has a snapshot in progress. Retry once it completes. | | `Cannot branch from VM in state ''` | The source is in a state that branching doesn't support. | ## Limitations - A branch must stay in the metro of its source. The CLI catches a cross-metro source before it sends the request. - Branching is only supported on `--type=micro` instances running on Firecracker (which is the default). Full VMs running on QEMU, which [GPU instances](/platform/instances#gpus) require, don't support branching yet, so a create request can't combine `--type=full` with `--branch`. - Branching only works with block-based volumes (for example, `ext4`). The branch clones the source's volume state consistently with its memory snapshot, which isn't supported for other volume types. ## Learn more * [Instance templates](/platform/instances#instance-templates): create instances from a stopped, snapshotted template. * [Instance forking](/features/forking): create a copy of an instance from within the instance itself. * [Serverless databases](/use-cases/serverless-databases): a use case that branches a running PostgreSQL instance to clone its state. * Unikraft Cloud's [REST API reference](/api/platform/v1), in particular the [create instance endpoint](/api/platform/v1/instances#create-instance). --- ## Document: Autoscale URL: /docs/features/autoscale # Autoscale > **Availability** > > Available on Unikraft Cloud, dedicated hosts, BYOC and on-prem. > > Exposed by the API and the SDKs. Coming soon in the CLI. > > - CLI: Dedicated CLI subcommands are coming soon. Until then, drive autoscale through the API, for example with the unikraft api command. [Autoscale API](https://unikraft.com/docs/api/platform/v1/autoscale) > > See [Hosting options](https://unikraft.com/docs/platform/hosting-options). {/* vale off */} :::caution[**Limited Access**] At the moment, autoscale **is not** enabled by default (you might get an *"Autoscale not enabled for your account"* error). If you would like to enable it, please reach out to the [Unikraft Cloud Discord](https://unikraft.com/discord) or send an email to [support@unikraft.com](mailto:support@unikraft.com). ::: {/* vale on */} Autoscaling is [load balancing](/features/load-balancing) where the *number of instances* used to handle your traffic automatically adapts to match the current traffic load. On Unikraft Cloud, scale-out (the process of adding instances to cope with increased load) happens in milliseconds. You can transparently and effortlessly handle load increase including traffic peaks. No more headaches due to slow autoscale like keeping hot instances around to deal with peaks, coming up with complex predictive algorithms, or other painful workarounds. You can set autoscale on and let Unikraft Cloud handle your traffic increases and peaks. ## The basics As with [load balancing](/features/load-balancing), autoscaling in Unikraft Cloud takes care of a [service](/platform/services). Services allow you to load balance traffic for an Internet-facing service like a web server by creating many instances within the same service. While you can add or remove instances to a service to scale your service, doing this manually makes it hard to react to changes in traffic load. Keeping many instances running to cope with intermittent bursts would be wasteful and expensive. This is where autoscale comes into play. With autoscale enabled, Unikraft Cloud takes care of the heavy lifting for you by continuously monitoring the load of your service and automatically creating or deleting instances as needed. :::note Autoscale, as well as load balancing in general, currently supports only Internet-facing services. ::: ## Setting up autoscale First, create an instance, in this example using NGINX: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/nginx/ unikraft build . --output /nginx:latest unikraft run --metro fra \ -m 256M \ -p 443:8080/http+tls \ --image /nginx:latest ``` The output shows the instance address and other details: ```ansi metro: fra name: nginx-67zbu uuid: 8a8bc1b9-0af6-420e-a426-190dc2da9eaa state: starting image: /nginx resources:  memory: 256MiB  vcpus: 1 service:  uuid: a942b9b5-ad17-3ffe-dcd2-ef4331f9087a  name: nameless-fog-0tvh1uov  domains:  - fqdn: nameless-fog-0tvh1uov.fra.unikraft.app networks: - uuid: 62d9bbf0-aec8-61f6-7bdb-86edf63dd068  private-ip: 10.0.3.3  mac: 12:b0:c6:23:ed:15 timestamps:  created: just now ``` :::note Autoscale requires an [instance template](/platform/instances#instance-templates) to create new instances during scale-out. If you enable autoscale through the API, provide the template under the top-level `template` field. ::: The next step is to convert the instance you deployed into a template that autoscale clones during scale-out: ```bash unikraft instance template create nginx-67zbu ``` The instance's `state` changes to `template`, and its name (`nginx-67zbu`) is the `` you reference below. Converting the instance into a template consumes it, so the service that the deploy step created for it (`nameless-fog-0tvh1uov`) no longer exists. Create a new persistent [service](/platform/services) for autoscale to manage: ```bash unikraft service create --metro fra \ --name nginx-autoscale-service \ --domain nginx-autoscale \ --service 443:8080/http+tls ``` The command prints the new service's details, including its `uuid` (for example, `996d006f-a491-44ae-a1dc-7a6186971351`). Use that `uuid`—shown as `` below—in the autoscale calls. All that's left to do now to set up autoscale is to configure the autoscale settings (including the **template** to clone) and to define a scaling *policy*. Unikraft Cloud then takes care of cloning this template whenever load increases. :::note You can create the template with [on-demand template creation](/features/on-demand-templates) instead of managing the template yourself. The instance-create call then prepares and reuses the underlying template automatically. ::: Achieve this with the following commands: ```bash unikraft api /v1/services//autoscale -X POST --metro fra \ '{ "min_size": 1, "max_size": 8, "warmup_time_ms": 1000, "cooldown_time_ms": 1000, "template": { "name": "" } }' unikraft api /v1/services//autoscale/policies -X POST --metro fra \ '{ "name": "scale-out-policy", "type": "step", "metric": "cpu", "adjustment_type": "percent", "steps": [ { "lower_bound": 600, "upper_bound": 800, "adjustment": 50 }, { "lower_bound": 800, "adjustment": 100 } ] }' unikraft api /v1/services//autoscale/policies -X POST --metro fra \ '{ "name": "scale-in-policy", "type": "step", "metric": "cpu", "adjustment_type": "percent", "steps": [ { "upper_bound": 500, "adjustment": -50 } ] }' ``` Note the following: * The first command sets the template to clone, and configures it to scale up to a maximum of 8 instances and a minimum of 1; the command also sets the warm up and cool down time to 1 second each, so it doesn't constantly fluctuate up and down. * The second command sets the *scale-out* policy based on CPU utilization (in millicores): between 60% and 80% utilization, the system increases instances by 50%. From 80% onward, the number of instances doubles. * The third command sets the *scale-in* policy: below 50% utilization, the system reduces the number of instances by half (note the `-` sign for scale-in). :::note The platform waits between decisions so the service doesn't constantly scale in and out. The warm-up time is how long it waits after scaling out before it can scale again, and the cool-down time is how long it waits after scaling in. Set them with the `warmup_time_ms` and `cooldown_time_ms` fields. Refer to the API autoscale [reference](/api/platform/v1/autoscale) for more details. Keep in mind that a few restrictions apply to how you define scale-in/scale-out steps. ::: ### High watermark {/* vale off */} :::caution[**Limited Access**] The high watermark signal is available to enterprise customers. If you would like to try it out now, please reach out to the [Unikraft Cloud Discord](https://unikraft.com/discord) or send an email to [support@unikraft.com](mailto:support@unikraft.com). ::: {/* vale on */} An autoscale group can be configured with a high watermark - as an early warning that it's getting close to its maximum size. Set it through the `high_watermark` field, as a fraction of `max_size` between `0` and `1`. The default is `0`, which turns the signal off. Once a service crosses its watermark, the platform can report this on the responses it sends back. A client that spreads load across metros uses this to steer new work elsewhere before the service fills up. Your metro operator switches that response header on. To check it's working, list the autoscale properties of the service: ```bash unikraft api /v1/services//autoscale -X GET --metro fra ``` You should see output like: ```json { "status": "success", "data": { "service_groups": [ { "status": "success", "uuid": "5ca059ec-a24a-41f2-8413-f09bc58730ca", "name": "nameless-fog-0tvh1uov", "enabled": true, "min_size": 1, "max_size": 8, "warmup_time_ms": 1000, "cooldown_time_ms": 1000, "template": { "name": "nginx-67zbu", "uuid": "8a8bc1b9-0af6-420e-a426-190dc2da9eaa" }, "policies": [ { "name": "scale-out-policy", "type": "step", "enabled": true, "metric": "cpu", "adjustment_type": "percent", "steps": [ { "adjustment": 50, "lower_bound": 600, "upper_bound": 800 }, { "adjustment": 100, "lower_bound": 800 } ] }, { "name": "scale-in-policy", "type": "step", "enabled": true, "metric": "cpu", "adjustment_type": "percent", "steps": [ { "adjustment": -50, "upper_bound": 500 } ] } ] } ] } } ``` To list an individual policy: ```bash unikraft api /v1/services//autoscale/policies/scale-out-policy -X GET --metro fra ``` You should see output like: ```json { "status": "success", "data": { "policies": [ { "status": "success", "policy": { "name": "scale-out-policy", "type": "step", "enabled": true, "metric": "cpu", "adjustment_type": "percent", "steps": [ { "adjustment": 50, "lower_bound": 600, "upper_bound": 800 }, { "adjustment": 100, "lower_bound": 800 } ] } } ] } } ``` {/* TODO: add example */} ## Policy types Four autoscale policy types are available. A service can have more than one policy active at the same time. ### Step policy The `step` policy scales instances based on metric thresholds. You define up to **4 steps**, each specifying a lower bound, upper bound, and the scaling change to apply when the metric falls in that range. You should order steps by lower bound with no gaps between them and no overlaps: ```bash unikraft api /v1/services//autoscale/policies -X POST --metro fra \ '{ "name": "my-step-policy", "type": "step", "metric": "cpu", "adjustment_type": "change", "steps": [ { "lower_bound": 600, "upper_bound": 800, "adjustment": 2 }, { "lower_bound": 800, "adjustment": 4 } ] }' ``` The following metrics can drive a step policy: | Metric | Description | |--------|-------------| | `cpu` | CPU utilization in millicores | | `inflight_reqs` | Number of requests the platform is processing across all instances | | `reqs_per_sec` | Request throughput in requests per second | Step policies support three scaling change types: | Type | Description | |------|-------------| | `change` | Change the instance count by the specified value (positive to scale out, negative to scale in) | | `exact` | Set the instance count to exactly the specified value | | `percent` | Change the instance count by the specified percentage of the current count | ### On-demand policy The `on-demand` policy creates a new instance immediately when an incoming request finds no available instances. This prevents request queuing but introduces cold start delays. ```bash title="unikraft" unikraft api /v1/services//autoscale -X POST --metro fra \ '{ "policies": [ { "name": "scale-out", "type": "on_demand" } ], ... }' ``` ```bash title="API" curl -X POST \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Content-Type: application/json" \ "https://api.fra.unikraft.cloud/v1/services//autoscale" \ -d '{ "policies": [ { "name": "scale-out", "type": "on_demand" } ], ... }' ``` ### Create policy The `create` policy provisions a new VM when an instance exceeds the `num_requests` threshold. Setting `replace` to true deletes the original VM after the new one starts. ```bash title="unikraft" unikraft api /v1/services//autoscale -X POST --metro fra \ '{ "policies": [ { "name": "create", "type": "create", "replace": true, "num_requests": 100 } ], ... }' ``` ```bash title="API" curl -X POST \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Content-Type: application/json" \ "https://api.fra.unikraft.cloud/v1/services//autoscale" \ -d '{ "policies": [ { "name": "create", "type": "create", "replace": true, "num_requests": 100 } ], ... }' ``` ### Idle policy The `idle` policy scales in (removes instances) when the service has been idle—receiving no requests—for a configurable period. ```bash title="unikraft" unikraft api /v1/services//autoscale -X POST --metro fra \ '{ "policies": [ { "name": "scale-in", "type": "idle", "idle_time_ms": 1000 } ], ... }' ``` ```bash title="API" curl -X POST \ -H "Authorization: Bearer ${UKC_TOKEN}" \ -H "Content-Type: application/json" \ "https://api.fra.unikraft.cloud/v1/services//autoscale" \ -d '{ "policies": [ { "name": "scale-in", "type": "idle", "idle_time_ms": 1000 } ], ... }' ``` ## Learn more * The [CLI reference](/docs/cli/unikraft). * Unikraft Cloud's [REST API reference](/api/platform/v1), and in particular the section on [autoscale](/api/platform/v1/autoscale). --- ## Document: Autokill URL: /docs/features/autokill # Autokill > **Availability** > > Available on Unikraft Cloud, dedicated hosts, BYOC and on-prem. > > Exposed by the API, the CLI and the SDKs. > > See [Hosting options](https://unikraft.com/docs/platform/hosting-options). Autokill automatically deletes instances, service groups, templates, and checkpoints when a configured condition triggers. It stops workloads from piling up after they finish and are no longer needed. You can configure autokill on: - [**Instances**](/platform/instances): killed after a time, or after reaching a request limit. - [**Instance templates**](/platform/instances#instance-templates): killed if nobody clones them within a specified time. - [**Service groups**](/platform/services): killed after the group has been empty for a specified time. - [**Checkpoints**](/features/checkpoints): killed if nobody restores them within a specified time. Set autokill with the `--autokill` flag on the `create` and `edit` commands of each resource. ## Instance autokill :::caution Autokill skips any instance that has a [delete lock](/platform/delete-locks) set. The autokill timer doesn't fire while a delete lock is active. ::: ### Kill after stopped duration Automatically kill an instance after it stops and remains stopped for a given time: ```bash unikraft instance create --metro fra \ --autokill time=1h ... ``` This example kills the instance 1 hour after it stops. ### Kill after a request count Kill an instance after it serves a specified request count: ```bash unikraft instance create --metro fra \ --autokill num-requests=1000 ... ``` ### Updating autokill on a stopped instance ```bash unikraft instance edit --autokill time=10s ``` The new value replaces the whole policy. To keep a request limit, pass it again, for example `--autokill time=10s,num-requests=1000`. ## Service group autokill Automatically kill a service group after it stays empty (no instances) for a given time: ```bash unikraft service create --metro fra \ --autokill time=5m ... ``` This example kills the service group 5 minutes after its last instance leaves. ## Instance template autokill :::info When you clone an instance from a template, the new instance inherits the autokill configuration. ::: Automatically kill an instance template if nobody clones it within a given time: ```bash unikraft instance template create my-instance \ --autokill time=24h ``` This example converts the existing instance `my-instance` into a template, in that instance's metro, and removes the template after 24 hours without a clone. ## Checkpoint autokill Automatically kill a [checkpoint](/features/checkpoints) if nobody restores it within a given time: ```bash unikraft instance checkpoint create my-instance \ --autokill time=1h ``` This example removes the checkpoint after 1 hour without a restore. ## Changing autokill Use the `edit` command of the resource to set or change its autokill policy: ```bash unikraft service edit --autokill time=10m unikraft instance template edit --autokill time=48h unikraft instance checkpoint edit --autokill time=2h ``` ## Listing and filtering The `autokill.time` and `autokill.num-requests` fields work like any other field. You can list, filter, sort, and inspect them: ```bash unikraft instance list --filter 'autokill.time>0s' --sort autokill.time ``` ## Learn more * Unikraft Cloud's [REST API reference](/api/platform/v1), in particular the sections on [instances](/api/platform/v1/instances) and [services](/api/platform/v1/servicegroups). --- ## Document: Annotations URL: /docs/features/annotations # Annotations > **Availability** > > Available on Unikraft Cloud, dedicated hosts, BYOC and on-prem. > > Exposed by the API, the CLI and the SDKs. > > See [Hosting options](https://unikraft.com/docs/platform/hosting-options). **Annotations** attach arbitrary key-value metadata to an instance. They use the same key syntax as [Kubernetes annotations](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/), so you can reuse your existing keys and tooling. Unlike [tags](/platform/tagging), which act as controller-level labels that only the platform sees, annotations also reach the guest. You can use them for three things: {/* vale off */} 1. **Custom metadata**—the instance's startdata includes its annotations, so the guest can read them at runtime. 2. **Guest network configuration**—the guest applies the [CNI result](#guest-network-configuration-with-cni) in the `unikraft.com/cni` annotation to its network interfaces. 3. **Structured log output**—you select which annotations the platform injects into the VM's console log output. {/* vale on */} {/* vale off */} :::caution Dedicated `unikraft` CLI subcommands for annotations are coming soon. In the meantime, drive annotations through the [API](/api/platform/v1/instances), which you can invoke with `curl` or the [`unikraft api`](/cli/unikraft/api) command. ::: {/* vale on */} ## Annotation keys An annotation is a set of key-value pairs. A value is any string without ASCII control characters (`0x00`–`0x1f` and `0x7f`, which covers tab, newline and carriage return), and multi-byte UTF-8 goes through fine. The API rejects a value carrying an ASCII control character with `400 Bad Request`. Keys follow the [Kubernetes annotation key syntax](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/#syntax-and-character-set): - A key can be a plain name (`my-key`) or carry an optional DNS prefix separated by a slash (`example.com/annotation2`). - Each key needs a name segment of at most 63 characters, containing alphanumerics, `-`, `_`, and `.`, and beginning and ending with an alphanumeric character. - The optional prefix is a DNS subdomain (a series of DNS labels joined by `.`, at most 253 characters) followed by a `/`. It can't be a wildcard domain. An instance holds at most 256 annotations. For example: ```json { "test.unikraft.com/my-annotation": "value3", "my-key": "value1", "example.com/annotation2": "value2" } ``` ## Creating an instance with annotations Add an `annotations` object to the instance body when you create it. Each entry is a key-value pair: ```bash unikraft api /v1/instances -X POST --metro fra \ '{ "name": "annotations-demo", "image": "/annotations-demo:latest", "annotations": { "test.unikraft.com/my-annotation": "value3", "my-key": "value1", "example.com/annotation2": "value2" } }' ``` Annotation keys are Kubernetes-compatible, so they can be plain keys (`my-key`) or prefixed with a domain (`example.com/annotation2`). ## Reading annotations from the guest The platform includes the instance's annotations in its **startdata**, which the guest reads from `/sys/class/uio/uio0/device/startdata`: ```bash cat /sys/class/uio/uio0/device/startdata ``` ```json { "ip": "10.0.0.5/30", "uuid": "d0e22bb8-b4c1-44ea-bb58-197fedbeb2ab", "annotations": { "test.unikraft.com/my-annotation": "value3", "my-key": "value1", "example.com/annotation2": "value2" }, "mac": "12:b0:0a:00:00:05", "gw": "10.0.0.6", "hostname": "annotations-demo" } ``` Your app parses this JSON and reads the `annotations` object like any other startdata field. ## Guest network configuration with CNI {/* vale off */} :::caution[**Limited Access**] CNI networking is only available in BYOC or on-prem Unikraft Cloud installations. It relies on TAP devices and networks that you set up on the host. To explore such a deal, reach out to the [Unikraft Cloud Discord](https://unikraft.com/discord) or send an email to [support@unikraft.com](mailto:support@unikraft.com). ::: {/* vale on */} `unikraft.com/cni` is a reserved annotation. Its value is a [CNI Result](https://www.cni.dev/docs/spec/#section-5-result-types) document, and the guest applies the addresses and routes it holds to its own network interfaces. This hands the addressing of an instance to an external [CNI](https://www.cni.dev/) plugin or IPAM system. The plugin decides the addresses and routes, writes its result into the annotation, and the guest configures itself from it. Your image needs no agent for it and no boot script of its own. The annotation pairs with an [interface without an address](/features/custom-network-configuration#interfaces-without-an-address), which reaches the guest as a bare link on a TAP device of your own. See [Kubernetes](/integrations/kubernetes#custom-networking-with-cni) for the CNI integration that Kraftlet drives on the cluster side. The annotation value is the document as a JSON string, so the JSON inside it carries escapes: ```bash unikraft api /v1/instances -X POST --metro fra \ '{ "name": "cni-demo", "image": "/cni-demo:latest", "network_interfaces": [ {}, { "name": "cni-demo-vpc", "tap_name": "vpc0", "mac": "0a:58:c0:a8:01:05", "autoconfig": false } ], "annotations": { "unikraft.com/cni": "{\"cniVersion\":\"1.0.0\",\"interfaces\":[{\"name\":\"eth0\",\"mac\":\"0a:58:c0:a8:01:05\"}],\"ips\":[{\"interface\":0,\"address\":\"192.168.1.5/24\"}]}" } }' ``` That instance keeps a pool-allocated primary interface and adds a second one on the `vpc0` TAP device, with a MAC address you pick and no IP address. Unescaped, the annotation holds a standard CNI Result (`types/100`): ```json { "cniVersion": "1.0.0", "interfaces": [ { "name": "eth0", "mac": "0a:58:c0:a8:01:05" } ], "ips": [ { "interface": 0, "address": "192.168.1.5/24" } ] } ``` The guest gives `192.168.1.5/24` to the link whose MAC address is `0a:58:c0:a8:01:05`, and brings that link up. ### Writing the document Give every address a MAC address to land on: - Each `ips[]` entry points through its `interface` index at an entry of the document's own `interfaces[]` array, not at the instance's `network_interfaces`. An entry with no `interface` field uses index `0`. - That `interfaces[]` entry supplies the MAC address of the interface to configure, so `interfaces[].name` can say anything. Without a MAC address to match, the index counts the instance's non-loopback interfaces from `0` instead. The entry carries no `mac`, the document holds no `interfaces[]` array, or the index sits past the end of that array. - Set the MAC address yourself when you create the instance, or read it from the [instance status](/api/platform/v1/instances#get-instance-by-uuid). - Addresses take CIDR notation, IPv4 or IPv6, and the prefix length is mandatory. - The document needs a `cniVersion` of `0.3.1`, `0.4.0`, `1.0.0`, or `1.1.0`, and has to be a result rather than a network configuration. - Keep the document on one line, because annotation values reject control characters. Only `interfaces[].mac`, `ips[].interface`, `ips[].address`, `ips[].gateway`, the [`routes`](#routes) array, and the `dns` object have any effect. `mtu` and `sandbox` do nothing, so you can pass a plugin's full result through untouched. The guest writes the `dns` object to its `/etc/resolv.conf` at boot, and again when the instance resumes from a snapshot. It takes up to three `nameservers`, six `search` domains, and eight `options`, and ignores further entries. When the object has both `domain` and `search`, `search` applies. A `dns` object without `nameservers` keeps the instance's own `nameserver` field, and so does a document without a `dns` object. An invalid `dns` object stops the instance at boot. Applying `dns` requires the latest base-compat guest kernel. The guest never configures IPv6 addresses from router advertisements, because it doesn't use stateless address autoconfiguration. It does take the default router and the maximum transmission unit from them, because some CNI plugins announce the IPv6 default route only in a router advertisement. This needs the latest base-compat guest kernel. The instance's own addresses and `gateway` field are IPv4 only, which leaves the document as the only source of IPv6 addresses. List an IPv6 [default route](#default-routes) in the document, announce one in a router advertisement, or configure one inside the guest, if the workload needs it. The guest also skips duplicate address detection, so its IPv6 link-local addresses and the addresses from the document are usable directly after boot. ### Routes The guest installs each entry of the document's `routes` array in its own routing table: ```json { "cniVersion": "1.0.0", "interfaces": [ { "name": "eth0", "mac": "0a:58:c0:a8:01:05" } ], "ips": [ { "interface": 0, "address": "192.168.1.5/24", "gateway": "192.168.1.1" } ], "routes": [ { "dst": "10.2.0.0/16", "gw": "192.168.1.254" }, { "dst": "10.3.0.0/16" } ] } ``` The guest sends traffic for `10.2.0.0/16` through `192.168.1.254`, and traffic for `10.3.0.0/16` through `192.168.1.1`, the `gateway` of the matching `ips` entry. Each route accepts the following fields: | Field | Required | Description | |-------|----------|-------------| | `dst` | Yes | The destination in CIDR notation, IPv4 or IPv6, such as `0.0.0.0/0` for a default route. | | `gw` | No | The next-hop address in the same IP family as `dst`, with the fallback described under [picking the next hop](#picking-the-next-hop). | | `priority` | No | The route metric, where a lower value wins. | | `table` | No | The numeric ID of the routing table to add the route to, which defaults to the main table. | | `scope` | No | The numeric route scope, such as `0` for global, `253` for link, and `254` for host, which defaults to `0` for a route with a next hop and `253` for a route without one. | The guest ignores `mtu`, `advmss`, and any other route field. A document holds at most 16 routes. #### Picking the next hop A route without a `gw` uses the `gateway` of the matching `ips` entry, which is the first entry with an address of the route's IP family and a `gateway` of its own. When no entry has a gateway, the route has no next hop, and the guest sends its traffic straight onto the link. Routes carry no interface of their own, so the guest picks one: - The interface whose address prefix contains the route's next hop. - Otherwise, the interface of the first `ips` entry with an address of the route's IP family. Every route needs at least one address of its own IP family in the document. #### Default routes A `gateway` in `ips` creates no route on its own. List the default route explicitly, as `{ "dst": "0.0.0.0/0" }` for IPv4 or `{ "dst": "::/0" }` for IPv6, and the guest uses that gateway as its [next hop](#picking-the-next-hop). #### Route order The guest adds routes after it configures the addresses, in the order of the `routes` array. List a route that makes a gateway reachable ahead of the routes that use that gateway. #### Routing tables A route with a `table` other than the main table only takes effect once a policy routing rule points traffic at that table. The guest adds the route but never adds such a rule, so the operator may need to add one inside the guest, for example with `ip rule add from 192.168.1.5 lookup 100`. ### Changing the configuration The guest applies the document at boot, and again whenever the instance resumes from a [snapshot](/features/snapshots), for example after [scale-to-zero](/features/scale-to-zero). A [patched](#patching-annotations) annotation takes effect once the instance stops, so the guest reads the new document on its next boot or resume. On a resume, the guest drops the addresses and routes it added that the document no longer lists, and adds the new ones, without a reboot. The guest never removes an address or a route it didn't add itself. ### When the document fails At boot, a document that fails to parse or apply stops the instance. That covers invalid JSON, a MAC address that matches no interface, a route with no address of its IP family, and a route that the guest kernel rejects. When the instance resumes from a snapshot, the same failure leaves the instance running and only logs it. Check that each MAC address in the document matches one on the instance, and that each route has an address to use. Then read the [instance logs](/platform/instances#instance-logs), which name any entry that fails to parse or apply. ## Patching annotations Update the annotations on an existing instance with a [`PATCH /instances`](/api/platform/v1/instances) request. The body is an array of patch operations, each naming the target instance, the `annotations` property, and one of three operations: `set`, `add`, or `del`. :::note Annotation changes apply only while the instance is in the `stopped` state. If you patch a running instance, the platform accepts the request and queues the change, then applies it the next time the instance stops. ::: ### Replace all annotations (`set`) `set` replaces all existing annotations with the object you provide: ```bash unikraft api /v1/instances -X PATCH --metro fra \ '[{ "name": "annotations-demo", "prop": "annotations", "op": "set", "value": { "env": "production", "team": "platform" } }]' ``` ### Add or update annotations (`add`) `add` merges the given annotations into the existing set. It overwrites keys that already exist and leaves the other keys untouched: ```bash unikraft api /v1/instances -X PATCH --metro fra \ '[{ "name": "annotations-demo", "prop": "annotations", "op": "add", "value": { "added-key": "added-value" } }]' ``` ### Delete annotations (`del`) `del` removes specific keys. The value is an array of key names (or a single key as a string): ```bash unikraft api /v1/instances -X PATCH --metro fra \ '[{ "name": "annotations-demo", "prop": "annotations", "op": "del", "value": ["my-key", "example.com/annotation2"] }]' ``` ## Annotations in log output Beyond the guest, the platform can inject selected annotations into a VM's **console log output**. This helps with structured logging to an external collector. This forwarding happens at the node level rather than through the instances API, so coordinate with Unikraft to enable it for your deployment. The platform exposes two options: ```bash --vmm-console-ports "app:socket/json:/tmp/vector.sock,app2:socket/json:/tmp/vector.sock" --vmm-console-annotations "app:my-key+test.unikraft.com/my-annotation,app2:*" ``` - `--vmm-console-ports` defines named console ports as `:[/][:]`. Here `app` and `app2` are `socket` ports using the `json` format, each writing to a Vector socket. - `--vmm-console-annotations` selects which annotations appear in each port's output, as `:+,...` or `:*`: - `app:my-key+test.unikraft.com/my-annotation`—forward only the listed annotation keys (joined with `+`) to port `app`. - `app2:*`—the `*` wildcard forwards all annotations to port `app2`. When the guest writes to the app console (for example, `echo "blub" > /dev/vport2p1`), the collector receives the message together with the selected annotations: ```json { "annotations": { "my-key": "value1", "test.unikraft.com/my-annotation": "value3" }, "host": "(unnamed)", "message": "blub", "name": "app", "source_type": "socket", "timestamp": "2026-05-08T08:35:26.489812959Z", "uuid": "9d262ffd-878f-47f3-98c3-bb017c11d690" } ``` ## Annotation events The platform's event log carries a `vm.annotate` event, which it emits whenever an instance's annotations change: ```json { "type": "vm.annotate", "timestamp": "2026-08-19T10:04:11Z", "data": { "vm": "b495f451-0370-4d8e-8b94-a5b22d2212f9", "annotations": { "env": "prod", "team": "search" } } } ``` An external system consumes the event to keep track of the instance's annotation values, without polling the instance API. The event also gives log processing a way to map an instance UUID back to its annotations. The [audit event stream](/platform/audit-events) doesn't carry `vm.annotate`, so read it from the node event log. ## Limitations - An instance holds at most 256 annotations. - Annotation keys must follow the [Kubernetes key syntax](#annotation-keys), and values are strings with no ASCII control characters in them. - A [patched](#patching-annotations) annotation change applies once the instance reaches the `stopped` state. - A [`unikraft.com/cni`](#guest-network-configuration-with-cni) document holds at most eight interfaces, 16 addresses, and 16 routes, and an `ips[].interface` index stays below eight. - The guest applies only `interfaces[].mac`, `ips[].interface`, `ips[].address`, `ips[].gateway`, and `routes` from that document. - Applying `routes` requires the latest base-compat guest kernel. An instance that resumes from a [snapshot](/features/snapshots) keeps the kernel that the snapshot captured, and applies routes only after it cold-boots on the latest kernel. - Dedicated `unikraft` CLI subcommands aren't available yet, so drive annotations through the [API](/api/platform/v1/instances). - The platform sets up [log-output](#annotations-in-log-output) forwarding at the node level, not through the instances API. - The [`vm.annotate` event](#annotation-events) needs switching on in the node's event configuration. ## Learn more * [Tags](/platform/tagging): controller-level labels for organizing platform resources. * [Custom network configuration](/features/custom-network-configuration): bring your own interfaces, MAC addresses and TAP devices, including [interfaces without an address](/features/custom-network-configuration#interfaces-without-an-address) for a CNI plugin to fill in. * [Kubernetes](/integrations/kubernetes#custom-networking-with-cni): how Kraftlet runs the CNI plugins of your cluster for Unikraft Cloud instances. * [Instances](/platform/instances): create and manage instances. * Unikraft Cloud's [REST API reference](/api/platform/v1), in particular the section on [instances](/api/platform/v1/instances). --- ## Document: Wordpress with Nginx and MariaDB URL: /docs/guides/wordpress # Wordpress with Nginx and MariaDB import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} This guide explains how to create and deploy a Wordpress app with Nginx and MariaDB. To run this example, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/wordpress` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/wordpress/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` ## Create Volumes Create the volumes for the Wordpress and MariaDB data: ```bash unikraft volume create --metro fra --name wordpress-wordpress-data --size 512M unikraft volume create --metro fra --name wordpress-db-data --size 512M ``` You can list the created volumes by running: ```bash unikraft volume list ``` ```ansi METRO NAME STATE SIZE CREATED fra wordpress-db-data available 512MiB just now fra wordpress-wordpress-data available 512MiB just now ``` ## Deploy MariaDB Build and deploy the MariaDB instance. MariaDB is an internal service (not publicly accessible), reached via the `wordpress-mariadb.internal` domain: ```bash unikraft build ./mariadb --output /mariadb:latest unikraft run --metro fra \ --name mariadb \ -m 1G \ --scale-to-zero policy=idle,cooldown-time=1000,stateful=true \ --domain wordpress-mariadb.internal \ --env MARIADB_ROOT_PASSWORD=unikraft \ --volume wordpress-db-data:/var/lib/mysql \ --image /mariadb:latest ``` Make sure to replace `` with your username / org-name. The output shows the instance details: ```ansi metro: fra name: mariadb uuid: 3af0eefb-29a8-4634-9b94-abf62d2fb90a state: starting image: /mariadb runtime:  env: MARIADB_ROOT_PASSWORD: * resources:  memory: 1GiB  vcpus: 1 service:  name: snowy-glitter-3uylzbqk  uuid: 0b3303d0-1a2e-4353-a9c7-29151263ef9f  domains:  - fqdn: wordpress-mariadb.internal volumes: - name: wordpress-db-data  uuid: 7b232d0f-dfbf-4ff7-9208-106b5d92bbe3  at: /var/lib/mysql networks: - uuid: 8cad0682-a700-4303-af8c-ebd18465ed32  private-ip: 10.0.1.73  mac: 12:b0:0a:00:01:49 timestamps:  created: just now scale-to-zero:  enabled: true  policy: idle  stateful: true  cooldown-time: 1s ``` ## Deploy Wordpress Build and deploy the Wordpress instance. Set `WORDPRESS_DB_HOST` to the same internal domain you assigned to the MariaDB instance (`wordpress-mariadb.internal` in this guide). ```bash unikraft build ./wordpress --output /wordpress:latest unikraft run --metro fra \ --name wordpress \ -m 2G \ -p 443:8080/tls+http \ --scale-to-zero policy=on,cooldown-time=1000 \ --env WORDPRESS_DB_HOST=wordpress-mariadb.internal \ --volume wordpress-wordpress-data:/var/www/html \ --image /wordpress:latest ``` The output shows the instance address and other details: ```ansi metro: fra name: wordpress uuid: ee9f5599-b33b-44f1-ab57-3871b440e810 state: starting image: /wordpress runtime:  env: WORDPRESS_DB_HOST: wordpress-mariadb.internal resources:  memory: 2GiB  vcpus: 1 service:  name: damp-lake-n09gzguc  uuid: 7f21d778-487f-4130-a94a-34b86862c3dd  domains:  - fqdn: damp-lake-n09gzguc.fra.unikraft.app volumes: - name: wordpress-wordpress-data  uuid: 0bec8b93-0691-43b6-b188-4ac170a3d0c7  at: /var/www/html networks: - uuid: e0f13623-fd04-4a69-9caf-47142ce47c4c  private-ip: 10.0.0.33  mac: 12:b0:0a:00:00:21 timestamps:  created: just now scale-to-zero:  enabled: true  policy: on  cooldown-time: 1s ``` Use a browser to access the install page of Wordpress using the URL from the `fqdn` field in the output. Fill out the form and complete the Wordpress install. You can list information about the instances by running: ```bash unikraft instance list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra mariadb standby /mariadb 1GiB 1 wordpress-mariadb.internal 5 minutes ago fra wordpress running /wordpress 2GiB 1 damp-lake-n09gzguc.fra.unikraft.app 1 minute ago ``` When done, you can remove the instances and volumes: ```bash unikraft instances delete mariadb wordpress unikraft volume delete wordpress-wordpress-data wordpress-db-data ``` ## Learn more Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: Wordpress URL: /docs/guides/wordpress-all-in-one # Wordpress import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} This guide shows you how to use [Wordpress](https://wordpress.com/), a web content management system. To run it, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/wordpress-all-in-one/` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/wordpress-all-in-one/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` When done, invoke the following command to deploy this app on Unikraft Cloud: ```bash unikraft build . --output /wordpress-all-in-one:latest unikraft run --metro fra \ -m 4G \ -p 443:3000/tls+http \ --scale-to-zero policy=on,cooldown-time=3000,stateful=true \ --image /wordpress-all-in-one:latest ``` The output shows the instance address and other details: ```ansi metro: fra name: wordpress-fx5rb uuid: bfb9d151-1604-452a-b2e0-f737486744df state: starting image: /wordpress resources:  memory: 4096MiB  vcpus: 1 service:  uuid: 398fe5bb-e172-465e-8f74-56ffcfb24a3d  name: cool-silence-h5c1es4z  domains:  - fqdn: cool-silence-h5c1es4z.fra.unikraft.app networks: - uuid: 26c200e0-43eb-dd46-e4be-e9505ff677d1  private-ip: 10.0.3.1  mac: 12:b0:4e:20:b3:e7 timestamps:  created: just now ``` In this case, the instance name is `wordpress-fx5rb`. They're different for each run. Use a browser to access the install page of Wordpress. Fill out the form and complete the Wordpress install. You can list information about the instance by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra wordpress-fx5rb running /wordpress-all-in-one 4096MiB 1 cool-silence-h5c1es4z.fra.unikraft.app 1 minute ago ``` When done, you can remove the instance: ```bash unikraft instances delete wordpress-fx5rb ``` ## Learn more Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: Wazero URL: /docs/guides/wazero-import-go # Wazero import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} This example comes from [Wazero's "import go" example](https://github.com/tetratelabs/wazero/tree/main/examples/import-go) and shows how to define, import and call a wasm blob from Go and run it on Unikraft Cloud. To run this example, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/wazero-import-go/` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/wazero-import-go/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` When done, invoke the following command to deploy this app on Unikraft Cloud: ```bash unikraft build . --output /wazero-import-go:latest unikraft run --metro fra \ -m 512M \ -p 443:8080/tls+http \ --scale-to-zero policy=on,cooldown-time=1000 \ --image /wazero-import-go:latest ``` The output shows the instance address and other details: ```ansi metro: fra name: wazero-import-go-r4dx8 uuid: a763e1c3-bb38-475f-95b6-1e78d8ca74fc state: starting image: /wazero-import-go resources:  memory: 512MiB  vcpus: 1 service:  uuid: f38a171d-e283-24ca-1158-c7907948071e  name: cool-morning-camrrhsa  domains:  - fqdn: cool-morning-camrrhsa.fra.unikraft.app networks: - uuid: bc4bb64c-8185-3b83-8693-b1464ab4723e  private-ip: 10.0.6.7  mac: 12:b0:05:55:02:fe timestamps:  created: just now ``` In this case, the instance name is `wazero-import-go-r4dx8` and the address is `https://cool-morning-camrrhsa.fra.unikraft.app`. They're different for each run. Use `curl` to query the Unikraft Cloud instance of the Go/wazero server: ```bash curl https://cool-morning-camrrhsa.fra.unikraft.app ``` ```text println >> 24 log_i32 >> 24 ``` You can list information about the instance by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra wazero-import-go-r4dx8 running /wazero-import-go 512MiB 1 cool-morning-camrrhsa.fra.unikraft.app 2 minutes ago ``` When done, you can remove the instance: ```bash unikraft instances delete wazero-import-go-r4dx8 ``` ## Background WebAssembly has neither a mechanism to get the current year, nor one to print to the console, so this example defines these in Go. Like Go, WebAssembly functions are namespaced into modules instead of packages. With Go only exported functions can import into another module. `age-calculator.go` shows how to export functions using [HostModuleBuilder](https://pkg.go.dev/github.com/tetratelabs/wazero#HostModuleBuilder) and how a WebAssembly module defined in its [text format](https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#text-format%E2%91%A0) imports it. This only uses the text format for demonstration purposes, to show you what's going on. It's likely, you will use another language to compile a Wasm (WebAssembly Module) binary, such as TinyGo. Regardless of how wasm produces, the export/import mechanics are the same! ### Using WASI WebAssembly System Interface (WASI) is a modular system interface for WebAssembly. This uses an ad-hoc Go-defined function to print to the console. An emerging specification standardizes system calls (like Go's [x/sys](https://pkg.go.dev/golang.org/x/sys/unix)) called WebAssembly System Interface [(WASI)](https://github.com/WebAssembly/WASI). While this isn't yet a W3C standard, wazero includes a [wasi package](https://pkg.go.dev/github.com/tetratelabs/wazero/wasi). ## Customize your app To customize the app, update the files in the repository, listed below: * `agecalculator.go`: The Go web server that calls the WASM/Wazero blog for the age calculation * `Kraftfile`: the Unikraft Cloud specification * `Dockerfile`: the Docker-specified app filesystem ## Learn more Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: vsftpd URL: /docs/guides/vsftpd # vsftpd import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} This guide explains how to create and deploy a [vsftpd](https://security.appspot.com/vsftpd.html) app, to secure access to the files of your VM. To run this example, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/vsftpd` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/vsftpd/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` When done, invoke the following command to deploy this app on Unikraft Cloud: ```bash unikraft volume create --set metro=fra --set name=vsftpd-workspace --set size=1G unikraft build . --output /vsftpd:latest unikraft run --metro fra \ -m 1G \ -p 20:20/tls \ -p 21:21/tls \ -p 222:22/tls \ -p 990:990/tls \ -p 10100:10100/tls \ --scale-to-zero policy=on,cooldown-time=40000,stateful=true \ --volume vsftpd-workspace:/root \ --image /vsftpd:latest ``` The output shows the instance address and other details: ```ansi metro: fra name: vsftpd uuid: 186a46a0-7c89-4bfd-83a8-649bcc60a96e state: starting image: /vsftpd resources:  memory: 1024MiB  vcpus: 1 service:  uuid: 4814b43a-c1d3-48f0-ef3e-9dba8bcaba25  name: broken-orangutan-jypu2z53  domains:  - fqdn: broken-orangutan-jypu2z53.fra.unikraft.app networks: - uuid: 6adc6c29-5c9b-e472-70ff-fc3f3816d5a2  private-ip: 10.0.0.109  mac: 12:b0:17:ff:e4:c7 timestamps:  created: just now ``` This will create a volume for data persistence, and mount it at `/root` inside the VM. In this case, the instance name is `vsftpd` and the address is `https://broken-orangutan-jypu2z53.fra.unikraft.app`. The name was preset, but the address is different for each run. **Note**: The `root` password defaults to `rootpass`. Don't forget to change it inside the `Dockerfile` and update the commands below. You can access the FTP server using a client like `lftp`: ```bash lftp -u root,rootpass ftps://broken-orangutan-jypu2z53.fra.unikraft.app:21 lftp root@broken-orangutan-jypu2z53.fra.unikraft.app:~> ls ``` You can list information about the volume by running: ```bash unikraft volumes list ``` ```ansi METRO NAME STATE SIZE CREATED fra vsftpd-workspace mounted 1.0GiB 9 minutes ago ``` You can list information about the instance by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra vsftpd standby /vsftpd 1.0GiB 1 broken-orangutan-jypu2z53.fra.unikraf… 2 minutes ago ``` When done, you can remove the instance: ```bash unikraft instances delete vsftpd ``` The volume isn't removed by default, so you can recreate the instance and still have access to your old data. Remove it using: ```bash unikraft volume delete vsftpd-workspace ``` ## Learn more Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: Remote IDEs: Visual Studio Code Server URL: /docs/guides/visual-studio-code-server # Remote IDEs: Visual Studio Code Server import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} Remote IDEs give developers a full development environment in the cloud, reachable from a browser, which suits cloud-native development, collaboration, and education. Editing sessions are bursty, so an IDE that scales to zero when idle and resumes in milliseconds bills for coding time rather than for the whole day. [Visual Studio Code](https://code.visualstudio.com/) is a source-code editor developed by Microsoft. It includes support for debugging, syntax highlighting, intelligent code completion, snippets, code refactoring, and embedded Git. It features a [Code server](https://code.visualstudio.com/docs/remote/vscode-server), which allows you to run Visual Studio Code remotely and access it through a web browser or your local Visual Studio Code client. This guide explains how to create and deploy a Visual Studio Code server app. To run it, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/visual-studio-code-server` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/visual-studio-code-server/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` When done, invoke the following command to deploy this app on Unikraft Cloud: ```bash unikraft volume create --set metro=fra --set name=code-workspace --set size=1G unikraft build . --output /visual-studio-code-server:latest unikraft run --metro fra \ -m 2G \ -p 443:8443/tls+http \ --scale-to-zero policy=on,cooldown-time=4000,stateful=true \ -e PGUID=0 \ -e PGID=0 \ -e PASSWORD=unikraft \ -e SUDO_PASSWORD=unikraft \ -e DEFAULT_WORKSPACE="/workspace" \ --volume code-workspace:/workspace \ --image /visual-studio-code-server:latest ``` The output shows the instance address and other details: ```ansi metro: fra name: code-server-6gxsp uuid: c1a619a0-e222-4042-94b8-ba4b39353417 state: starting image: /visual-studio-code-server resources:  memory: 2048MiB  vcpus: 1 service:  uuid: 4a0866e4-3bec-3666-c4aa-61672de542e2  name: blue-shape-chmxf1g4  domains:  - fqdn: blue-shape-chmxf1g4.fra.unikraft.app networks: - uuid: dcec209f-31a6-b355-a88e-f5ac3edfa20e  private-ip: 10.0.0.49  mac: 12:b0:ec:d2:df:1c timestamps:  created: just now ``` This will create a volume for data persistence, and mount it at `/workspace` inside the VM. In this case, the instance name is `code-server-6gxsp` and the address is `https://blue-shape-chmxf1g4.fra.unikraft.app`. They are different for each run. Enter the provided address into your browser of choice to access the Code server instance. You can list information about the volume by running: ```bash unikraft volumes list ``` ```ansi METRO NAME STATE SIZE CREATED fra code-workspace mounted 1.0GiB 13 minutes ago ``` You can list information about the instance by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra code-server-6gxsp standby /visual-studio-code-server 2.0GiB 1 blue-shape-chmxf1g4.fra.unikraft.app 2 minutes ago ``` When done, you can remove the instance: ```bash unikraft instances delete code-server-6gxsp ``` The volume isn't removed by default, so you can recreate the instance and still have access to your old data. Remove it using: ```bash unikraft volume delete code-workspace ``` ## Learn more Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: API Gateways: Tyk URL: /docs/guides/tyk # API Gateways: Tyk import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} An API gateway is the entry point to your services, handling authentication, routing, rate-limiting, and observability at the edge of your infrastructure. API traffic fluctuates, so a gateway that scales to zero when idle and starts in milliseconds when traffic returns keeps your API edge ready without paying for it overnight. This example uses [`Tyk`](https://tyk.io/), an API gateway and management platform. Tyk is used together with Redis to store API tokens and OAuth clients. To run this example, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/tyk/` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/tyk/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` ## Redis The `REDIS_PASSWORD` environment variable sets the Redis `requirepass` directive. If not provided, it defaults to `unikraft`. Build and deploy the Redis instance (used internally by Tyk): ```bash unikraft build ./redis --output /redis:latest unikraft run --metro fra \ -m 256M \ --scale-to-zero policy=idle,cooldown-time=1000,stateful=true \ --domain tyk-redis.internal \ -e REDIS_PASSWORD=unikraft \ --image /redis:latest ``` Make sure to replace `` with your username / org-name in the unikraft CLI commands above. The output shows the Redis instance details: ```ansi metro: fra name: redis-6vgvc uuid: 63b86d17-06ca-4f95-b921-56e5b3245554 state: starting image: /redis resources:  memory: 256MiB  vcpus: 1 service:  name: snowy-water-wivk1i4r  uuid: 6612de91-d639-4b5a-95d1-b7ae6e91e3c1  domains:  - fqdn: tyk-redis.internal networks: - uuid: 19e4b80a-9501-448d-99a6-ec3f7b90805e  private-ip: 10.0.0.29  mac: 12:b0:0a:00:01:49 timestamps:  created: just now scale-to-zero:  enabled: true  policy: idle  stateful: true  cooldown-time: 1s ``` ## Tyk Build and deploy the Tyk instance. Set `TYK_GW_STORAGE_HOST` to the same internal domain you assigned to the Redis instance (`tyk-redis.internal` in this guide). If `TYK_GW_STORAGE_HOST` is unset, Tyk tries to connect to a Redis instance at `tyk-redis.internal` by default. ```bash unikraft build ./tyk --output /tyk:latest unikraft run --metro fra \ -m 256M \ -p 443:8080/tls+http \ --scale-to-zero policy=on,cooldown-time=1000 \ -e TYK_GW_STORAGE_PASSWORD=unikraft \ -e TYK_GW_STORAGE_HOST=tyk-redis.internal \ --image /tyk:latest ``` Make sure to replace `` with your username / org-name in the unikraft CLI commands above. The output shows the Tyk instance details: ```ansi metro: fra name: tyk-s9ixd uuid: 4e8a5e56-2d0b-4ca4-88b4-aa816129a66d state: starting image: /tyk resources:  memory: 256MiB  vcpus: 1 service:  name: icy-haze-8ph4u8cz  uuid: 89079646-353a-4a19-99ac-5498c7d626ad  domains:  - fqdn: icy-haze-8ph4u8cz.fra.unikraft.app networks: - uuid: 8085804f-0fe0-4847-ad6e-8518edba126e  private-ip: 10.0.0.1  mac: 12:b0:0a:00:0e:b1 timestamps:  created: just now ``` In this case, the instance names are `redis-6vgvc` and `tyk-s9ixd`, and the Tyk address is `https://icy-haze-8ph4u8cz.fra.unikraft.app`. They're different for each run. Use `curl` to query the Tyk instance on Unikraft Cloud: ```bash curl https://icy-haze-8ph4u8cz.fra.unikraft.app/hello ``` ```text {"status":"pass","version":"v5.3.0-dev","description":"Tyk GW","details":{"redis":{"status":"pass","componentType":"datastore","time":"2026-05-25T12:26:07Z"}}} ``` You can list information about the instances by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE MEMORY VCPUS FQDN CREATED fra tyk-s9ixd standby /tyk 256MiB 1 icy-haze-8ph4u8cz.fra.unikraft.app just now fra redis-6vgvc running /redis 256MiB 1 tyk-redis.internal 1 minute ago ``` When done, you can remove the instances: ```bash unikraft instances delete redis-6vgvc tyk-s9ixd ``` ## Customize your app To customize the Tyk app, you can update: * `Kraftfile`: the Unikraft Cloud specification * `Dockerfile` / `rootfs/`: the Tyk filesystem (in this case the configuration file `/etc/tyk.conf`) It's unlikely you will have to update the `Kraftfile` specification. Update the contents of the `rootfs/etc/tyk.conf` file for a different configuration. You can also update the `Dockerfile` in order to extend the Tyk filesystem with extra data files or configuration files. ## Learn more Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: Traefik URL: /docs/guides/traefik # Traefik import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} This example uses the [`Traefik`](https://traefik.io/traefik/) cloud native app proxy. To run it, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/traefik/` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/traefik/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` When done, invoke the following command to deploy this app on Unikraft Cloud: ```bash unikraft build . --output /traefik:latest unikraft run --metro fra \ -m 1G \ -p 443:80/tls+http \ -p 8080:8080/tls \ --scale-to-zero policy=on,cooldown-time=1000,stateful=true \ --image /traefik:latest ``` The output shows the instance address and other details: ```ansi metro: fra name: traefik-wqe7e uuid: 69d25b0b-1813-4a3f-88e6-64abbc78b359 state: starting image: /traefik resources:  memory: 1024MiB  vcpus: 1 service:  uuid: ec7570da-5700-01b5-aaa1-0734498c11eb  name: holy-cherry-rye39b1x  domains:  - fqdn: holy-cherry-rye39b1x.fra.unikraft.app networks: - uuid: b59f4362-dc72-7efe-477b-1efe227e1b08  private-ip: 10.0.28.16  mac: 12:b0:31:58:d7:d0 timestamps:  created: just now ``` In this case, the instance name is `traefik-wqe7e` and the address is `https://holy-cherry-rye39b1x.fra.unikraft.app`. They're different for each run. Use `curl` to query the Unikraft Cloud instance of Traefik. ```bash curl https://holy-cherry-rye39b1x.fra.unikraft.app:8080/dashboard ``` ```text Traefik ... ``` Or better yet, point a browser at the dashboard. :::danger This set up exposes the dashboard on port 8080 without authentication. Please change default.toml as needed. ::: You can list information about the instance by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra traefik-wqe7e running /traefik 1024MiB 1 holy-cherry-rye39b1x.fra.unikraft.app 2 minutes ago ``` When done, you can remove the instance: ```bash unikraft instances delete traefik-wqe7e ``` ## Customize your app To customize Traefik app you can change the `default.toml` configuration file. ## Learn more Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: Spin URL: /docs/guides/spin-wagi-http # Spin import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} This guide explains how to create and deploy a simple Spin HTTP app. This guide comes from [Spin's `spin-wagi-http` example](https://github.com/fermyon/spin/tree/v2.1.0/examples/spin-wagi-http). It shows how to run a Spin app serving routes from two programs written in different languages (Rust and C++). To run this example, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/spin-wagi-http/` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/spin-wagi-http/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` When done, invoke the following command to deploy this app on Unikraft Cloud: ```bash unikraft build . --output /spin-wagi-http:latest unikraft run --metro fra \ -m 4G \ -p 443:3000/tls+http \ --scale-to-zero policy=on,cooldown-time=1000,stateful=true \ --image /spin-wagi-http:latest ``` The output shows the instance address and other details: ```ansi metro: fra name: spin-wagi-http-is72r uuid: 045c1bda-0f2e-4f8b-98c7-a208bfa7d143 state: starting image: /spin-wagi-http resources:  memory: 4096MiB  vcpus: 1 service:  uuid: 9b5b88fd-16ce-3db6-a828-ee885647d820  name: damp-bobo-wg43p36e  domains:  - fqdn: damp-bobo-wg43p36e.fra.unikraft.app networks: - uuid: db3851f6-ace1-8601-b6fa-925b7fdf8390  private-ip: 10.0.28.16  mac: 12:b0:fc:f5:09:d5 timestamps:  created: just now ``` In this case, the instance name is `spin-wagi-http-is72r` and the address is `https://damp-bobo-wg43p36e.fra.unikraft.app`. They're different for each run. Then `curl` the hello route: ```bash curl -i https://damp-bobo-wg43p36e.fra.unikraft.app/hello Hello, Fermyon! ``` And `curl` the goodbye route: ```bash curl -i https://damp-bobo-wg43p36e.fra.unikraft.app/goodbye Goodbye, Fermyon! ``` You can list information about the instance by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra spin-wagi-http-is72r running /spin-wagi-http 4.0GiB 1 damp-bobo-wg43p36e.fra.unikraft.app 2 minutes ago ``` When done, you can remove the instance: ```bash unikraft instances delete spin-wagi-http-is72r ``` ## Customize your app To customize the app, update the files in the repository, listed below: * `wagi-http-cpp`: C++ server handling the hello route * `http-rust`: Rust server handling the goodbye route * `Kraftfile`: the Unikraft Cloud specification * `Dockerfile`: the Docker-specified app filesystem * `spin.toml`: The Spin TOML configuration file Lines in the `Kraftfile` have the following roles: * `spec: v0.7`: The current `Kraftfile` specification version is `0.7`. * `runtime: base-compat:latest`: The runtime kernel to use is the base compatibility kernel. * `rootfs`: Build the app root filesystem. `source: ./Dockerfile` means the filesystem is built using the `Dockerfile`. `format: erofs` means the filesystem type is [EROFS](https://erofs.docs.kernel.org/). * `cmd: ["/usr/bin/spin", "up", "--from", "/app/spin.toml", "--listen", "0.0.0.0:3000"]`: Use `spin` as the command to start the app, with the given parameters. The following options are available for customizing the app: * If only updating the existing files under the `wagi-http-cpp` and `http-rust` directories, you don't need to make any other changes. * If you create any new source files, copy them into the app filesystem by using the `COPY` command in the `Dockerfile`. * More extensive changes may require extending the `Dockerfile` ([see `Dockerfile` syntax reference](https://docs.docker.com/engine/reference/builder/)). ## Learn more Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: Skipper URL: /docs/guides/skipper0.18 # Skipper import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} This example uses [`Skipper`](https://opensource.zalando.com/skipper/), an HTTP router and reverse proxy for service composition To run this example, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/skipper0.18/` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/skipper0.18/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` When done, invoke the following command to deploy this app on Unikraft Cloud: ```bash unikraft build . --output /skipper018:latest unikraft run --metro fra \ -m 256M \ -p 443:9090/tls+http \ --scale-to-zero policy=on,cooldown-time=1000,stateful=true \ --image /skipper018:latest ``` The output shows the instance address and other details: ```ansi metro: fra name: skipper018-mx4ai uuid: 34e3d740-c2b0-4644-b7e1-647350f688dc state: starting image: /skipper018 resources:  memory: 256MiB  vcpus: 1 service:  uuid: b32c9035-d669-79fa-9955-9ad52cd1fcb4  name: aged-sea-o7d3c42s  domains:  - fqdn: aged-sea-o7d3c42s.fra.unikraft.app networks: - uuid: 70cfb329-9ab3-fc8c-aff9-a3bbbbeb70f3  private-ip: 10.0.6.4  mac: 12:b0:32:1b:02:7b timestamps:  created: just now ``` In this case, the instance name is `skipper018-mx4ai` and the address is `https://aged-sea-o7d3c42s.fra.unikraft.app`. They're different for each run. Use `curl` to query the Unikraft Cloud instance of Skipper. ```bash curl https://aged-sea-o7d3c42s.fra.unikraft.app ``` ```text Hello, world from Skipper on Unikraft! ``` You can list information about the instance by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra skipper018-mx4ai running /skipper018 256MiB 1 aged-sea-o7d3c42s.fra.unikraft.app 2 minutes ago ``` When done, you can remove the instance: ```bash unikraft instances delete skipper018-mx4ai ``` ## Customize your app To customize Skipper you can change the `example.eskip` configuration file. ## Learn more Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: Ruby on Rails URL: /docs/guides/ruby3.2-rails # Ruby on Rails import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} This guide explains how to create and deploy a [Ruby on Rails](https://rubyonrails.org/) app. To run this example, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/ruby3.2-rails/` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/ruby3.2-rails/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` When done, invoke the following command to deploy this app on Unikraft Cloud: ```bash unikraft build . --output /ruby32-rails:latest unikraft run --metro fra \ -m 1G \ -p 443:3000/tls+http \ --scale-to-zero policy=idle,cooldown-time=1000,stateful=true \ -e GEM_HOME=/usr/local/bundle \ -e BUNDLE_APP_CONFIG=/usr/local/bundle \ --image /ruby32-rails:latest ``` The output shows the instance address and other details: ```ansi metro: fra name: ruby32-rails-apa93 uuid: 2f85b9db-94f8-45d2-8e38-ed9b56cb8695 state: starting image: /ruby32-rails resources:  memory: 1024MiB  vcpus: 1 service:  uuid: 42265dab-df44-6d66-0075-fc07455178c8  name: aged-waterfall-qraz0s7d  domains:  - fqdn: aged-waterfall-qraz0s7d.fra.unikraft.app networks: - uuid: 7ae3414b-1d87-c831-d26a-506a4cf9ac71  private-ip: 10.0.3.3  mac: 12:b0:39:3e:b5:36 timestamps:  created: just now ``` In this case, the instance name is `ruby32-rails-apa93` and the address is `https://aged-waterfall-qraz0s7d.fra.unikraft.app`. They're different for each run. Use `curl` to query the Unikraft Cloud instance of the Python-based HTTP web server: ```bash curl https://aged-waterfall-qraz0s7d.fra.unikraft.app/hello ``` ```text [...]

Hello World

Hello, World! [...] ``` You can list information about the instance by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra ruby32-rails-apa93 running /ruby32-rails 1.0GiB 1 aged-waterfall-qraz0s7d.fra.unikraft.app 2 minutes ago ``` When done, you can remove the instance: ```bash unikraft instances delete ruby32-rails-apa93 ``` ## Customize your app To customize the app, update the files in the repository, listed below: * `app/` and `config/`: the contents to update the Rails setup * `Kraftfile`: the Unikraft Cloud specification * `Dockerfile`: the Docker-specified app filesystem The `app/` and `config/` directories contain files that are to overwrite generated Rails files: ```bash tree app/ config/ ``` ```text app/ |-- controllers/ | `-- hello_controller.rb `-- views/ `-- hello/ `-- index.html.erb config/ |-- environments/ | `-- development.rb `-- routes.rb ``` These files add the configuration (controller, view, route) to print the `Hello, World!` message in Rails. They overwrite the generated Rails configuration to provide the app setup. Update these files, and other files, with required contents for your own app. Lines in the `Kraftfile` have the following roles: * `spec: v0.7`: The current `Kraftfile` specification version is `0.7`. * `runtime: base-compat:latest`: The runtime kernel to use is the base compatibility kernel. * `rootfs`: Build the app root filesystem. `source: ./Dockerfile` means the filesystem is built using the `Dockerfile`. `format: erofs` means the filesystem type is [EROFS](https://erofs.docs.kernel.org/). * `cmd: ["/usr/bin/ruby", "/app/bin/rails", "server", "-b", "0.0.0.0"]`: Use `/usr/bin/ruby /app/bin/rails server -b 0.0.0.0` as the starting command of the instance. Lines in the `Dockerfile` have the following roles: * The `RUN` lines install Rails and generate the default app setup for a `hello` app. * `COPY . /app/`: Copies the local files (from `app/` and `config/`) to the app, overwriting generated contents to provide the user-specified app. * `FROM scratch`: Build the filesystem from the [`scratch` container image](https://hub.docker.com/_/scratch/), to [create a base image](https://docs.docker.com/build/building/base-images/). * `COPY --from=build /app /app`: Copy the app directory to the filesystem. The following options are available for customizing the app: * If you only update the implementation in the `app/` or `config/` directories, or adding new directories or files to overwrite Rails generated files, you don't need to make any other changes. * If changing the app name, change the `hello` name in `RUN rails generate controller hello` to a new one. The file names in the `app/` and `config/` directories have to be similarly updated. * More extensive changes may require extending the `Dockerfile` ([see `Dockerfile` syntax reference](https://docs.docker.com/engine/reference/builder/)). ## Learn more Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: Redis URL: /docs/guides/redis7.2 # Redis import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} This guide shows you how to use [Redis](https://redis.io), an open source in-memory storage, used as a distributed, in-memory key–value database, cache and message broker, with optional durability. To run it, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/redis7.2/` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/redis7.2/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` When done, invoke the following command to deploy this app on Unikraft Cloud: ```bash unikraft build . --output /redis72:latest unikraft run --metro fra \ -m 512M \ -p 6379:6379/tls \ --scale-to-zero policy=off \ --image /redis72:latest ``` The output shows the instance address and other details: ```ansi metro: fra name: redis72-alb4r uuid: d3c3141b-97b2-4e1d-87ae-39e4f14ab49e state: starting image: /redis72 resources:  memory: 512MiB  vcpus: 1 service:  uuid: 7a4f2b3c-1d8e-4a92-b3f5-e6c1d2a3b4e5  name: rough-wind-8vxrd1ms  domains:  - fqdn: rough-wind-8vxrd1ms.fra.unikraft.app networks: - uuid: 9b5e1f8d-3c2a-7b46-d1e9-f2a3b4c5d6e7  private-ip: 10.0.3.2  mac: 12:b0:4e:20:b3:e7 timestamps:  created: just now ``` In this case, the instance name is `redis72-alb4r` which is different for every run. To test the deployment, first forward the port using `socat`: ```bash socat TCP-LISTEN:6379,fork OPENSSL:rough-wind-8vxrd1ms.fra.unikraft.app:6379,verify=0 ``` Then, from another console, you can now use the `redis-benchmark` client to connect to Redis, for example: ```console redis-benchmark -t ping,set,get -n 10000 ``` You should see output like: ```ansi ====== PING_INLINE ====== 10000 requests completed in 32.03 seconds 50 parallel clients 3 bytes payload keep alive: 1 host configuration "save": host configuration "appendonly": no multi-thread: no 0.01% <= 138 milliseconds 0.05% <= 139 milliseconds 2.34% <= 140 milliseconds 4.49% <= 141 milliseconds 8.57% <= 142 milliseconds 16.06% <= 143 milliseconds 21.83% <= 144 milliseconds 26.25% <= 145 milliseconds 34.54% <= 146 milliseconds ... ``` To disconnect, kill the `socat` command with ctrl-C. :::note This guide uses `socat` for port forwarding only when a service doesn't support TLS and isn't HTTP-based (TLS/SNI determines the correct instance to send traffic to). Also note that port forwarding isn't needed when connecting via an instance's private IP/FQDN. For example, when a Redis instance serves as a cache server to another instance that acts as a frontend and which **does** support TLS. ::: You can list information about the instance by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra redis72-alb4r running /redis72 512MiB 1 rough-wind-8vxrd1ms.fra.unikraft.app 1 minute ago ``` When done, you can remove the instance: ```bash unikraft instances delete redis72-alb4r ``` ## Customize your app To customize the app, update the files in the repository, listed below: * `Kraftfile`: the Unikraft Cloud specification, including command-line arguments * `Dockerfile`: In case you need to add files to your instance's rootfs ## Learn more Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: Playwright (Chromium) with Python FastAPI URL: /docs/guides/python-playwright-chromium # Playwright (Chromium) with Python FastAPI import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} [Playwright](https://playwright.dev/) is a framework for web testing and Automation. To run this example, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/python-playwright-chromium/` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/python-playwright-chromium/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` When done, invoke the following command to deploy this app on Unikraft Cloud: ```bash unikraft build . --output /python-playwright-chromium:latest unikraft run --metro fra \ -m 4G \ -p 443:8080/tls+http \ --scale-to-zero policy=idle,cooldown-time=1000,stateful=true \ --image /python-playwright-chromium:latest ``` The output shows the instance address and other details: ```ansi metro: fra name: python-playwright-chromium-m6k3p uuid: e6f7a8b9-c0d1-2e3f-4a5b-e6f7a8b9c0d1 state: starting image: /python-playwright-chromium resources:  memory: 4096MiB  vcpus: 1 service:  uuid: f7a8b9c0-d1e2-3f4a-5b6c-f7a8b9c0d1e2  name: young-night-kq8bv2mx  domains:  - fqdn: young-night-kq8bv2mx.fra.unikraft.app networks: - uuid: a8b9c0d1-e2f3-4a5b-6c7d-a8b9c0d1e2f3  private-ip: 10.0.6.5  mac: 12:b0:e4:b0:23:1d timestamps:  created: just now ``` In this case, the instance name is `python-playwright-chromium-m6k3p` and the address is `https://young-night-kq8bv2mx.fra.unikraft.app`. They're different for each run. The command will deploy the files in the current directory. It results in the creation of a remote web-based service for creating PNG screenshots of remote pages. Use the `?page=` to point the service to the remote page to screenshot. Query the service using commands such as: ```console curl "https://..unikraft.app/?page=https://google.com" -o ss-google.png curl "https://..unikraft.app/?page=https://github.com" -o ss-github.png ``` You can list information about the instance by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra python-playwright-chromium-m6k3p running /python-playwright-chromium 4096MiB 1 young-night-kq8bv2mx.fra.unikraft.app 2 minutes ago ``` When done, you can remove the instance: ```bash unikraft instances delete ``` ## Learn more - [Playwright's Documentation](https://playwright.dev/docs/intro) - [FastAPI's Tutorial](https://fastapi.tiangolo.com/tutorial/) - [Unikraft Cloud's Documentation](https://unikraft.cloud/docs/) - [Building `Dockerfile` Images with `Buildkit`](https://unikraft.org/guides/building-dockerfile-images-with-buildkit) Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: Prometheus and Grafana URL: /docs/guides/prometheus-grafana # Prometheus and Grafana import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} [Prometheus](https://prometheus.io) is a monitoring system and time-series database, and [Grafana](https://grafana.com) is an open source analytics and visualization platform. This example deploys both on Unikraft Cloud to chart the [instance metrics](/platform/metrics) of your own account: Prometheus scrapes the metrics endpoint over a private network, and Grafana serves a pre-built dashboard behind a public port. The dashboard is a starting point rather than a supported product surface. Rename panels, drop rows you don't need, and tune the alert thresholds to your workload. ## Deployment To run this example, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/prometheus-grafana` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/prometheus-grafana/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` ## Configure The configuration files ship to each instance as read-only [ROMs](/features/roms), uploaded when the instance starts. Finish this section **before** deploying: changing a file later means deploying again with a new ROM, not editing a file in place. Fill in your metro and API token in [`prometheus/rom/prometheus.yml`](./prometheus/rom/prometheus.yml): ```bash export UKC_TOKEN= sed -i "s//fra/g; s//${UKC_TOKEN}/" prometheus/rom/prometheus.yml ``` :::warning A Unikraft Cloud API token is not read-only. The same token that reads metrics can also create, stop, and **delete** instances and volumes in your account. Never commit `prometheus.yml` with a real token filled in, and rotate the token if it lands somewhere unexpected. ::: The repository ships these files: | File | Purpose | | ---- | ------- | | `prometheus/rom/prometheus.yml` | Scrape config for `/v1/instances/metrics`. Mounted at `/etc/prometheus`. | | `prometheus/rom/alerts.yml` | Five starter alerting rules. | | `grafana/provisioning/datasources/datasource.yml` | Points Grafana at the Prometheus instance. Mounted at `/etc/grafana/provisioning`. | | `grafana/provisioning/dashboards/dashboards.yml` | Loads dashboards from the ROM directory. | | `grafana/dashboards/vm-instances.json` | The VM instances dashboard. Mounted at `/var/lib/grafana/dashboards`. | ## Prometheus Create a volume so the time-series database survives a restart: ```bash unikraft volume create --metro fra --name prometheus-data --size 1G ``` ```ansi metro: fra name: prometheus-data uuid: 689b21aa-8f2c-4d94-ba84-947119c11bc9 state: available size: 1GiB filesystem: ext4 quota-policy: static persistent: true access-mode: rwo timestamps:  created: just now ``` Build and deploy Prometheus. It gets an internal domain and no published port, so only Grafana on the same private network can reach it: ```bash unikraft build prometheus/ --output /prometheus:latest unikraft run --metro fra \ --name prometheus \ -m 1024M \ --domain prometheus-internal.internal. \ --rom dir=./prometheus/rom,at=/etc/prometheus \ --volume prometheus-data:/prometheus \ --image /prometheus:latest ``` The output shows the instance details: ```ansi metro: fra name: prometheus uuid: 7b71c6ec-1029-4040-b134-1eec95421f0b state: starting image: /prometheus resources:  memory: 1GiB  vcpus: 1 service:  name: rough-river-122ff7v6  uuid: ccc672b5-adda-4150-a5b7-1f438467d87d  domains:  - fqdn: prometheus-internal.internal volumes: - name: prometheus-data  uuid: 689b21aa-8f2c-4d94-ba84-947119c11bc9  at: /prometheus roms: - name: etc-prometheus  image: 7e85d7d4-4d33-411c-9560-36978707202e  at: /etc/prometheus networks: - uuid: 08305497-be6b-4786-801a-70f4aad8ad2a  private-ip: 10.0.0.65  mac: 12:b0:0a:00:00:41 timestamps:  created: just now ``` The `domains` entry confirms the internal FQDN, and the `roms` entry confirms the config directory was uploaded and mounted. :::warning Don't publish a port for Prometheus. It has no authentication of its own, so exposing it would hand your metrics, and the alert rules describing your estate, to anyone who finds the address. ::: :::note Don't enable scale-to-zero on Prometheus either. A sleeping Prometheus stops scraping, which leaves gaps in the data. ::: ## Grafana Build and deploy Grafana, this time with a public port: ```bash unikraft build grafana/ --output /grafana:latest unikraft run --metro fra \ --name grafana \ -p 443:3000/tls+http \ -m 1024M \ --scale-to-zero policy=on,cooldown-time=1000,stateful=true \ -e GF_SECURITY_ADMIN_USER=admin \ -e GF_SECURITY_ADMIN_PASSWORD= \ -e GF_USERS_ALLOW_SIGN_UP=false \ -e GF_ANALYTICS_REPORTING_ENABLED=false \ -e GF_ANALYTICS_CHECK_FOR_UPDATES=false \ --rom dir=./grafana/provisioning,at=/etc/grafana/provisioning \ --rom dir=./grafana/dashboards,at=/var/lib/grafana/dashboards \ --image /grafana:latest ``` :::warning Port 443 makes Grafana reachable by anyone who knows its address, with only the login in front of your metrics. Choose a strong, unique `` rather than the placeholder, and keep `GF_USERS_ALLOW_SIGN_UP=false` so visitors can't create their own accounts. ::: The output shows the generated address under `domains`: ```ansi metro: fra name: grafana uuid: 43e60d11-3db3-4da2-a491-5abd869eae3e state: starting image: /grafana runtime:  env: GF_ANALYTICS_CHECK_FOR_UPDATES: false GF_ANALYTICS_REPORTING_ENABLED: false GF_SECURITY_ADMIN_PASSWORD: * GF_SECURITY_ADMIN_USER: admin GF_USERS_ALLOW_SIGN_UP: false resources:  memory: 1GiB  vcpus: 1 service:  name: twilight-brook-c7ps65ek  uuid: 3e220713-e378-441c-80e5-25ace1474714  domains:  - fqdn: twilight-brook-c7ps65ek.fra.unikraft.app roms: - name: etc-grafana-provisioning  image: 0f6ea33c-e602-4932-acf0-3c7a14cdca97  at: /etc/grafana/provisioning - name: var-lib-grafana-dashboards  image: 2a479b52-0d6f-4a93-a930-1a19de9bec8c  at: /var/lib/grafana/dashboards networks: - uuid: 96ebdef3-7a98-46c0-9b7b-5c8ebb310620  private-ip: 10.0.0.49  mac: 12:b0:0a:00:00:31 timestamps:  created: just now scale-to-zero:  enabled: true  policy: on  stateful: true  cooldown-time: 1s ``` The FQDN is generated for you, and it differs on each run. The CLI masks `GF_SECURITY_ADMIN_PASSWORD` in its output, but the value still reaches the instance as an environment variable. Point your browser at the address and log in with the credentials you set above. The dashboard appears in the **Unikraft Cloud** folder. Data appears after the first couple of scrapes. The `rate()`-based panels need one to two minutes of history before they render anything. You can list information about the instances by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra grafana standby /grafana 1GiB 1 twilight-brook-c7ps65ek.fra.unikraft.app just now fra prometheus running /prometheus 1GiB 1 prometheus-internal.internal 6 minutes ago ``` Grafana shows as `standby` rather than `running` because `cooldown-time=1000` is 1000 **milliseconds**, so it parks almost immediately when idle. The next request wakes it. When done, you can remove the instances and the volume: ```bash unikraft instances delete grafana prometheus unikraft volume delete prometheus-data ``` ## What the dashboard shows The dashboard holds 24 panels across five rows, driven by the metrics that the [instance metrics reference](/platform/metrics) documents: | Row | Panels | | --- | ------ | | Fleet overview | Instance counts by state, total resident memory, active connections, and a per-instance state table | | Resource usage | CPU time rate in cores, resident memory, uptime, and restarts per hour | | Network | Inbound and outbound throughput, plus packet rates | | Requests and connections | Active, queued, and processed connections or requests | | Boot and wakeup latency | Boot, network setup, and template creation times, with wakeup latency percentiles and a heatmap | A **Data source** variable selects which Prometheus to query, and an **Instance** variable filters by `instance_uuid`. ## What to watch out for A few behaviours surprise people the first time: - **The endpoint reports only live instances.** An instance appears only while a live virtual machine backs it, so a fully stopped instance produces no series at all. Don't build a "stopped instances" count on this endpoint, because it would always read zero. - **Per-instance identity lives in `instance_uuid`.** Prometheus injects its own `instance` label holding the API host it scraped, which is identical for every instance. Group and join on `instance_uuid` instead. - **Panels label each series by UUID.** The Prometheus exposition carries no instance name, so legends show raw UUIDs. - **Counters restart from zero.** CPU time, the byte and packet counters, and the processed-request counter are reported since the last start of the instance, so a restart resets them. The `rate()`-based panels handle the reset. - **The wakeup latency panels stay empty without scale-to-zero.** Only instances with [scale-to-zero](/features/scale-to-zero) enabled record the histogram. ## Customize your app To customize the app, update the files in the repository, listed below: * `prometheus/rom/prometheus.yml`: scrape targets, intervals, and labels. To watch more than one metro, add a `static_configs` entry per metro, each with its own `region` label. * `prometheus/rom/alerts.yml`: alerting rules and thresholds. * `grafana/provisioning/`: the Grafana data source and dashboard provider. * `grafana/dashboards/vm-instances.json`: the dashboard itself. * `Kraftfile`: the Unikraft Cloud specification, including command-line arguments * `Dockerfile`: In case you need to add files to your instance's rootfs The dashboard queries carry a hard-coded `job="instances"` selector. If you rename the job in `prometheus.yml`, update the dashboard queries to match, or every panel reads "No data". ## Learn more Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: Serverless Databases: PostgreSQL URL: /docs/guides/postgres # Serverless Databases: PostgreSQL import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} Serverless databases scale up the moment a query arrives and cost nothing while idle, instead of keeping a provisioned instance running around the clock. This guide shows you how to use [PostgreSQL](https://www.postgresql.org/), a powerful, open source object-relational database system, in exactly that fashion. The database runs in a lightweight virtual machine that scales to zero when not in use and resumes in milliseconds on the next connection. To run it, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/postgres/` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/postgres/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` When done, invoke the following command to deploy this app on Unikraft Cloud: ```bash unikraft build . --output /postgres:latest unikraft run --metro fra \ -m 1G \ -p 5432:5432/tls \ --scale-to-zero policy=idle,cooldown-time=1000,stateful=true \ -e POSTGRES_PASSWORD=unikraft \ --image /postgres:latest ``` The output shows the instance address and other details: ```ansi metro: fra name: postgres-saan9 uuid: 3a1371f2-68c6-4187-84f8-c080f2b028ca state: starting image: /postgres resources:  memory: 1024MiB  vcpus: 1 service:  uuid: 8e9d810b-b1da-a30b-fd42-5c30c1900cb5  name: young-thunder-fbafrsxj  domains:  - fqdn: young-thunder-fbafrsxj.fra.unikraft.app networks: - uuid: f1fab4c9-7951-75e3-ea1c-d87e47b4c9e2  private-ip: 10.0.3.1  mac: 12:b0:31:34:b1:96 timestamps:  created: just now ``` In this case, the instance name is `postgres-saan9` and the service `young-thunder-fbafrsxj`. They're different for each run. If you use port 5432/tls per the example above, you can now directly connect to postgres: ```console psql -U postgres -h young-thunder-fbafrsxj.fra.unikraft.app ``` Use the `unikraft` password at the password prompt. You should see output like: ```ansi Password for user postgres: psql (15.5 (Ubuntu 15.5-0ubuntu0.23.04.1), server 16.2) WARNING: psql major version 15, server major version 16. Some psql features might not work. Type "help" for help. postgres=# ``` Use SQL and `psql` commands for your work. :::tip This example uses the [`idle` scale-to-zero policy](/api/platform/v1/instances#scaletozero_policy) by default. It means that the instance will scale-to-zero even in the presence of `psql` connections. The PostgreSQL example makes use of scale-to-zero app support. This ensures that the instance isn't put into standby even for long running queries (during which the connections are also idle). To this end, the example loads the [`pg_ukc_scaletozero`](https://github.com/unikraft-cloud/pg_ukc_scaletozero) module into PostgreSQL, which suspends scale-to-zero during query processing. You can see this in action by running `SELECT pg_sleep(10);` and verifying that the instance keeps on running. ::: You can list information about the instance by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra postgres-saan9 running /postgres 1.0GiB 1 young-thunder-fbafrsxj.fra.unikraft.app 2 minutes ago ``` When done, you can remove the instance: ```bash unikraft instance remove postgres-saan9 ``` ## Using volumes You can use [volumes](/platform/volumes) for data persistence for your PostgreSQL instance. For that you would first create a volume: ```bash unikraft volume create --set metro=fra --set name=postgres --set size=200M ``` Then start the PostgreSQL instance and mount that volume: ```bash unikraft build . --output /postgres:latest unikraft run --metro fra \ -m 1G \ -p 5432:5432/tls \ --scale-to-zero policy=idle,cooldown-time=1000,stateful=true \ -e POSTGRES_PASSWORD=unikraft \ -e PGDATA=/volume/postgres \ --volume postgres:/volume \ --image /postgres:latest ``` ## Customize your deployment Your deployment is a standard PostgreSQL installation. Customizing the deployment means providing a different environment. An obvious one is to use a different database password when starting PostgreSQL. For that you use a different `POSTGRES_PASSWORD` environment variable when starting the PostgreSQL instance. You could also use a different location to mount your volume or set extra configuration options. You can use the PostgreSQL instance in conjunction with a frontend service, [see the guide here](/platform/services). ## Learn more Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: Phoenix with PostgreSQL URL: /docs/guides/phoenix-postgres # Phoenix with PostgreSQL import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} [Phoenix](https://phoenixframework.org/) is a web development framework written in Elixir, designed for building scalable and maintainable applications. This example demonstrates how to deploy a Phoenix application with a PostgreSQL database on [Unikraft Cloud](https://unikraft.cloud/). This example app has been generated using [Phoenix Express](https://hexdocs.pm/phoenix/up_and_running.html#phoenix-express), and the Dockerfile used for building the rootfs was generated following the instructions [here](https://hexdocs.pm/phoenix/releases.html). To run this example, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/phoenix-postgres` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/phoenix-postgres/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` ## PostgreSQL Create a volume for PostgreSQL data persistence: ```bash unikraft volume create --metro fra --name db-data --size 512M ``` You can list the created volume by running: ```bash unikraft volume list ``` ```ansi METRO NAME STATE SIZE CREATED fra db-data available 512MiB just now ``` Update `POSTGRES_PASSWORD` with a secure password and build and deploy the PostgreSQL instance: ```bash unikraft build ./postgres --output /postgres:latest unikraft run --metro fra \ -m 2G \ --scale-to-zero policy=idle,cooldown-time=1000,stateful=true \ --domain postgres.internal \ --env POSTGRES_USER=postgres \ --env POSTGRES_PASSWORD=example_123 \ --env POSTGRES_DB=myapp_prod \ --env PGDATA=/var/lib/postgresql/data/pgdata \ --volume db-data:/var/lib/postgresql/data \ --image /postgres:latest ``` Make sure to replace `` with your username / org-name. The output shows the PostgreSQL instance details: ```ansi metro: fra name: postgres-ik5at uuid: 3776dbfe-2937-45e7-8079-c54275ef3cff state: starting image: /postgres runtime:  env: PGDATA: /var/lib/postgresql/data/pgdata POSTGRES_DB: myapp_prod POSTGRES_PASSWORD: * POSTGRES_USER: postgres resources:  memory: 2GiB  vcpus: 1 service:  name: dry-cloud-q8u7yjkl  uuid: cf364e60-46b0-4031-bace-296fa8e24ac4  domains:  - fqdn: postgres.internal volumes: - name: db-data  uuid: 0efda5f6-21d2-48c7-84f5-8efe87e33090  at: /var/lib/postgresql/data networks: - uuid: cab79d9f-08a8-4741-8efd-58d9a1faa35d  private-ip: 10.0.14.117  mac: 12:b0:0a:00:0e:75 timestamps:  created: just now scale-to-zero:  enabled: true  policy: idle  stateful: true  cooldown-time: 1s ``` ## Phoenix Generate a secret key for Phoenix: ```bash openssl rand -base64 48 ``` Replace `` in the commands below with the generated value, and update the password in `DATABASE_URL` to match the one set for PostgreSQL. Then, deploy the Phoenix instance: ```bash unikraft build ./phoenix --output /phoenix:latest unikraft run --metro fra \ -m 2G \ -p 443:4000/tls+http \ --scale-to-zero policy=on,cooldown-time=1000 \ --env SECRET_KEY_BASE= \ --env DATABASE_URL=ecto://postgres:example_123@postgres.internal:5432/myapp_prod \ --image /phoenix:latest ``` The output shows the instance address and other details: ```ansi metro: fra name: phoenix-hd29m uuid: fa4db9cf-f7b5-4b8b-b007-8fd1ce87c7e1 state: starting image: /phoenix runtime:  env: DATABASE_URL: ecto://postgres:example_123@postgres.internal:5432/myapp_prod SECRET_KEY_BASE: resources:  memory: 2GiB  vcpus: 1 service:  name: wild-moon-pkwsqc49  uuid: 1d1b8732-d2d5-41ab-ab8f-1b19fc27c14d  domains:  - fqdn: wild-moon-pkwsqc49.fra.unikraft.app networks: - uuid: d14f07de-1c81-4309-bbf3-ab8ab6aeefe6  private-ip: 10.0.14.53  mac: 12:b0:0a:00:0e:35 timestamps:  created: just now scale-to-zero:  enabled: true  policy: on  cooldown-time: 1s ``` In this case, the instance names are `postgres-ik5at` and `phoenix-hd29m`. They're different for each run. Use a browser to access the Phoenix application using the URL from the `fqdn` field in the output. You can list information about the instances by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE MEMORY VCPUS FQDN CREATED fra phoenix-hd29m running /phoenix 2GiB 1 wild-moon-pkwsqc49.fra.unikraft.app just now fra postgres-ik5at standby /postgres 2GiB 1 postgres.internal 3 minutes ago ``` When done, you can remove the instances: ```bash unikraft instances delete postgres-ik5at phoenix-hd29m ``` ## Volume This deployment creates a volume (`db-data`) for PostgreSQL data persistence. The volume persists after removing instances, allowing you to redeploy without losing data. To remove the volume: ```bash unikraft volume delete db-data ``` ## Learn more - [Phoenix's Documentation](https://hexdocs.pm/phoenix/) - [PostgreSQL's Documentation](https://www.postgresql.org/docs/) - [Unikraft Cloud's Documentation](https://unikraft.cloud/docs/) - [Building `Dockerfile` Images with `Buildkit`](https://unikraft.org/guides/building-dockerfile-images-with-buildkit) Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: Guides Overview URL: /docs/guides/overview # Guides Overview This page lists all available guides for Unikraft Cloud. The guides are in two groups: * **Tutorials** explain a platform topic that applies to any app. Two examples are root filesystem formats and environment variables. * **Example apps** show how to deploy one specific app or service. They mimic the [examples](https://github.com/unikraft-cloud/examples) repository, and are constantly updated with new content. :::note Unikraft Cloud can run any workload—define it in a `Dockerfile` and it will run it. These guides are here to make that journey as fast as possible for you. ::: ## Tutorials - [Docker To Unikraft Cloud](/tutorials/docker-to-ukc) - [KraftKit To Unikraft](/tutorials/kraftkit-to-unikraft) - [Environment Variables](/tutorials/environment-variables) - [Rootfs Formats](/tutorials/rootfs-formats) - [Rootfs Compression](/tutorials/rootfs-compression) - [Rootfses, Volumes and ROMs](/tutorials/rootfs-volumes-roms) - [Scale To Zero Triggers](/tutorials/scale-to-zero-triggers) The sections below list the example apps, grouped by category. {/* vale off */} ## HTTP Servers Deploy HTTP servers written in your language of choice on Unikraft Cloud. ### C / C++ - [C HTTP Server](/guides/httpserver-gcc13.2) - [C++ HTTP Server](/guides/httpserver-gpp13.2) - [C++ Boost HTTP Server](/guides/httpserver-boost1.74-gpp13.2) - [SSH and HTTP Server with C and Debugging Tools](/guides/httpserver-c-debug) ### .NET - [.NET HTTP Server](/guides/httpserver-dotnet10.0) ### Elixir / Erlang - [Elixir HTTP Server](/guides/httpserver-elixir1.16) - [Erlang HTTP Server](/guides/httpserver-erlang26.2) ### Go - [Go HTTP Server](/guides/httpserver-go1.21) - [Go and Redis HTTP Server](/guides/httpserver-go1.22-redis) ### Java - [Java HTTP Server](/guides/httpserver-java21) - [Spring Boot HTTP Server](/guides/httpserver-java17-springboot) - [Spring Petclinic](/guides/httpserver-java17-spring-petclinic) ### JavaScript / TypeScript - [Node HTTP Server](/guides/httpserver-node26) - [Bun HTTP Server](/guides/httpserver-bun) - [Express HTTP Server](/guides/httpserver-expressjs4.18-node21) - [Puppeteer HTTP Server](/guides/httpserver-node-express-puppeteer) - [Prisma HTTP Server](/guides/httpserver-prisma-expressjs4.19-node18) - [Next.js HTTP Server](/guides/httpserver-node21-nextjs) - [React Router HTTP Server](/guides/httpserver-node22-react-router) - [SolidJS HTTP Server](/guides/httpserver-node21-solid-start) - [SvelteKit HTTP Server](/guides/httpserver-node22-sveltekit) - [Vite HTTP Server (Nginx)](/guides/httpserver-nginx-vite-vanilla) - [Vite HTTP Server (Node)](/guides/httpserver-node-vite-vanilla) - [Vite SSR HTTP Server (Node)](/guides/httpserver-node-vite-ssr-vanilla) ### Lua - [Lua HTTP Server](/guides/httpserver-lua5.1) ### Perl - [Perl HTTP Server](/guides/httpserver-perl5.42) ### PHP - [PHP HTTP Server](/guides/httpserver-php8.2) ### Python - [Python HTTP Server](/guides/httpserver-python3.12) - [Django HTTP Server](/guides/httpserver-python3.12-django5.0) - [FastAPI HTTP Server](/guides/httpserver-python3.12-fastapi-0.121.3) - [Flask HTTP Server](/guides/httpserver-python3.12-flask3.0) - [Flask and SQLite HTTP Server](/guides/httpserver-python3.12-flask3.0-sqlite) - [Flask + Redis HTTP Server](/guides/httpserver-flask-redis) ### Ruby - [Ruby HTTP Server](/guides/httpserver-ruby3.2) ### Rust - [Rust HTTP Server](/guides/httpserver-rust1.91) - [Rust (Actix Web) HTTP Server](/guides/httpserver-rust1.88-actix-web4) - [Rust (Leptos + Trunk) HTTP Server](/guides/httpserver-rust-trunkrs-leptos) - [Rust (Rocket) HTTP Server](/guides/httpserver-rust1.88-rocket0.5) - [Rust (Tokio) HTTP Server](/guides/httpserver-rust1.75-tokio) ## Web Servers & Reverse Proxies Serve static content or route traffic with popular web server and proxy tools. - [Nginx](/guides/nginx) - [Caddy](/guides/caddy2.7-go1.21) - [HAProxy](/guides/haproxy) - [Skipper](/guides/skipper0.18) - [Traefik](/guides/traefik) - [Tyk](/guides/tyk) ## Databases & Caching Run databases and caching layers on Unikraft Cloud. - [DragonflyDB](/guides/dragonflydb) - [DuckDB with Go](/guides/duckdb-go1.21) - [MariaDB](/guides/mariadb) - [Memcached](/guides/memcached1.6) - [MongoDB](/guides/mongodb) - [MySQL](/guides/mysql) - [Neo4j](/guides/neo4j) - [PostgreSQL](/guides/postgres) - [Redis](/guides/redis7.2) ## Storage & Media Host object storage and image processing services. - [MinIO](/guides/minio) - [Imaginary](/guides/imaginary) ## WebAssembly Runtimes Run WebAssembly workloads on Unikraft Cloud. - [Spin](/guides/spin-wagi-http) - [Wazero](/guides/wazero-import-go) ## Code Execution & Build Environments Package user code as auxiliary ROMs and run it on a generic base image. - [Build/Test Environments with ROMs](/guides/build-environments) - [Node.js Code Execution with ROMs](/guides/node-code-execution) ## AI & MCP Servers Deploy Model Context Protocol (MCP) servers and AI-enabled services. - [Simple MCP Server](/guides/mcp-server-simple) - [ArXiv MCP Server](/guides/mcp-server-arxiv) - [OpenClaw](/guides/openclaw) ## Observability & Monitoring Collect metrics and visualise them with standard tools. - [Grafana](/guides/grafana) - [OpenTelemetry Collector](/guides/opentelemetry-collector) - [Prometheus and Grafana](/guides/prometheus-grafana) ## Developer Tools & Remote Access Access remote development environments and file transfer services. - [Visual Studio Code Server](/guides/visual-studio-code-server) - [noVNC](/guides/novnc-browser) - [Debian SSH Server](/guides/debian-ssh) - [vsftpd](/guides/vsftpd) ## Full-Stack Applications & CMS Deploy complete web applications and content management systems. - [WordPress (All-in-One)](/guides/wordpress-all-in-one) - [WordPress with Nginx and MariaDB](/guides/wordpress) - [Ruby on Rails](/guides/ruby3.2-rails) - [Phoenix with PostgreSQL](/guides/phoenix-postgres) - [Flask with MongoDB](/guides/nginx-flask-mongo) - [Hugo](/guides/hugo0.122) ## Browser Automation Run browser automation and end-to-end testing workloads on Unikraft Cloud. - [Chromium CDP](/guides/chromium-cdp) - [Chromium CDP with Token Authentication](/guides/chromium-cdp-auth) - [Playwright Chromium (Node)](/guides/node-playwright-chromium) - [Playwright Firefox (Node)](/guides/node-playwright-firefox) - [Playwright WebKit (Node)](/guides/node-playwright-webkit) - [Playwright Chromium (Python)](/guides/python-playwright-chromium) ## Real-time & Gaming Multiplayer and WebSocket-based applications. - [Node WebSocket Server](/guides/node21-websocket) - [Agar.io Clone](/guides/node18-agario) - [Wings.io Clone](/guides/node18-wingsio) - [Node AllKaraoke](/guides/node24-karaoke) - [Minecraft](/guides/minecraft) ## Integration & Automation Examples for webhooks, background jobs, and automation. - [GitHub Webhook Receiver](/guides/github-webhook-node) {/* vale on */} --- ## Document: OpenTelemetry Collector URL: /docs/guides/opentelemetry-collector # OpenTelemetry Collector import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} This example uses [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/), a vendor-agnostic implementation of how to receive, process and export telemetry data. OpenTelemetry Collector works with Unikraft / Unikraft Cloud to process telemetry data. To run this example, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/opentelemetry-collector/` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/opentelemetry-collector/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` When done, invoke the following command to deploy this app on Unikraft Cloud: ```bash unikraft build . --output /opentelemetry-collector:latest unikraft run --metro fra \ -m 1536M \ --image /opentelemetry-collector:latest ``` The output shows the instance details: ```ansi metro: fra name: opentelemetry-collector-bvtnh uuid: 40e8b154-b3b6-4312-ae69-2cdb794b15e4 state: starting image: /opentelemetry-collector resources:  memory: 1536MiB  vcpus: 1 networks: - uuid: e74ba590-cbec-404b-d076-16aca1b52404  private-ip: 10.0.3.3  mac: 12:b0:aa:f7:b9:26 timestamps:  created: just now ``` In this case, the instance name is `opentelemetry-collector-bvtnh`. They're different for each run. Note that the instance doesn't export a service. The default configuration can receive telemetry data from other instances by specifying the private IP or internal DNS as destination. Use port 4317 for gRPC and port 4318 for HTTP. The only configured exporter is the debug exporter. Feel free to change and redeploy! You can list information about the instance by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra opentelemetry-collector-bvtnh running /opentelemetry 1536MiB 1 2 minutes ago ``` When done, you can remove the instance: ```bash unikraft instances delete opentelemetry-collector-bvtnh ``` ## Customize your app To customize the OpenTelemetry Collector app, update `Kraftfile` or, more likely, the `rootfs/etc/otel/config.yaml` files: You can update the `rootfs/etc/otel/config.yaml` file as detailed in the [documentation](https://opentelemetry.io/docs/collector/configuration/). Such as adding another export, apart from the debug exporter. ## Learn more Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: Sandboxes: OpenClaw URL: /docs/guides/openclaw # Sandboxes: OpenClaw import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} Sandboxes are on-demand, isolated execution environments for work you would rather not run next to anything else, such as AI agent tool calls, user-submitted code, or third-party plugins. This example uses [OpenClaw](https://openclaw.ai/), an autonomous AI agent framework, as the sandboxed workload: the gateway and everything the agent runs stay inside a dedicated microVM, which scales to zero when idle and resumes in milliseconds. This guide explains how to create and deploy your very own OpenClaw gateway on Unikraft Cloud. To run this example, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/openclaw` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/openclaw/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` When done, you may create the OpenClaw Unikraft Cloud image and deploy an instance from it like so: ```bash unikraft build . --output /openclaw:latest unikraft run --metro fra \ -m 4G \ -p 18789:18789/tls \ -p 2222:2222/tls \ --scale-to-zero policy=on,cooldown-time=10000,stateful=true \ -e PUBKEY="...." \ --image /openclaw:latest ``` Make sure to replace `` with your username / org-name and to set your SSH public key as the `PUBKEY` environment variable above. The output shows the instance address and other details: ```ansi metro: fra name: openclaw-8tosm uuid: e2a6183a-721b-4145-bfaf-37a5f859bbc1 state: running image: /openclaw runtime: env: PUBKEY: * resources: memory: 4GiB vcpus: 1 service: uuid: 7ab20338-b04d-4869-947b-9433e21677b1 name: divine-flower-bxsaapup domains: - fqdn: divine-flower-bxsaapup.fra.unikraft.app networks: - uuid: 2b0b120b-6ce5-4b19-ac4c-04ee8f11526e private-ip: 10.0.12.97 mac: 12:b0:0a:00:0c:61 timestamps: created: just now ``` In this case, the instance name is `openclaw-8tosm` and the address is `divine-flower-bxsaapup.fra.unikraft.app`. These will be different for each run. You can now SSH into this instance and run the OpenClaw onboarding process. In order to SSH, you need to set up a tunnel that handles the TLS connection to the Unikraft Cloud instance. This way, you have a non-TLS port that your SSH client can connect to: ```bash socat TCP-LISTEN:2222,reuseaddr,fork OPENSSL:divine-flower-bxsaapup.fra.unikraft.app:2222,verify=0 ``` Then connect to the instance via SSH using: ```bash ssh -l root localhost -p 2222 ``` You can list information about the instance by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra openclaw-8tosm running /openclaw:latest 4GiB 1 divine-flower-bxsaapup.fra.unikraft.app 2 minutes ago ``` When done, you can remove the instance using: ```bash unikraft instances delete openclaw-8tosm ``` ## Learn more Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` ## OpenClaw Setup Once you have SSH'd into your instance, you may run: ```bash openclaw onboard ``` This will set up your OpenClaw gateway on the instance. You will be asked to provide your LLM's API key here. Once done, make note of your `gateway.auth.token` (henceforth referenced as ``) from `~/.openclaw/openclaw.json` ```bash cat ~/.openclaw/openclaw.json ``` Set `gateway.controlUi.allowedOrigins` in `~/.openclaw/openclaw.json`: ```json ... "gateway": { ... "controlUi": { "allowedOrigins": [ "https://proud-smoke-cjf0wro8.fra.unikraft.app:18789" ] }, ... }, ... ``` Replace the domain in the above URL with the address of your instance (noted earlier). Run the gateway: ```bash openclaw gateway run --bind lan ``` You may now access the web dashboard using the following URL: ```ansi https://
:18789?token= ``` Where `
` is your above noted address and `` is your above noted token. For security reasons, you will have to manually approve your web "device" in order to start using the web dashboard. Create a new SSH connection to your OpenClaw instance: ```bash ssh -l root localhost -p 2222 ``` First, find your device ID: ```bash openclaw devices list ``` Look under the `Request` column. Device IDs look like `cabd915e-137a-4bc4-b640-d0e507684d65` Finally, approve your device with: ```bash openclaw devices approve ``` Once this is done, refresh your OpenClaw web dashboard. You now have full access to your very own OpenClaw deployment on Unikraft Cloud! {/* vale on */} --- ## Document: Remote Desktops: noVNC URL: /docs/guides/novnc-browser # Remote Desktops: noVNC import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} Remote desktops put a full Linux GUI behind a browser tab, which powers agentic computer-use workloads, secure browsing sessions, and disposable workstations. They are memory-hungry and used in short, interactive bursts, so each desktop runs in its own instance that scales to zero between sessions. This guide explains how to create and deploy a [noVNC](https://novnc.com/info.html) app, allowing you to access remote desktops through a web interface inside a modern browser. **Note**: Anthropic's [Computer Use Demo](https://github.com/anthropics/claude-quickstarts/tree/main/computer-use-demo) inspired this example. To run this example, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/novnc-browser` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/novnc-browser/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` When done, invoke the following command to deploy this app on Unikraft Cloud: ```bash unikraft build . --output /novnc-browser:latest unikraft run --metro fra \ -m 4G \ -p 443:6080/tls+http \ -e NOVNC_PASSWORD= \ --scale-to-zero policy=on,cooldown-time=4000,stateful=true \ --image /novnc-browser:latest ``` Replace `` with a long random password. For more information, see [Authentication](#authentication). The output shows the instance address and other details: ```ansi metro: fra name: vnc-browser uuid: 90a59b05-0ae1-4ca6-8383-79c5115355ee state: starting image: /novnc-browser runtime:  env: NOVNC_PASSWORD: * resources:  memory: 4096MiB  vcpus: 1 service:  uuid: aaf03f7c-65e6-5624-d5f4-84e87450beee  name: weathered-fog-y5jjmwfd  domains:  - fqdn: weathered-fog-y5jjmwfd.fra.unikraft.app networks: - uuid: 61708609-d291-572d-4a4c-399413238199  private-ip: 10.0.0.49  mac: 12:b0:1e:47:6c:59 timestamps:  created: just now ``` In this case, the instance name is `vnc-browser` and the address is `https://weathered-fog-y5jjmwfd.fra.unikraft.app`. The name was preset, but the address is different for each run. Enter the provided address into your browser of choice to access the remote desktop interface. The browser asks for a user name and a password. Use the user name `novnc` and the password from `NOVNC_PASSWORD`. You can list information about the instance by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra vnc-browser standby /novnc-browser 4.0GiB 1 weathered-fog-y5jjmwfd.fra.unikraft.app 2 minutes ago ``` When done, you can remove the instance: ```bash unikraft instances delete vnc-browser ``` ## Authentication When you set the `NOVNC_PASSWORD` environment variable, each request to the remote desktop must have one of these credentials: - HTTP Basic Auth with the user name and the password. The browser shows a login dialog. - The query parameter `token=`, for a link that opens the desktop without a login dialog. - The session cookie, which the browser gets after a login with `token`. Unikraft Cloud encrypts the connection with TLS. | Variable | Value | Default | | ---------------- | ----------------------------------------------- | ----------------- | | `NOVNC_PASSWORD` | Password for the remote desktop | No authentication | | `NOVNC_USER` | User name for HTTP Basic Auth, without a `:` | `novnc` | If `NOVNC_PASSWORD` is set but empty, the desktop does not start. The same applies to an empty `NOVNC_USER` when the desktop has a password. :::warning If you do not set `NOVNC_PASSWORD`, the remote desktop has no authentication. A person who knows the address can control the desktop. The instance log then shows `WARNING: noVNC has no authentication`. ::: A program, for example an AI agent, can also use the desktop. It can send the `Authorization: Basic ` header with each request, also with the WebSocket request to `/websockify`. For example: ```bash curl -u 'novnc:' https://weathered-fog-y5jjmwfd.fra.unikraft.app/vnc.html ``` With Playwright, set the credentials on the browser context: ```python context = browser.new_context( http_credentials={"username": "novnc", "password": ""}) ``` To open the desktop with a link, put the URL-encoded password in the `token` parameter: ```text https://weathered-fog-y5jjmwfd.fra.unikraft.app/?token=&autoconnect=true ``` The desktop then sets a session cookie and sends the browser to the same address without `token`. Thus, the password does not stay in the address bar. The browser history can keep the link with the password, so do not open the link on a shared computer. The link needs HTTPS, because the browser keeps the session cookie only on a secure connection. In the `token` parameter, a `+` is a space, and a `&`, `%` or `#` breaks the link. To URL-encode the password, run this command and type the password: ```bash python3 -c 'import getpass, urllib.parse; print(urllib.parse.quote(getpass.getpass(), safe=""))' ``` An agent can also connect to the WebSocket with the token: `wss://weathered-fog-y5jjmwfd.fra.unikraft.app/websockify?token=`. :::note The password in a `token` parameter is in the address of the first request. Proxies can write this address to their logs. ::: A page of another site can show the desktop in a frame. Put `vnc.html` and the token in the address of the frame: ```html ``` In a frame, the address keeps the token. The page that shows the frame contains the password, thus show this page only to persons who can know the password. Without the token in the address of the frame, a page of another site cannot show the desktop in a frame. A page of another site cannot connect to the WebSocket of the desktop. Do not put the credentials in the address as `https://user:password@...`. Some browsers do not send them with the WebSocket request. ## Learn more Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: Node AllKaraoke URL: /docs/guides/node24-karaoke # Node AllKaraoke import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} [Allkaraoke](https://github.com/Asvarox/allkaraoke) offers an ultrastar deluxe-like online platform for karaoke. To run this example, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/node24-karaoke` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/node24-karaoke/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` When done, invoke the following command to deploy this app on Unikraft Cloud: ```bash unikraft build . --output /node24-karaoke:latest unikraft run --metro fra \ -m 2G \ -p 443:8080/tls+http \ --scale-to-zero policy=on,cooldown-time=2000,stateful=true \ --image /node24-karaoke:latest ``` The output shows the instance address and other details: ```ansi metro: fra name: node24-karaoke-9lw5q uuid: e5f6a7b8-c9d0-1234-efab-345678901234 state: starting image: /node24-karaoke resources:  memory: 2GiB  vcpus: 1 service:  uuid: ef4112f8-10fc-fe6e-f48c-43a6623ec878  name: wild-song-p5q2nrwx  domains:  - fqdn: wild-song-p5q2nrwx.fra.unikraft.app networks: - uuid: cf5f3cbb-abf5-632e-3dd6-2de91885c6d9  private-ip: 10.0.3.8  mac: 12:b0:30:64:22:f9 timestamps:  created: just now ``` In this case, the instance name is `node24-karaoke-9lw5q` and the address is `https://wild-song-p5q2nrwx.fra.unikraft.app`. They're different for each run. Use `curl` to query the Unikraft Cloud instance of the AllKaraoke instance: ```bash curl https://wild-song-p5q2nrwx.fra.unikraft.app ``` ```text AllKaraoke.Party - Free Online Karaoke ... ... ``` You can list information about the instance by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra node24-karaoke-9lw5q running /node24-karaoke 2GiB 1 wild-song-p5q2nrwx.fra.unikraft.app 2 minutes ago ``` When done, you can remove the instance: ```bash unikraft instances delete node24-karaoke-9lw5q ``` ## Customize your app To customize the app, update the files in the repository, listed below: * `Kraftfile`: the Unikraft Cloud specification * `Dockerfile`: the Docker-specified app filesystem * `entrypoint.sh`: the shell script used to start the AllKaraoke server Lines in the `Kraftfile` have the following roles: * `spec: v0.7`: The current `Kraftfile` specification version is `0.7`. * `runtime: base-compat:latest`: The kernel to use. * `rootfs`: Build the app root filesystem. `source: ./Dockerfile` means the filesystem is built using the `Dockerfile`. `format: erofs` means the filesystem type is [EROFS](https://erofs.docs.kernel.org/). * `cmd: ["/entrypoint.sh"]`: Use `/entrypoint.sh` as the starting command of the instance. Lines in the `Dockerfile` have the following roles: * `FROM node:24-bookworm-slim AS build`: Build the AllKaraoke project using the Node.js 24 Bookworm slim image. * `RUN git clone ...; pnpm install; pnpm build`: Clone the AllKaraoke repository, install dependencies, and build it for production. * `FROM node:24-bookworm-slim AS prod`: Use a fresh Node.js 24 Bookworm slim image for the runtime. * `COPY ...`: Copy required files to the app filesystem: the `node` binary executable, system libraries, the built AllKaraoke artifacts, and the entrypoint script. The following options are available for customizing the app: * If you want to use a specific version of AllKaraoke, update the `git clone` command in the `Dockerfile` to pin a particular commit or tag. * If you want to add extra files, you need to copy them into the filesystem using the `COPY` command in the `Dockerfile`. * If you want to change the startup behavior, update the `entrypoint.sh` script. * More extensive changes may require extending the `Dockerfile` ([see `Dockerfile` syntax reference](https://docs.docker.com/engine/reference/builder/)). ## Learn more - [Allkaraoke official deployment](https://allkaraoke.party/) - [Unikraft Cloud's Documentation](https://unikraft.cloud/docs/) - [Building `Dockerfile` images with `Buildkit`](https://unikraft.org/guides/building-dockerfile-images-with-buildkit) Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: Node WebSocket Server URL: /docs/guides/node21-websocket # Node WebSocket Server import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} [WebSocket](https://en.wikipedia.org/wiki/WebSocket) is a bidirectional communication protocol over TCP, compatible with HTTP. This example builds an echo-reply WebSocket server in [Node](https://nodejs.org/en). To run this example, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/node21-websocket/` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/node21-websocket/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` When done, invoke the following command to deploy this app on Unikraft Cloud: ```bash unikraft build . --output /node21-websocket:latest unikraft run --metro fra \ -m 1G \ -p 443:8080/tls+http \ --scale-to-zero policy=on,cooldown-time=1000,stateful=true \ --image /node21-websocket:latest ``` The output shows the instance address and other details: ```ansi metro: fra name: node21-websocket-j2x9r uuid: d5e6f7a8-b9c0-1d2e-3f4a-d5e6f7a8b9c0 state: starting image: /node21-websocket resources:  memory: 1024MiB  vcpus: 1 service:  uuid: e6f7a8b9-c0d1-2e3f-4a5b-e6f7a8b9c0d1  name: lively-breeze-hp3wx6yt  domains:  - fqdn: lively-breeze-hp3wx6yt.fra.unikraft.app networks: - uuid: f7a8b9c0-d1e2-3f4a-5b6c-f7a8b9c0d1e2  private-ip: 10.0.5.4  mac: 12:b0:d3:af:12:0c timestamps:  created: just now ``` In this case, the instance name is `node21-websocket-j2x9r` and the address is `https://lively-breeze-hp3wx6yt.fra.unikraft.app`. They're different for each run. The command will build the files in the current directory. After deploying, you can query the service with a WebSocket client, such as [`wscat`](https://github.com/websockets/wscat). Install `wscat` with `npm`: ```console npm install -g wscat ``` Then query the WebSocket server deployed on Unikraft Cloud, using its URL: ```console wscat --connect wss://..unikraft.app ``` Then enter messages, that will be replied by the server. You can list information about the instance by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra node21-websocket-j2x9r running /node21-websocket 1024MiB 1 lively-breeze-hp3wx6yt.fra.unikraft.app 2 minutes ago ``` When done, you can remove the instance: ```bash unikraft instances delete ``` ## Learn more - [WebSocket documentation](https://nextjs.org/docs) - [ws: A Node.js WebSocket library](https://github.com/websockets/ws) - [Unikraft Cloud's Documentation](https://unikraft.cloud/docs/) - [Building `Dockerfile` Images with `Buildkit`](https://unikraft.org/guides/building-dockerfile-images-with-buildkit) Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: Wings.io (Node) URL: /docs/guides/node18-wingsio # Wings.io (Node) import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} [Wings.io](https://wings.io/) is a multiplayer .io game where players control a plane and try to shoot down other players while avoiding being shot themselves. This guide deploys an implementation of the game using Node.js on Unikraft Cloud. To run this example, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/node18-wingsio/` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/node18-wingsio/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` When done, invoke the following command to deploy this app on Unikraft Cloud: ```bash unikraft build . --output /node18-wingsio:latest unikraft run --metro fra \ -m 1G \ -p 443:3000/tls+http \ --scale-to-zero policy=on,cooldown-time=1500,stateful=true \ --image /node18-wingsio:latest ``` The output shows the instance address and other details: ```ansi metro: fra name: node18-wingsio-h4n8m uuid: c4d5e6f7-a8b9-0c1d-2e3f-c4d5e6f7a8b9 state: starting image: /node18-wingsio resources:  memory: 1024MiB  vcpus: 1 service:  uuid: d5e6f7a8-b9c0-1d2e-3f4a-d5e6f7a8b9c0  name: swift-cloud-gk7us4cz  domains:  - fqdn: swift-cloud-gk7us4cz.fra.unikraft.app networks: - uuid: e6f7a8b9-c0d1-2e3f-4a5b-e6f7a8b9c0d1  private-ip: 10.0.4.4  mac: 12:b0:c2:9e:01:fb timestamps:  created: just now ``` In this case, the instance name is `node18-wingsio-h4n8m` and the address is `https://swift-cloud-gk7us4cz.fra.unikraft.app`. They're different for each run. The command will deploy an `wings.io` alternative called `https://github.com/Blendlight/wings.io-clone-io`. After deploying, you can query the service using the provided URL. You can list information about the instance by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra node18-wingsio-h4n8m running /node18-wingsio 1024MiB 1 swift-cloud-gk7us4cz.fra.unikraft.app 2 minutes ago ``` When done, you can remove the instance: ```bash unikraft instances delete ``` ## Learn more - [Node.js's Documentation](https://nodejs.org/docs/latest/api/) - [Unikraft Cloud's Documentation](https://unikraft.cloud/docs/) - [Building `Dockerfile` Images with `Buildkit`](https://unikraft.org/guides/building-dockerfile-images-with-buildkit) Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: Agar.io (Node) URL: /docs/guides/node18-agario # Agar.io (Node) import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} [Agar.io](https://agar.io/) is a popular multiplayer game where players control a cell and aim to grow by consuming smaller cells while avoiding being consumed by larger ones. This guide deploys an implementation of the game using Node.js on Unikraft Cloud. To run this example, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/node18-agario/` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/node18-agario/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` When done, invoke the following command to deploy this app on Unikraft Cloud: ```bash unikraft build . --output /node18-agario:latest unikraft run --metro fra \ -m 1G \ -p 443:3000/tls+http \ --scale-to-zero policy=on,cooldown-time=2000,stateful=true \ --image /node18-agario:latest ``` The output shows the instance address and other details: ```ansi metro: fra name: node18-agario-5k2xp uuid: b3c4d5e6-f7a8-9b0c-1d2e-b3c4d5e6f7a8 state: starting image: /node18-agario resources:  memory: 1024MiB  vcpus: 1 service:  uuid: c4d5e6f7-a8b9-0c1d-2e3f-c4d5e6f7a8b9  name: dark-meadow-fj9tm6bq  domains:  - fqdn: dark-meadow-fj9tm6bq.fra.unikraft.app networks: - uuid: d5e6f7a8-b9c0-1d2e-3f4a-d5e6f7a8b9c0  private-ip: 10.0.3.5  mac: 12:b0:b1:8d:f0:ea timestamps:  created: just now ``` In this case, the instance name is `node18-agario-5k2xp` and the address is `https://dark-meadow-fj9tm6bq.fra.unikraft.app`. They're different for each run. The command will deploy an `agar.io` alternative called `https://github.com/owenashurst/agar.io-clone`. After deploying, you can query the service using the provided URL. You can list information about the instance by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra node18-agario-5k2xp running /node18-agario 1024MiB 1 dark-meadow-fj9tm6bq.fra.unikraft.app 2 minutes ago ``` When done, you can remove the instance: ```bash unikraft instances delete ``` ## Learn more - [Node.js's Documentation](https://nodejs.org/docs/latest/api/) - [Unikraft Cloud's Documentation](https://unikraft.cloud/docs/) - [Building `Dockerfile` Images with `Buildkit`](https://unikraft.org/guides/building-dockerfile-images-with-buildkit) Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: Playwright (WebKit) with Node.js URL: /docs/guides/node-playwright-webkit # Playwright (WebKit) with Node.js import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} [Playwright](https://playwright.dev/) is a framework for web testing and Automation. To run this example, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/node-playwright-webkit/` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/node-playwright-webkit/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` When done, invoke the following command to deploy this app on Unikraft Cloud: ```bash unikraft build . --output /node-playwright-webkit:latest unikraft run --metro fra \ -m 4G \ -p 443:8080/tls+http \ --scale-to-zero policy=idle,cooldown-time=1000,stateful=true \ --image /node-playwright-webkit:latest ``` The output shows the instance address and other details: ```ansi metro: fra name: node-playwright-webkit-t7r2j uuid: a2b3c4d5-e6f7-8a9b-0c1d-a2b3c4d5e6f7 state: starting image: /node-playwright-webkit resources:  memory: 4096MiB  vcpus: 1 service:  uuid: b3c4d5e6-f7a8-9b0c-1d2e-b3c4d5e6f7a8  name: silent-fog-er8np3fb  domains:  - fqdn: silent-fog-er8np3fb.fra.unikraft.app networks: - uuid: c4d5e6f7-a8b9-0c1d-2e3f-c4d5e6f7a8b9  private-ip: 10.0.6.4  mac: 12:b0:a0:7c:ef:d9 timestamps:  created: just now ``` In this case, the instance name is `node-playwright-webkit-t7r2j` and the address is `https://silent-fog-er8np3fb.fra.unikraft.app`. They're different for each run. The command will deploy the files in the current directory. It results in the creation of a remote web-based service for creating PNG screenshots of remote pages. Use the `?page=` to point the service to the remote page to screenshot. Query the service using commands such as: ```console curl "https://..unikraft.app/?page=https://google.com" -o ss-google.png curl "https://..unikraft.app/?page=https://bing.com" -o ss-bing.png ``` You can list information about the instance by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra node-playwright-webkit-t7r2j running /node-playwright-webkit 4096MiB 1 silent-fog-er8np3fb.fra.unikraft.app 2 minutes ago ``` When done, you can remove the instance: ```bash unikraft instances delete ``` ## Learn more - [Playwright's Documentation](https://playwright.dev/docs/intro) - [Unikraft Cloud's Documentation](https://unikraft.cloud/docs/) - [Building `Dockerfile` Images with `Buildkit`](https://unikraft.org/guides/building-dockerfile-images-with-buildkit) Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: Playwright (Firefox) with Node.js URL: /docs/guides/node-playwright-firefox # Playwright (Firefox) with Node.js import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} [Playwright](https://playwright.dev/) is a framework for web testing and Automation. To run this example, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/node-playwright-firefox/` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/node-playwright-firefox/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` When done, invoke the following command to deploy this app on Unikraft Cloud: ```bash unikraft build . --output /node-playwright-firefox:latest unikraft run --metro fra \ -m 4G \ -p 443:8080/tls+http \ --scale-to-zero policy=idle,cooldown-time=1000,stateful=true \ --image /node-playwright-firefox:latest ``` The output shows the instance address and other details: ```ansi metro: fra name: node-playwright-firefox-q3m9k uuid: d4e5f6a7-b8c9-0d1e-2f3a-d4e5f6a7b8c9 state: starting image: /node-playwright-firefox resources:  memory: 4096MiB  vcpus: 1 service:  uuid: e5f6a7b8-c9d0-1e2f-3a4b-e5f6a7b8c9d0  name: bright-lake-dh6xp2sq  domains:  - fqdn: bright-lake-dh6xp2sq.fra.unikraft.app networks: - uuid: f6a7b8c9-d0e1-2f3a-4b5c-f6a7b8c9d0e1  private-ip: 10.0.5.3  mac: 12:b0:9f:6b:de:c8 timestamps:  created: just now ``` In this case, the instance name is `node-playwright-firefox-q3m9k` and the address is `https://bright-lake-dh6xp2sq.fra.unikraft.app`. They're different for each run. The command will deploy the files in the current directory. It results in the creation of a remote web-based service for creating PNG screenshots of remote pages. Use the `?page=` to point the service to the remote page to screenshot. Query the service using commands such as: ```console curl "https://..unikraft.app/?page=https://google.com" -o ss-google.png curl "https://..unikraft.app/?page=https://bing.com" -o ss-bing.png ``` You can list information about the instance by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra node-playwright-firefox-q3m9k running /node-playwright-firefox 4096MiB 1 bright-lake-dh6xp2sq.fra.unikraft.app 2 minutes ago ``` When done, you can remove the instance: ```bash unikraft instances delete ``` ## Learn more - [Playwright's Documentation](https://playwright.dev/docs/intro) - [Unikraft Cloud's Documentation](https://unikraft.cloud/docs/) - [Building `Dockerfile` Images with `Buildkit`](https://unikraft.org/guides/building-dockerfile-images-with-buildkit) Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: Playwright (Chromium) with Node.js URL: /docs/guides/node-playwright-chromium # Playwright (Chromium) with Node.js import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} [Playwright](https://playwright.dev/) is a framework for web testing and Automation. To run this example, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/node-playwright-chromium/` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/node-playwright-chromium/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` When done, invoke the following command to deploy this app on Unikraft Cloud: ```bash unikraft build . --output /node-playwright-chromium:latest unikraft run --metro fra \ -m 4G \ -p 443:8080/tls+http \ --scale-to-zero policy=idle,cooldown-time=1000,stateful=true \ --image /node-playwright-chromium:latest ``` The output shows the instance address and other details: ```ansi metro: fra name: node-playwright-chromium-v5f8p uuid: a1b2c3d4-e5f6-7a8b-9c0d-a1b2c3d4e5f6 state: starting image: /node-playwright-chromium resources:  memory: 4096MiB  vcpus: 1 service:  uuid: b2c3d4e5-f6a7-8b9c-0d1e-b2c3d4e5f6a7  name: gentle-moon-cx2jh5wd  domains:  - fqdn: gentle-moon-cx2jh5wd.fra.unikraft.app networks: - uuid: c3d4e5f6-a7b8-9c0d-1e2f-c3d4e5f6a7b8  private-ip: 10.0.4.3  mac: 12:b0:8e:5a:cd:b7 timestamps:  created: just now ``` In this case, the instance name is `node-playwright-chromium-v5f8p` and the address is `https://gentle-moon-cx2jh5wd.fra.unikraft.app`. They're different for each run. The command will deploy the files in the current directory. It results in the creation of a remote web-based service for creating PNG screenshots of remote pages. Use the `?page=` to point the service to the remote page to screenshot. Query the service using commands such as: ```console curl "https://..unikraft.app/?page=https://google.com" -o ss-google.png curl "https://..unikraft.app/?page=https://bing.com" -o ss-bing.png ``` You can list information about the instance by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra node-playwright-chromium-v5f8p running /node-playwright-chromium 4096MiB 1 gentle-moon-cx2jh5wd.fra.unikraft.app 2 minutes ago ``` When done, you can remove the instance: ```bash unikraft instances delete ``` ## Learn more - [Playwright's Documentation](https://playwright.dev/docs/intro) - [Unikraft Cloud's Documentation](https://unikraft.cloud/docs/) - [Building `Dockerfile` Images with `Buildkit`](https://unikraft.org/guides/building-dockerfile-images-with-buildkit) Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: Serverless Functions: Node.js Code Execution with ROMs URL: /docs/guides/node-code-execution # Serverless Functions: Node.js Code Execution with ROMs import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} Serverless functions let you deploy small pieces of business logic without managing servers or runtimes. This example implements that model for Node.js, keeping the function code separate from the runtime image that executes it, so you update a function without rebuilding the runtime. This guide explains how to deploy TypeScript/JavaScript functions as auxiliary Read-Only Memory (ROM) images, then load them dynamically in a Node.js runtime. With Unikraft Cloud, you can create a base image with a generic runtime, package custom code as ROMs, and attach different ROMs to instances of the same base image. ## Prerequisites 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way is via [Docker](https://docs.docker.com/engine/install/). Alternatively, set up and use BuildKit directly, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/node-code-execution` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/node-code-execution/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` ## Deployment Workflow ### Package the base image First, package and push the base Node.js image (see `server.ts` for the runtime implementation): ```bash unikraft build . --output /node-code-exec:latest ``` The server implementation in `server.ts` is a simple Node.js application that listens for HTTP requests and executes JavaScript code from the attached ROM, if available. There is a little tweak—right before loading the ROM code and starting the server, it writes `1` to the special file `/uk/libukp/template_instance` (see /platform/instances#instance-templates), triggering a conversion of the instance into a template. ### Create an instance template from the base image Create an instance that uses the base Node.js image without any ROM attached: ```bash unikraft run --metro fra \ --name node-exec \ -m 512M \ --image /node-code-exec:latest ``` The output shows the instance details: ```ansi metro: fra name: node-exec uuid: 96608ed2-45e0-4c8f-8269-5d8cd3e4b41a state: starting image: /node-code-exec resources:  memory: 512MiB  vcpus: 1 networks: - uuid: 6f7a8b9c-0d1e-2f3a-4b5c-f6a7b8c9d0e1  private-ip: 10.0.5.4  mac: 12:b0:6c:3e:ab:95 timestamps:  created: just now ``` This instance is short-lived, since right before the server starts, it triggers a conversion into a template. To check that the template is ready, run: ```bash unikraft instances templates list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS CREATED fra node-exec template /node-code-exec 512MiB 1 5 seconds ago ``` ### Package the ROMs Create and push the ROMs with the code (see `rom1/fs/rom.js` and `rom2/fs/rom.ts`): ```bash unikraft build rom1/ --output /node-rom1:latest unikraft build rom2/ --output /node-rom2:latest ``` ### Create instances from the template with different ROMs attached Create a new instance from the template, attaching the first ROM: ```bash unikraft run --metro fra \ --name node-exec-rom1 \ -p 443:8080/tls+http \ --scale-to-zero policy=on,cooldown-time=1000,stateful=true \ --rom image=/node-rom1:latest,at=/rom \ --template node-exec ``` Create another instance from the same template, but with the second ROM attached: ```bash unikraft run --metro fra \ --name node-exec-rom2 \ -p 443:8080/tls+http \ --scale-to-zero policy=on,cooldown-time=1000,stateful=true \ --rom image=/node-rom2:latest,at=/rom \ --template node-exec ``` List the instances and note their FQDN values: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra node-exec-rom2 standby /node-code-exec 512MiB 1 nameless-wood-gw7pbnls.fra.unikraft.app 2 minutes ago fra node-exec-rom1 standby /node-code-exec 512MiB 1 sparkling-dawn-syowlbtj.fra.unikraft.app 3 minutes ago ``` Test both instances: ```bash curl https://sparkling-dawn-syowlbtj.fra.unikraft.app curl https://nameless-wood-gw7pbnls.fra.unikraft.app ``` ```text Bye, World! Auf Wiedersehen! ``` ## Learn more Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: Nginx URL: /docs/guides/nginx # Nginx import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} This example uses [`Nginx`](https://nginx.org), one of the most popular web servers. Nginx can be used with Unikraft / Unikraft Cloud to serve static web content. To run this example, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/nginx/` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/nginx/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` When done, invoke the following command to deploy this app on Unikraft Cloud: ```bash unikraft build . --output /nginx:latest unikraft run --metro fra \ -m 256M \ -p 443:8080/tls+http \ --scale-to-zero policy=on,cooldown-time=1000 \ --image /nginx:latest ``` The output shows the instance address and other details: ```ansi metro: fra name: nginx-67zbu uuid: 8a8bc1b9-0af6-420e-a426-190dc2da9eaa state: starting image: /nginx resources:  memory: 256MiB  vcpus: 1 service:  uuid: a942b9b5-ad17-3ffe-dcd2-ef4331f9087a  name: nameless-fog-0tvh1uov  domains:  - fqdn: nameless-fog-0tvh1uov.fra.unikraft.app networks: - uuid: 62d9bbf0-aec8-61f6-7bdb-86edf63dd068  private-ip: 10.0.3.3  mac: 12:b0:c6:23:ed:15 timestamps:  created: just now scale-to-zero:  enabled: true  policy: on  cooldown-time: 1s ``` In this case, the instance name is `nginx-67zbu` and the address is `https://nameless-fog-0tvh1uov.fra.unikraft.app`. They're different for each run. Use `curl` to query the Unikraft Cloud instance of Nginx. ```bash curl https://nameless-fog-0tvh1uov.fra.unikraft.app ``` ```text Welcome to nginx! [...] ``` You can list information about the instance by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra nginx-67zbu running /nginx 256MiB 1 nameless-fog-0tvh1uov.fra.unikraft.app 2 minutes ago ``` When done, you can remove the instance: ```bash unikraft instances delete nginx-67zbu ``` ## Customize your app To customize the Nginx app, update the files in the repository, listed below: * `Kraftfile`: the Unikraft Cloud specification * `rootfs/wwwroot/index.html`: the index page of the content served * `rootfs/conf/nginx.conf`: the Nginx configuration file Update the contents of the `rootfs/wwwroot/` directory to serve different static web content. For example, you could change the contents of `rootfs/wwwroot/index.html` to: ```html Hello

Hello, World!

``` After re-deploying the Nginx image on Unikraft Cloud, using `curl` or a browser to query it will present the new page contents. Tools like [`Jekyll`](https://jekyllrb.com/) or [`Hugo`](https://gohugo.io/) can generate the static web content located in the `rootfs/wwwroot/` offline. If required, you can also customize the configuration of Nginx in `rootfs/conf/nginx.conf`. You can set a new webroot (different than `wwwroot`), or a different internal port, or a different index page, etc. ## Learn more Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: Flask with MongoDB URL: /docs/guides/nginx-flask-mongo # Flask with MongoDB import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} [Flask](https://flask.palletsprojects.com/en/stable/) is a lightweight WSGI web application framework in Python, and [MongoDB](https://www.mongodb.com/) is a NoSQL database that stores data in JSON-like documents. This example deploys three services on Unikraft Cloud: NGINX (reverse proxy), Flask (backend), and MongoDB (database). **Credits**: This example is based on this [Awesome Compose example](https://github.com/docker/awesome-compose/tree/master/nginx-flask-mongo). ## Deployment To run this example, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/nginx-flask-mongo` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/nginx-flask-mongo/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` ## MongoDB Create a volume for MongoDB data persistence: ```bash unikraft volume create --metro fra --name mongo-data --size 1G ``` You can list the created volume by running: ```bash unikraft volume list ``` ```ansi METRO NAME STATE SIZE CREATED fra mongo-data available 1GiB just now ``` First, deploy the MongoDB instance. MongoDB is an internal service (not publicly accessible), reached via the `mongo.internal` domain: ```bash unikraft build ./mongo --output /mongo:latest unikraft run --metro fra \ -m 1024M \ --scale-to-zero policy=idle,cooldown-time=1000,stateful=true \ --domain mongo.internal \ --volume mongo-data:/data/db \ --image /mongo:latest ``` The output shows the MongoDB instance details: ```ansi metro: fra name: mongo-o3qhq uuid: 90158c53-6654-4e73-bad1-1d6ab4452001 state: starting image: /mongo resources:  memory: 1GiB  vcpus: 1 service:  name: restless-glade-l8pu2mf0  uuid: 77a04441-2479-433a-b468-32f23e475f58  domains:  - fqdn: mongo.internal volumes: - name: mongo-data  uuid: 9c7723f3-7e1f-4e06-afe6-c811240faf5a  at: /data/db networks: - uuid: 4f891227-d381-42f4-88a4-25a97b95a9e3  private-ip: 10.0.15.21  mac: 12:b0:0a:00:0f:15 timestamps:  created: just now scale-to-zero:  enabled: true  policy: idle  stateful: true  cooldown-time: 1s ``` ## Flask Next, deploy the Flask backend. It connects to MongoDB using the `MONGO_SERVER_URL` environment variable and is reached internally via `backend.internal`: ```bash unikraft build ./flask --output /flask:latest unikraft run --metro fra \ -m 1024M \ --scale-to-zero policy=on,cooldown-time=1000 \ --domain backend.internal \ --env FLASK_SERVER_PORT=9091 \ --env MONGO_SERVER_URL=mongo.internal:27017 \ --image /flask:latest ``` The output shows the Flask instance details: ```ansi metro: fra name: flask-9a68z uuid: bb6d91f7-0714-45e5-b14a-ec82a5dac36e state: starting image: /flask runtime:  env: FLASK_SERVER_PORT: 9091 MONGO_SERVER_URL: mongo.internal:27017 resources:  memory: 1GiB  vcpus: 1 service:  name: broken-bird-8isa6q21  uuid: cd9fe757-784a-49d4-8936-1b6859b3a72d  domains:  - fqdn: backend.internal networks: - uuid: 2da7f679-7067-4fb5-908b-853607d383f2  private-ip: 10.0.17.97  mac: 12:b0:0a:00:11:61 timestamps:  created: just now scale-to-zero:  enabled: true  policy: on  cooldown-time: 1s ``` ## NGINX Finally, deploy NGINX as the public-facing reverse proxy. It forwards requests to the Flask backend at `backend.internal:9091` by default. To use a different backend domain, set the `BACKEND_HOST` environment variable to the same value you pass as the Flask domain. ```bash unikraft build ./nginx --output /nginx:latest unikraft run --metro fra \ -m 512M \ -p 443:80/tls+http \ --scale-to-zero policy=on,cooldown-time=1000 \ -e BACKEND_HOST=backend.internal \ --image /nginx:latest ``` The output shows the NGINX instance details including its public FQDN: ```ansi metro: fra name: nginx-jnpwi uuid: 57f64e99-bd06-46fd-98f4-26b64751623e state: starting image: /nginx resources:  memory: 512MiB  vcpus: 1 service:  name: snowy-river-gotjeojl  uuid: 287ee3b8-43bc-47d1-a88e-4d6c72d2d682  domains:  - fqdn: snowy-river-gotjeojl.fra.unikraft.app networks: - uuid: 107e4a03-e285-4d1d-84cb-24f86d7af875  private-ip: 10.0.14.201  mac: 12:b0:0a:00:0e:c9 timestamps:  created: just now scale-to-zero:  enabled: true  policy: on  cooldown-time: 1s ``` You can list all deployed instances with: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE MEMORY VCPUS FQDN CREATED fra nginx-jnpwi standby /nginx 512MiB 1 snowy-river-gotjeojl.fra.unikraft.app 11 minutes ago fra flask-9a68z standby /flask 1GiB 1 backend.internal 12 minutes ago fra mongo-o3qhq standby /mongo 1GiB 1 mongo.internal 14 minutes ago ``` ## Test the deployment The FQDN of the NGINX instance can be found in the `FQDN` column of the `unikraft instances list` output above. Use `curl` to query it (replace with your actual FQDN): ```bash curl https:// ``` ```text Hello from the MongoDB client! ``` ## Clean up When done, remove the instances and volume: ```bash unikraft instances delete mongo-o3qhq flask-9a68z nginx-jnpwi unikraft volume delete mongo-data ``` ## Learn more Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). - [Flask Documentation](https://flask.palletsprojects.com/en/stable/) - [MongoDB Documentation](https://www.mongodb.com/docs/) - [Nginx Documentation](https://nginx.org/en/docs/) - [Awesome Compose](https://github.com/docker/awesome-compose) - [Unikraft Cloud's Documentation](https://unikraft.cloud/docs/) - [Building `Dockerfile` Images with `Buildkit`](https://unikraft.org/guides/building-dockerfile-images-with-buildkit) {/* vale on */} --- ## Document: Neo4j URL: /docs/guides/neo4j # Neo4j import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} This guide shows you how to use [Neo4j](https://neo4j.com), one of the most popular open source graph databases. To run this example, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/neo4j/` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/neo4j/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` When done, invoke the following command to deploy this app on Unikraft Cloud: ```bash unikraft build . --output /neo4j:latest unikraft run --metro fra \ -m 2G \ -p 443:7474/tls+http \ -p 7687:7687/tls \ --scale-to-zero policy=idle,cooldown-time=4000,stateful=true \ -e "NEO4J_HOME=/var/lib/neo4j" \ -e "JAVA_HOME=/opt/java/openjdk" \ -e "LANG=C.UTF-8" \ -e "NEO4J_EDITION=community" \ -e "PATH=/var/lib/neo4j/bin:/opt/java/openjdk/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" \ --image /neo4j:latest ``` :::note You can also set a custom password for the default neo4j user by adding the following env var to the run command `-e "NEO4J_AUTH_PASSWORD=..."` ::: The output shows the instance address and other details: ```ansi metro: fra name: neo4j-t117i uuid: c59f865e-e954-4bf8-8da8-7ee6f1c1aab2 state: starting image: /neo4j resources:  memory: 2GiB  vcpus: 1 service:  name: fragrant-fog-pj1gi4jl  uuid: a3d29dfa-650b-4e8c-a8e6-055f59dd4c92  domains:  - fqdn: fragrant-fog-pj1gi4jl.fra.unikraft.app networks: - uuid: 1a65f2c3-cbfd-48bf-b8db-5cb90ed62a7c  private-ip: 10.0.1.73  mac: 12:b0:0a:00:01:49 timestamps:  created: just now scale-to-zero:  enabled: true  policy: idle  stateful: true  cooldown-time: 4s ``` In this case, the instance name is `neo4j-t117i` and the address is `https://fragrant-fog-pj1gi4jl.fra.unikraft.app` They're different for each run. Use `curl` to query the Unikraft Cloud Neo4j instance: ```bash curl https://fragrant-fog-pj1gi4jl.fra.unikraft.app ``` ```text {"bolt_routing":"neo4j://fragrant-fog-pj1gi4jl.fra.unikraft.app:7687","query":"https://fragrant-fog-pj1gi4jl.fra.unikraft.app:7687/db/{databaseName}/query/v2","transaction":"https://fragrant-fog-pj1gi4jl.fra.unikraft.app/db/{databaseName}/tx","bolt_direct":"bolt://fragrant-fog-pj1gi4jl.fra.unikraft.app:7687","neo4j_version":"2026.04.0","neo4j_edition":"community"} ``` Or even better, point a browser at it 😀. Make sure you have the protocol set to `neo4j+s` when logging in. You can also try connecting to the instance using the `cypher-shell` CLI ```bash cypher-shell -a neo4j+s://fragrant-fog-pj1gi4jl.fra.unikraft.app:7687 -u neo4j ``` You can list information about the instance by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra neo4j-t117i standby /neo4j 2GiB 1 fragrant-fog-pj1gi4jl.fra.unikraft.app just now ``` When done, you can remove the instance: ```bash unikraft instances delete neo4j-t117i ``` ## Using volumes You can use [volumes](/platform/volumes) for data persistence for your neo4j instance. For that you would first create a volume: ```bash unikraft volume create --set metro=fra --set name=neo4j-store --set size=512M ``` Then start the neo4j instance and mount that volume: ```bash unikraft build . --output /neo4j:latest unikraft run --metro fra \ -m 2G \ -p 443:7474/tls+http \ -p 7687:7687/tls \ --scale-to-zero policy=idle,cooldown-time=4000,stateful=true \ -e "NEO4J_HOME=/var/lib/neo4j" \ -e "JAVA_HOME=/opt/java/openjdk" \ -e "LANG=C.UTF-8" \ -e "NEO4J_EDITION=community" \ -e "PATH=/var/lib/neo4j/bin:/opt/java/openjdk/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" \ --volume neo4j-store:/data \ --image /neo4j:latest ``` ## Customize your app To customize the app, update the files in the repository, listed below: * `Kraftfile`: the Unikraft Cloud specification, including command-line arguments * `Dockerfile`: In case you need to add files to your instance's rootfs ## Learn more Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: MySQL URL: /docs/guides/mysql # MySQL import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} This guide shows you how to use [MySQL](https://www.mysql.com), one of the most popular open source relational databases. To run it, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/mysql/` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/mysql ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` When done, invoke the following command to deploy this app on Unikraft Cloud: ```bash unikraft build . --output /mysql:latest unikraft run --metro fra \ -m 1G \ -p 3306:3306/tls \ --scale-to-zero policy=idle,cooldown-time=1000,stateful=true \ -e MYSQL_ROOT_PASSWORD="unikraft" \ --image /mysql:latest ``` The output shows the instance address and other details: ```ansi metro: fra name: mysql-b3mlh uuid: 19479d80-0995-4ed2-b08e-86b29eda3f3 state: starting image: /mysql runtime: env: MYSQL_ROOT_PASSWORD: * resources: memory: 1GiB vcpus: 1 service: name: purple-wood-hqb663gu uuid: d2bcd1c0-c980-4d5b-9777-2bec2b0d0645 domains: - fqdn: purple-wood-hqb663gu.fra.unikraft.app networks: - uuid: 3e3a6cd5-ca66-41ec-8c28-5280b049f732 private-ip: 10.0.14.181 mac: 12:b0:0a:00:0e:b5 timestamps: created: just now scale-to-zero: enabled: true policy: idle stateful: true cooldown-time: 1s ``` In this case, the instance name is `mysql-b3mlh` which is different for each run. To test the deployment, first forward the port with the `socat` command. ```bash socat TCP-LISTEN:3306,reuseaddr,fork OPENSSL:purple-wood-hqb663gu.fra.unikraft.app:3306,verify=0 ``` You can now, on a separate console, use the `mysql` command line tool to test that the set up works: ```bash mysql -h 127.0.0.1 --ssl-mode=DISABLED -u root -punikraft mysql <<< "select count(*) from user" ``` Or use the `mariadb` client command line tool: ```bash mariadb -h 127.0.0.1 --ssl=OFF -u root -punikraft mysql <<< "select count(*) from user" ``` You should see output such as: ```ansi count(*) 6 ``` To disconnect, kill the `socat` command using `Ctrl+c`. :::note This guide uses `socat` for port forwarding only when a service doesn't support TLS and isn't HTTP-based (TLS/SNI determines the correct instance to send traffic to). Also note that port forwarding isn't needed when connecting via an instance's private IP/FQDN. For example, when a MySQL instance serves as a database server to another instance that acts as a frontend and which **does** support TLS. ::: You can list information about the instance by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra mysql-b3mlh running /mysql 1024MiB 1 purple-wood-hqb663gu.fra.unikraft.app 2 minutes ago ``` When done, you can remove the instance: ```bash unikraft instance remove mysql-b3mlh ``` :::tip This example uses the [`idle` scale-to-zero policy](/api/platform/v1/instances#scaletozero_policy) by default. ::: ## Using volumes You can use [volumes](/platform/volumes) for data persistence for your MySQL instance. For that you would first create a volume: ```bash unikraft volume create --set metro=fra --set name=mysql-store --set size=512M ``` Then start the MySQL instance and mount that volume: ```bash unikraft build . --output /mysql:latest unikraft run --metro fra \ -m 1G \ -p 3306:3306/tls \ --scale-to-zero policy=idle,cooldown-time=1000,stateful=true \ -e MYSQL_ROOT_PASSWORD="unikraft" \ --volume mysql-store:/var/lib \ --image /mysql:latest ``` ## Learn more Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: MongoDB URL: /docs/guides/mongodb # MongoDB import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} This guide shows you how to use [MongoDB](https://www.mongodb.com), a source-available, cross-platform, document-oriented database program. To run it, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/mongodb/` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/mongodb/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` When done, invoke the following command to deploy this app on Unikraft Cloud: ```bash unikraft build . --output /mongodb:latest unikraft run --metro fra \ -m 1G \ -p 27017:27017/tls \ --scale-to-zero policy=idle,cooldown-time=1000,stateful=true \ --image /mongodb:latest ``` The output shows the instance address and other details: ```ansi metro: fra name: mongodb-6tiuu uuid: 99779597-0bdb-4160-b902-a160c3ab4b2a state: starting image: /mongodb resources:  memory: 1024MiB  vcpus: 1 service:  uuid: 91309c0c-fd6f-271d-cab6-2694b0991fbe  name: bold-brook-khkwv7of  domains:  - fqdn: bold-brook-khkwv7of.fra.unikraft.app networks: - uuid: 24cc79e5-bb3f-cdcd-fd5d-e4605015a228  private-ip: 10.0.6.4  mac: 12:b0:d7:7b:83:97 timestamps:  created: just now ``` In this case, the instance name is `mongodb-6tiuu` and the the address is `bold-brook-khkwv7of.fra.unikraft.app` which is different for each run. You can use the mongosh client to connect to the server: ```console mongosh "mongodb://bold-brook-khkwv7of.fra.unikraft.app:27017/?tls=true" ``` You should see output like: ```console Current Mongosh Log ID: 69d7a077dc5d28998344ba88 Connecting to: mongodb://bold-brook-khkwv7of.fra.unikraft.app:27017/?tls=true&directConnection=true&appName=mongosh+2.8.2 Using MongoDB: 6.0.13 Using Mongosh: 2.8.2 For mongosh info see: https://docs.mongodb.com/mongodb-shell/ To help improve our products, anonymous usage data is collected and sent to MongoDB periodically (https://www.mongodb.com/legal/privacy-policy). You can opt-out by running the disableTelemetry() command. test> ``` You can list information about the instance by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra mongodb-6tiuu running /mongodb 1.0GiB 1 bold-brook-khkwv7of.fra.unikraft.app 2 minutes ago ``` When done, you can remove the instance: ```bash unikraft instances delete mongodb-6tiuu ``` ## Using volumes You can use [volumes](/platform/volumes) for data persistence for your MongoDB instance. For that you would first create a volume: ```bash unikraft volume create --set metro=fra --set name=mongodb-store --set size=512M ``` Then start the MongoDB instance and mount that volume: ```bash unikraft build . --output /mongodb:latest unikraft run --metro fra \ -m 1G \ -p 27017:27017/tls \ --scale-to-zero policy=idle,cooldown-time=1000,stateful=true \ --volume mongodb-store:/data/db \ --image /mongodb:latest ``` ## Customize your app To customize the app, update the files in the repository, listed below: * `Kraftfile`: the Unikraft Cloud specification, including command-line arguments * `Dockerfile`: In case you need to add files to your instance's rootfs ## Learn more Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: Minio URL: /docs/guides/minio # Minio import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} This guide shows you how to use [MinIO](https://min.io), a High Performance Object Storage which is Open Source, Amazon S3 compatible, Kubernetes Native and works for cloud native workloads like AI. To run it, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/minio/` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/minio/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` When done, invoke the following command to deploy this app on Unikraft Cloud: ```bash unikraft build . --output /minio:latest unikraft run --metro fra \ -m 512M \ -p 443:9001/tls+http \ -p 9000:9000/tls \ --scale-to-zero policy=on,cooldown-time=1000 \ --image /minio:latest ``` The output shows the instance address and other details: ```ansi metro: fra name: minio-w2my8 uuid: 31e691ad-05a0-48b6-ad49-7f79da8e1754 state: starting image: /minio resources:  memory: 512MiB  vcpus: 1 service:  uuid: 8fdad5ff-9276-5f35-f94e-3d9d9b244a15  name: icy-bird-tregaga9  domains:  - fqdn: icy-bird-tregaga9.fra.unikraft.app networks: - uuid: f3a7d1f6-b1ca-68d0-6b45-7e9179ad0966  private-ip: 10.0.6.4  mac: 12:b0:44:5e:b0:54 timestamps:  created: just now ``` In this case, the instance name is `minio-w2my8` and the address is `https://icy-bird-tregaga9.fra.unikraft.app`. They're different for each run. To test, point your browser at the address. The default account/password are `minioadmin/minioadmin`. You can list information about the instance by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra minio-w2my8 running /minio 512MiB 1 icy-bird-tregaga9.fra.unikraft.app 2 minutes ago ``` When done, you can remove the instance: ```bash unikraft instances delete minio-w2my8 ``` ## Customize your app To customize the app, update the files in the repository, listed below: * `Kraftfile`: the Unikraft Cloud specification, including command-line arguments * `Dockerfile`: In case you need to add files to your instance's rootfs ## Learn more Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: Game Servers: Minecraft URL: /docs/guides/minecraft # Game Servers: Minecraft import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} Game servers have expensive startup paths, loading assets and initializing world state, and long idle stretches between sessions, which usually means paying for a server that runs all day. Minecraft is a strong example of the pattern: JVM warm-up and world initialization happen once into a template snapshot, and an instance pauses when the last player disconnects, then resumes from that snapshot on the next connection. This example runs a Minecraft Java server on Unikraft Cloud with: - A reusable base image (`minecraft:latest`) built from [itzg/minecraft-server](https://hub.docker.com/r/itzg/minecraft-server) (Java 25) - Configuration overrides through auxiliary ROMs (optional) - Template-based startup for faster instance creation - TLS-exposed Minecraft and SSH endpoints ## Prerequisites 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/minecraft/` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/minecraft/ ``` 3. Review and adjust the base server settings in [`base/.env`](./base/.env). You can check out [this documentation](https://docker-minecraft-server.readthedocs.io/en/latest/) for available configuration options. Make sure to also set your `PUBKEY` for SSH access, and optionally set `TEMPLATE_WITH_WORLD` if you want the template to include the world (see below). Optionally, create per-config overrides in `/.env` (for example [`bingo/.env`](./bingo/.env)). All the `.env` files are packaged as [auxiliary ROMs](/features/roms) and mounted at `/rom/`. Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` ## Deployment Workflow ### Package the base image First, package and push the base Minecraft server image: ```bash unikraft build . --output /minecraft:latest ``` The image is built from the Docker image [itzg/minecraft-server](https://hub.docker.com/r/itzg/minecraft-server) and includes a few tweaks: - The entrypoint is wrapped around a custom [`wrapper.sh`](./wrapper.sh) script that: - Starts an SSH server - Loads the environment configuration from the attached ROM(s) - Disables scale-to-zero before executing the original entrypoint - The server configuration scripts from the original image are patched (see [patches/](./patches)) to trigger the template snapshot before the full warm-up of the server, which allows faster instance creation from the template. Scale-to-zero is enabled after server initialization. ### Create an instance template from the base image This step is required to enable fast startups from the base image, which is particularly important for a Minecraft server given the long initialization time. If you only customized the server using the base [`.env`](./base/.env) file, create the instance with one ROM: ```bash unikraft run --metro fra \ --name minecraft-tpl \ -m 4096M \ --vcpus 4 \ --rom dir=base,at=/rom/base \ --image /minecraft:latest ``` The output shows the instance details: ```ansi metro: fra name: minecraft-tpl uuid: d9d7be54-5495-45d0-b5af-48be7f30d1d8 state: starting image: /minecraft resources:  memory: 4GiB  vcpus: 4 roms: - name: rom  image: c99125f6-6b8c-4f94-b94c-e2d9551b253b  at: /rom networks: - uuid: 123b0ed0-9f2f-417c-9307-34424d80e4cb  private-ip: 10.0.0.29  mac: 12:b0:0a:00:00:1d timestamps:  created: just now ``` If you also have per-config overrides (for example in [`bingo/.env`](./bingo/.env)), create the instance with multiple ROMs: ```bash unikraft run --metro fra \ --name minecraft-tpl \ -m 4096M \ --vcpus 4 \ --rom dir=base,at=/rom/base \ --rom dir=bingo,at=/rom/bingo \ --image /minecraft:latest ``` The instance will run until initialization is complete, then it will be snapshotted as a template and immediately deleted. The exact moment of snapshotting can be configured in [`patches/start-finalExec`](./patches/start-finalExec), but it can also be configured by the following environment variable: - If `TEMPLATE_WITH_WORLD=true`, then the snapshot will be triggered **after** world generation - Otherwise, the snapshot will be triggered **after** extracting the jar files and writing configuration files, **before** world generation You can follow the logs of the instance to check the progress: ```bash unikraft instances logs minecraft-tpl -f ``` Once they stop, the template is ready and you can check it with: ```bash unikraft instances templates list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS CREATED fra minecraft-tpl template /minecraft 4GiB 4 5 seconds ago ``` ### Create an instance from the template You can now create new instances from the template, which will boot much faster than the original base image: ```bash unikraft run --metro fra \ --name minecraft \ -p 2222:2222/tls \ -p 25565:25565/tls \ --scale-to-zero policy=on,cooldown-time=5000,stateful=true \ --template minecraft-tpl ``` The output shows the instance address and other details: ```ansi metro: fra name: minecraft uuid: 56787b5d-14fb-4908-88d0-e9609d1ac1e0 state: running image: /minecraft resources:  memory: 4GiB  vcpus: 4 service: hidden-water-ewr8l9sp roms: - name: rom  image: c99125f6-6b8c-4f94-b94c-e2d9551b253b  at: /rom networks: - uuid: b6c7d615-7e74-49d6-a8b6-14c39323233b  private-ip: 10.0.0.29  mac: 12:b0:0a:00:00:1d timestamps:  created: just now scale-to-zero: policy=on,stateful=true,cooldown-time=5s ``` In this case, the instance name is `minecraft` and the address is `https://hidden-water-ewr8l9sp.fra.unikraft.app`. A sample log output of the server getting ready: ```bash unikraft instances logs minecraft -f ``` ```text [12:59:45] [Server thread/INFO]: Preparing spawn area: 100% [12:59:45] [Server thread/INFO]: Time elapsed: 3641 ms [12:59:45] [Server thread/INFO]: Done (3.850s)! For help, type "help" [12:59:45] [Server thread/INFO]: Starting remote control listener [12:59:45] [Server thread/INFO]: Thread RCON Listener started [12:59:45] [Server thread/INFO]: RCON running on 0.0.0.0:25575 [12:59:45] [Server thread/INFO]: Saving chunks for level 'ServerLevel[world]'/minecraft:overworld [12:59:45] [Server thread/INFO]: Saving chunks for level 'ServerLevel[world]'/minecraft:the_end [12:59:45] [Server thread/INFO]: Saving chunks for level 'ServerLevel[world]'/minecraft:the_nether [12:59:45] [Server thread/INFO]: ThreadedAnvilChunkStorage (world): All chunks are saved [12:59:45] [Server thread/INFO]: ThreadedAnvilChunkStorage (DIM1): All chunks are saved [12:59:45] [Server thread/INFO]: ThreadedAnvilChunkStorage (DIM-1): All chunks are saved [12:59:45] [Server thread/INFO]: ThreadedAnvilChunkStorage: All dimensions are saved ``` ## Connect to Minecraft and SSH At the moment, Unikraft Cloud can only expose these services over TLS. Since Minecraft clients and SSH clients do not support TLS, you can use `socat` to terminate TLS locally and expose plain TCP endpoints for your clients to connect to. ### Minecraft proxy ```bash socat TCP-LISTEN:25565,reuseaddr,fork OPENSSL:hidden-water-ewr8l9sp.fra.unikraft.app:25565,verify=0 ``` Then connect your Minecraft client to `localhost:25565`. ### SSH proxy ```bash socat TCP-LISTEN:2222,reuseaddr,fork OPENSSL:hidden-water-ewr8l9sp.fra.unikraft.app:2222,verify=0 ``` Then connect your SSH client to `localhost:2222` with the username `root` and the SSH key you configured in the `.env` file. ## Administration ### RCON SSH into the instance (via the local TLS proxy), then run: ```bash rcon-cli --host 127.0.0.1 ``` You can then execute any Minecraft server command through the RCON interface, for example: ```bash say Hello from Unikraft Cloud! ``` You can also give yourself operator permissions to execute these commands in-game: ```bash /op ``` ### Whitelist in offline mode Offline mode does not produce online UUIDs. If you whitelist players manually, compute UUIDs per username and update `/data/whitelist.json`. ```python import hashlib, uuid; h = hashlib.md5(b'OfflinePlayer:').digest(); u = uuid.UUID(bytes=h); print(u) ``` Format: ```json [ { "uuid": "", "name": "" } ] ``` ## Learn more Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: Memcached URL: /docs/guides/memcached1.6 # Memcached import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} This guide shows you how to use [Memcached](https://memcached.org). Memcached is an in-memory key-value store for small chunks of arbitrary data (strings, objects) from results of database calls, API calls, or page rendering. To run it, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/memcached1.6/` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/memcached1.6/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` When done, invoke the following command to deploy this app on Unikraft Cloud: ```bash unikraft build . --output /memcached16:latest unikraft run --metro fra \ -m 256M \ -p 11211:11211/tls \ --scale-to-zero policy=off \ --image /memcached16:latest ``` The output shows the instance address and other details: ```ansi metro: fra name: memcached16-arkv7 uuid: da436eca-bc64-46d7-a04c-72832652b10e state: starting image: /memcached16 resources:  memory: 256MiB  vcpus: 1 service:  uuid: ce58c122-62e5-b82d-955d-63053dbc13ec  name: weathered-smoke-hehsdinv  domains:  - fqdn: weathered-smoke-hehsdinv.fra.unikraft.app networks: - uuid: 1e9e37f9-1ba5-7c92-350e-f399db32d93d  private-ip: 10.0.6.5  mac: 12:b0:e3:15:81:6b timestamps:  created: just now ``` In this case, the instance name is `memcached16-arkv7` which is different for each run. To test the deployment, first forward the port with the `socat` command: ```bash socat TCP-LISTEN:11211,reuseaddr,fork OPENSSL:weathered-smoke-hehsdinv.fra.unikraft.app:11211,verify=0 ``` Now, on a separate console, run the following commands to test that it works (you should see output when incrementing): ```console telnet 127.0.0.1 11211 set test 0 0 1 0 incr test 1 incr test 1 ``` To exit telnet run: ```console Ctrl + ] Ctrl + C ``` To disconnect, kill the `socat` command with ctrl-C. :::note This guide uses `socat` for port forwarding only when a service doesn't support TLS and isn't HTTP-based (TLS/SNI determines the correct instance to send traffic to). Also note that port forwarding isn't needed when connecting via an instance's private IP/FQDN. For example, when a Memcached instance serves as a cache server to another instance that acts as a frontend and which **does** support TLS. ::: You can list information about the instance by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra memcached16-arkv7 running /memcached16 256MiB 1 weathered-smoke-hehsdinv.fra.unikraft… 2 minutes ago ``` When done, you can remove the instance: ```bash unikraft instances delete memcached16-arkv7 ``` ## Customize your app To customize the app, update the files in the repository, listed below: * `Kraftfile`: the Unikraft Cloud specification, including command-line arguments * `Dockerfile`: In case you need to add files to your instance's rootfs ## Learn more Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: Simple MCP Server URL: /docs/guides/mcp-server-simple # Simple MCP Server import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} This example demonstrates how to build a minimal custom MCP (Model Context Protocol) server from scratch using [FastMCP 2.0](https://github.com/jlowin/fastmcp). This is a great starting point for building your own custom MCP servers with business-specific tools and logic. ## Running on Unikraft Cloud To run this MCP server on Unikraft Cloud: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 1. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/mcp-server-simple/` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/mcp-server-simple/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` When done, invoke the following command to deploy this app on Unikraft Cloud: ```bash unikraft build . --output /mcp-server-simple:latest unikraft run --metro fra \ -m 512M \ -p 443:8080/tls+http \ --scale-to-zero policy=on,cooldown-time=1000,stateful=true \ --image /mcp-server-simple:latest ``` The output shows your instance details: ```ansi metro: fra name: mcp-server-simple-bbdcb uuid: e87d3591-3497-4f30-bd76-1dc886059647 state: starting image: /mcp-server-simple resources:  memory: 512MiB  vcpus: 1 service:  uuid: 3cfc78af-748d-c6ba-5bfb-427b3bd7e9aa  name: cool-paper-b6mht7jv  domains:  - fqdn: cool-paper-b6mht7jv.fra.unikraft.app networks: - uuid: 6a712117-5b67-d89a-7e2e-4a57e1732724  private-ip: 10.0.0.193  mac: 12:b0:39:2b:a3:15 timestamps:  created: just now ``` In this case, the instance name is `mcp-server-simple-bbdcb` and the service `cool-paper-b6mht7jv`. They're different for each run. For testing, you can use the example client included in this directory. First, [install `uv`](https://docs.astral.sh/uv/getting-started/installation) if you haven't already, then run: ```bash export MCP_SERVER_URL=https://cool-paper-b6mht7jv.fra.unikraft.app/mcp uv run client.py ``` ```bash Connecting to https://cool-paper-b6mht7jv.fra.unikraft.app/mcp... --- Listing Tools --- Name: get_weather Description: Get current weather for a city. Args: city: Name of the city Returns: Weather information including temperature, conditions, and humidity Name: get_time Description: Get current time in a timezone. ... ``` You can list information about the instance by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra mcp-server-simple-bbdcb standby /mcp-server-simple 512MiB 1 cool-paper-b6mht7jv.fra.unikraft.app 2 minutes ago ``` When done, you can delete the instance with: ```bash unikraft instances delete mcp-server-simple-bbdcb ``` ## Available tools This MCP Server provides the following tools: * **get_weather**: Get simulated weather for a city * **get_time**: Get current time in a timezone * **calculate**: Perform basic arithmetic operations (add, subtract, multiply, divide) ## Building your own tools This example shows the basic structure of an MCP server using FastMCP. To add your own tools: 1. **Define the tool** using the `@mcp.tool()` decorator: * Add type hints for parameters * Include a docstring describing what the tool does * Parameters are automatically converted to JSON schema 1. **Write the tool** function: * Process the parameters * Return a string result (or raise an exception for errors) 1. **Test locally** before deploying: ```bash pip install -r requirements.txt python server.py ``` Example: ```python from fastmcp import FastMCP mcp = FastMCP("My Custom Server") @mcp.tool() def my_custom_tool(param1: str, param2: int) -> str: """Description of what this tool does. Args: param1: Description of first parameter param2: Description of second parameter Returns: Result description """ # Your implementation here return f"Processed {param1} with {param2}" if __name__ == "__main__": mcp.run(transport="http", host="0.0.0.0", port=8080) ``` ## Architecture The server uses FastMCP, a high-level framework for building MCP servers that features: * **HTTP Transport**: Built-in Streamable HTTP transport * **Type-Safe**: Automatic JSON schema generation from Python type hints * **Simple API**: Decorator-based tool definitions This provides: * Standard MCP protocol support * Automatic tool schema generation * Built-in HTTP server * Easy deployment to any platform ## Learn more * [MCP Documentation](https://modelcontextprotocol.io/) * [FastMCP documentation](https://gofastmcp.com/getting-started/welcome) * [Building MCP Servers](https://modelcontextprotocol.io/docs/building-servers) * [Unikraft Cloud Documentation](https://unikraft.com/docs/) * [Building `Dockerfile` Images with `Buildkit`](https://unikraft.org/guides/building-dockerfile-images-with-buildkit) Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: MCP Servers: ArXiv MCP Server URL: /docs/guides/mcp-server-arxiv # MCP Servers: ArXiv MCP Server import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} [MCP (Model Context Protocol)](https://modelcontextprotocol.io/docs/getting-started/intro) servers are the bridge between AI assistants and real-world resources such as file systems, databases, APIs, and search engines. AI interactions are bursty, so a server that scales to zero when unused and wakes in milliseconds only costs you for the queries it answers. This example demonstrates how to deploy the [ArXiv MCP server](https://github.com/blazickjp/arxiv-mcp-server) on Unikraft Cloud. The ArXiv MCP Server is a third-party library that provides stdio-based MCP tools for accessing arXiv research papers. This example uses [FastMCP 2.0](https://github.com/jlowin/fastmcp) to create a proxy MCP server that exposes these tools over streamable HTTP. The server gives AI agents and assistants the ability to: * Search for papers with filters for date ranges and categories * Download and read paper content * List downloaded papers * Analyze papers using specialized prompts. ## Running on Unikraft Cloud To run this MCP server on Unikraft Cloud: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 1. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/mcp-server-arxiv/` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/mcp-server-arxiv/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` When done, invoke the following command to deploy this app on Unikraft Cloud: ```bash unikraft build . --output /mcp-server-arxiv:latest unikraft run --metro fra \ -m 2G \ -p 443:8080/tls+http \ --scale-to-zero policy=on,cooldown-time=1000,stateful=true \ --image /mcp-server-arxiv:latest ``` The output shows your instance details: ```ansi metro: fra name: mcp-server-arxiv-l7l24 uuid: 1a721bb8-4472-4149-9870-789b1df5f80a state: starting image: /mcp-server-arxiv resources:  memory: 2048MiB  vcpus: 1 service:  uuid: 94b10356-3df8-b2fa-cd17-60ca8193c86c  name: billowing-breeze-nuusy7l2  domains:  - fqdn: billowing-breeze-nuusy7l2.fra.unikraft.app networks: - uuid: e6754486-5398-bb06-420e-de23ed73da3f  private-ip: 10.0.1.149  mac: 12:b0:26:13:a0:89 timestamps:  created: just now ``` In this case, the instance name is `mcp-server-arxiv-l7l24` and the service `billowing-breeze-nuusy7l2`. They're different for each run. For testing, you can use the example client included in this directory. First, [install `uv`](https://docs.astral.sh/uv/getting-started/installation/) if you haven't already, then run: ```bash export MCP_SERVER_URL=https://billowing-breeze-nuusy7l2.fra.unikraft.app/mcp uv run client.py ``` ```bash Connecting to https://billowing-breeze-nuusy7l2.fra.unikraft.app/mcp... --- Listing Tools --- Name: search_papers Description: Search for papers on arXiv with advanced filtering and query optimization. ... ``` You can list information about the instance by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra mcp-server-arxiv-l7l24 standby /mcp-server-arxiv 2.0GiB 1 billowing-breeze-nuusy7l2.fra.unikraf… 2 minutes ago ``` When done, you can delete the instance with: ```bash unikraft instances delete mcp-server-arxiv-l7l24 ``` ## Using volumes You can use [volumes](/platform/volumes) for data persistence. For that you would first create a volume: ```bash unikraft volume create --set metro=fra --set name=mcp-server-arxiv-data --set size=500M ``` Then start the MCP server instance and mount that volume (while specifying the storage path): ```bash unikraft build . --output /mcp-server-arxiv:latest unikraft run --metro fra \ -m 2G \ -p 443:8080/tls+http \ --scale-to-zero policy=on,cooldown-time=1000,stateful=true \ -v mcp-server-arxiv-data:/volume \ --image /mcp-server-arxiv:latest \ -- "/usr/local/bin/python /src/server.py --storage-path /volume" ``` ## Available tools The ArXiv MCP Server provides the following tools: * **search_papers**: Query arXiv papers with filters for date ranges and categories * **download_paper**: Download a paper by its arXiv ID * **list_papers**: View all downloaded papers * **read_paper**: Access the content of a downloaded paper ## Learn more * [ArXiv MCP Server Documentation](https://github.com/blazickjp/arxiv-mcp-server) * [FastMCP documentation](https://gofastmcp.com/getting-started/welcome) * [Model Context Protocol](https://modelcontextprotocol.io/) * [Unikraft Cloud Documentation](https://unikraft.com/docs/) * [Building `Dockerfile` Images with `Buildkit`](https://unikraft.org/guides/building-dockerfile-images-with-buildkit) Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: MariaDB URL: /docs/guides/mariadb # MariaDB import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} This guide shows you how to use [MariaDB](https://mariadb.org), one of the most popular open source relational databases. To run it, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/mariadb/` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/mariadb ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` When done, invoke the following command to deploy this app on Unikraft Cloud: ```bash unikraft build . --output /mariadb:latest unikraft run --metro fra \ -m 1G \ -p 3306:3306/tls \ --scale-to-zero policy=idle,cooldown-time=1000,stateful=true \ -e MARIADB_ROOT_PASSWORD="unikraft" \ --image /mariadb:latest ``` The output shows the instance address and other details: ```ansi metro: fra name: mariadb-w2g2z uuid: ba696c22-adff-4fba-88b9-d1b790ca2357 state: starting image: /mariadb resources:  memory: 1024MiB  vcpus: 1 service:  uuid: ca8ae7e9-3767-85f6-3d70-b77bcf894a7c  name: twilight-sun-82lt4ddk  domains:  - fqdn: twilight-sun-82lt4ddk.fra.unikraft.app networks: - uuid: 935ba4ef-39c2-07e7-d2ae-8cd3c9aae07c  private-ip: 10.0.6.3  mac: 12:b0:ee:29:71:e4 timestamps:  created: just now ``` In this case, the instance name is `mariadb-w2g2z` which is different for each run. To test the deployment, first forward the port with the `socat` command. ```bash socat TCP-LISTEN:3306,reuseaddr,fork OPENSSL:twilight-sun-82lt4ddk.fra.unikraft.app:3306,verify=0 ``` You can now, on a separate console, use the `mysql` command line tool to test that the set up works: ```bash mysql -h 127.0.0.1 --ssl-mode=DISABLED -u root -punikraft mysql <<< "select count(*) from user" ``` Or use the `mariadb` client command line tool: ```bash mariadb -h 127.0.0.1 --ssl=OFF -u root -punikraft mysql <<< "select count(*) from user" ``` You should see output such as: ```ansi count(*) 6 ``` To disconnect, kill the `socat` command using `Ctrl+c`. :::note This guide uses `socat` for port forwarding only when a service doesn't support TLS and isn't HTTP-based (TLS/SNI determines the correct instance to send traffic to). Also note that port forwarding isn't needed when connecting via an instance's private IP/FQDN. For example, when a MariaDB instance serves as a database server to another instance that acts as a frontend and which **does** support TLS. ::: You can list information about the instance by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra mariadb-w2g2z running /mariadb 1024MiB 1 twilight-sun-82lt4ddk.fra.unikraft.app 2 minutes ago ``` When done, you can remove the instance: ```bash unikraft instance remove mariadb-w2g2z ``` :::tip This example uses the [`idle` scale-to-zero policy](/api/platform/v1/instances#scaletozero_policy) by default (see the `labels` section in the `Kraftfile`). ::: ## Using volumes You can use [volumes](/platform/volumes) for data persistence for your MariaDB instance. For that you would first create a volume: ```bash unikraft volume create --set metro=fra --set name=mariadb-store --set size=512M ``` Then start the MariaDB instance and mount that volume: ```bash unikraft build . --output /mariadb:latest unikraft run --metro fra \ -m 1G \ -p 3306:3306/tls \ --scale-to-zero policy=idle,cooldown-time=1000,stateful=true \ -e MARIADB_ROOT_PASSWORD="unikraft" \ --volume mariadb-store:/var/lib \ --image /mariadb:latest ``` ## Customize your app To customize the app, update the files in the repository, listed below: * `Kraftfile`: the Unikraft Cloud specification, including command-line arguments * `Dockerfile`: In case you need to add files to your instance's rootfs ## Learn more Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: Imaginary URL: /docs/guides/imaginary # Imaginary import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} This example uses [`imaginary`](https://github.com/h2non/imaginary), an HTTP microservice for high-level image processing. To run this example, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/imaginary/` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/imaginary/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` When done, invoke the following command to deploy this app on Unikraft Cloud: ```bash unikraft build . --output /imaginary:latest unikraft run --metro fra \ -m 512M \ -p 443:8080/tls+http \ --scale-to-zero policy=on,cooldown-time=1000 \ --image /imaginary:latest ``` The output shows the instance address and other details: ```ansi metro: fra name: imaginary-mwb4y uuid: 8cf18bf7-2bf6-4f23-be07-f9c234c7962d state: starting image: /imaginary resources:  memory: 512MiB  vcpus: 1 service:  uuid: b9b7fa60-5f4f-13e5-41e9-eeea476f4398  name: divine-wind-1ycjvhqs  domains:  - fqdn: divine-wind-1ycjvhqs.fra.unikraft.app networks: - uuid: 91467bd9-8d52-a378-4426-7014ca09e5d5  private-ip: 10.0.3.3  mac: 12:b0:e2:ed:95:49 timestamps:  created: just now ``` In this case, the instance name is `imaginary-mwb4y` and the address is `https://divine-wind-1ycjvhqs.fra.unikraft.app`. They're different for each run. Use `curl` to query the Unikraft Cloud instance of Imaginary. You will get a health status of the service: ```bash curl -s https://divine-wind-1ycjvhqs.fra.unikraft.app/health | jq ``` ```json { "uptime": 414, "allocatedMemory": 0.19, "totalAllocatedMemory": 0.72, "goroutines": 6, "completedGCCycles": 13, "cpus": 1, "maxHeapUsage": 3.63, "heapInUse": 0.19, "objectsInUse": 846, "OSMemoryObtained": 7.73 } ``` To test the Imaginary instance on Unikraft Cloud use the `/form` endpoint. That is, open the `https://divine-wind-1ycjvhqs.fra.unikraft.app/form` address in the browser and use the existing forms to process an image. To make actual use of the Imaginary instance, use [the endpoints of the HTTP API](https://github.com/h2non/imaginary/blob/master/README.md#get). The API provides endpoints, together with parameters, for different image processing options: `/crop`, `/resize`, `/flip`, `/convert`, `/watermark`, `/rotate`, `/blur` etc. You can list information about the instance by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra imaginary-mwb4y running /imaginary 512MiB 1 divine-wind-1ycjvhqs.fra.unikraft.app 2 minutes ago ``` When done, you can remove the instance: ```bash unikraft instances delete imaginary-mwb4y ``` The Imaginary Unikraft Cloud service works as is: you deploy it and then you query [the endpoints of the HTTP API](https://github.com/h2non/imaginary/blob/master/README.md#get). You can customize the command line options used to start the service, by updating the `cmd` line in the `Kraftfile`: ```yaml spec: v0.7 runtime: base-compat:latest cmd: ["/usr/bin/imaginary", "-p", "8080"] ``` You can update the `cmd` line with [command line option for Imaginary](https://github.com/h2non/imaginary/blob/master/README.md#command-line-usage). ## Learn more Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: Hugo URL: /docs/guides/hugo0.122 # Hugo import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} This guide shows you how to use [Hugo](https://gohugo.io/commands/hugo_server/), a high performance webserver, with the [ananke](https://github.com/budparr/gohugo-theme-ananke.git) theme. To run it, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/hugo0.122/` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/hugo0.122/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` When done, invoke the following command to deploy this app on Unikraft Cloud: ```bash unikraft build . --output /hugo0122:latest unikraft run --metro fra \ -m 512M \ -p 443:1313/tls+http \ --scale-to-zero policy=on,cooldown-time=1000 \ --image /hugo0122:latest ``` The output shows the instance address and other details: ```ansi metro: fra name: hugo0122-zpabu uuid: dfc6e06c-76cc-4aa1-a053-c4eded0d2456 state: starting image: /hugo0122 resources:  memory: 512MiB  vcpus: 1 service:  uuid: f5492054-269e-2bcb-1d6c-18b2129c423a  name: morning-rain-jikpfy3t  domains:  - fqdn: morning-rain-jikpfy3t.fra.unikraft.app networks: - uuid: 4dd794e8-ee05-6d97-fdc8-b86f92dc1b44  private-ip: 10.0.6.4  mac: 12:b0:fe:77:90:47 timestamps:  created: just now ``` In this case, the instance name is `hugo0122-zpabu` and the address is `https://morning-rain-jikpfy3t.fra.unikraft.app`. They're different for each run. Use `curl` to query the Unikraft Cloud instance of Hugo. ```bash curl https://morning-rain-jikpfy3t.fra.unikraft.app ``` ```html [...] ``` You can list information about the instance by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra hugo0122-zpabu running /hugo0122 512MiB 1 morning-rain-jikpfy3t.fra.unikraft.app 2 minutes ago ``` When done, you can remove the instance: ```bash unikraft instances delete hugo0122-zpabu ``` ## Customize your app To customize the Hugo app, update the files in the repository, listed below: * `Kraftfile`: the Unikraft Cloud specification * `site/`: sample site content * `Dockerfile`: In case you need to add files to your instance's rootfs Update the contents of the `site/` directory to serve different static web content. After re-deploying the Hugo image on Unikraft Cloud, using `curl` or a browser to query it will present the new page contents. Tools like [`Jekyll`](https://jekyllrb.com/) or [`Hugo`](https://gohugo.io/) can generate the static web content located in the `site/` offline. ## Learn more Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: Rust HTTP Server URL: /docs/guides/httpserver-rust1.91 # Rust HTTP Server import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} This guide explains how to create and deploy a Rust app. To run this example, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/httpserver-rust1.91` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/httpserver-rust1.91/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` When done, invoke the following command to deploy this app on Unikraft Cloud: ```bash unikraft build . --output /httpserver-rust191:latest unikraft run --metro fra \ -m 384M \ -p 443:8080/tls+http \ --scale-to-zero policy=on,cooldown-time=1000 \ --image /httpserver-rust191:latest ``` The output shows the instance address and other details: ```ansi metro: fra name: httpserver-rust191-pinzf uuid: 8acb3d35-38ba-4929-81de-950340662c14 state: starting image: /httpserver-rust191 resources:  memory: 384MiB  vcpus: 1 service:  uuid: 3bf42986-3032-1ff2-fe4d-2041db03b628  name: snowy-feather-k4pfgl8t  domains:  - fqdn: snowy-feather-k4pfgl8t.fra.unikraft.app networks: - uuid: d64344f4-e159-c7c3-7f1b-ba10bcc60f67  private-ip: 10.0.2.53  mac: 12:b0:1d:12:0e:46 timestamps:  created: just now ``` In this case, the instance name is `httpserver-rust191-pinzf` and the address is `snowy-feather-k4pfgl8t.fra.unikraft.app`. They're different for each run. Use `curl` to query the Unikraft Cloud instance: ```bash curl https://snowy-feather-k4pfgl8t.fra.unikraft.app ``` ```text Hello, World! ``` You can list information about the instance by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra httpserver-rust191-pinzf standby /httpserver-rust191 384MiB 1 snowy-feather-k4pfgl8t.fra.unikraft.app 2 minutes ago ``` When done, you can remove the instance: ```bash unikraft instances delete httpserver-rust191-pinzf ``` ## Learn more Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: Rust (Rocket) HTTP Server URL: /docs/guides/httpserver-rust1.88-rocket0.5 # Rust (Rocket) HTTP Server import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} This example uses [`Rocket`](https://rocket.rs/), a popular Rust web framework. To run it, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/httpserver-rust1.88-rocket0.5/` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/httpserver-rust1.88-rocket0.5 ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` When done, invoke the following command to deploy this app on Unikraft Cloud: ```bash unikraft build . --output /httpserver-rust188-rocket05:latest unikraft run --metro fra \ -m 256M \ -p 443:8080/tls+http \ --scale-to-zero policy=on,cooldown-time=1000 \ --image /httpserver-rust188-rocket05:latest ``` The output shows the instance address and other details: ```ansi metro: fra name: httpserver-rust188-rocket05-tuwq3 uuid: b6fe13e4-93b7-402b-bdec-1bc4d81bc275 state: starting image: /httpserver-rust188-rocket05 resources:  memory: 256MiB  vcpus: 1 service:  uuid: 57da8f4a-5c68-3d4c-3b8f-987ee2ba0fb3  name: empty-bobo-n3htmpye  domains:  - fqdn: empty-bobo-n3htmpye.fra.unikraft.app networks: - uuid: fb33101e-f3b6-0859-38b9-71fb057cab4a  private-ip: 10.0.6.6  mac: 12:b0:07:e5:d1:fe timestamps:  created: just now ``` In this case, the instance name is `httpserver-rust188-rocket05-tuwq3` and the address is `https://empty-bobo-n3htmpye.fra.unikraft.app`. They're different for each run. Use `curl` to query any of the Rocket server's paths, for example: ```bash curl https://empty-bobo-n3htmpye.fra.unikraft.app/wave/Rocketeer/100 ``` ```text 👋 Hello, 100 year old named Rocketeer! ``` You can list information about the instance by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra httpserver-rust188-rocket05-tuwq3 running /httpserver-rust188-rocket05 256MiB 1 empty-bobo-n3htmpye.fra.unikraft.app 2 minutes ago ``` When done, you can remove the instance: ```bash unikraft instances delete httpserver-rust188-rocket05-tuwq3 ``` ## Customize your app To customize the app, update the files in the repository, listed below: * `src/main.rs`: the actual server implementation * `Cargo.toml`: the Cargo package manager configuration file * `Kraftfile`: the Unikraft Cloud specification * `Dockerfile`: the Docker-specified app filesystem The following options are available for customizing the app: * If you only update the implementation in the `src/main.rs` source file, you don't need to make any other changes. * If you create any new source files, copy them into the app filesystem by using the `COPY` command in the `Dockerfile`. If you add new Rust source code files, be sure to configure required dependencies in the `Cargo.toml` file. * If you build a new executable, update the `cmd` line in the `Kraftfile` and replace `/server` with the path to the new executable. * More extensive changes may require extending the `Dockerfile` ([see `Dockerfile` syntax reference](https://docs.docker.com/engine/reference/builder/)). ## Learn more Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: Rust (Actix Web) HTTP Server URL: /docs/guides/httpserver-rust1.88-actix-web4 # Rust (Actix Web) HTTP Server import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} This example uses [`actix-web`](https://actix.rs), a popular Rust web framework. To run it, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 1. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/httpserver-rust1.88-actix-web4/` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/httpserver-rust1.88-actix-web4/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` When done, invoke the following command to deploy this app on Unikraft Cloud: ```bash unikraft build . --output /httpserver-rust188-actix-web4:latest unikraft run --metro fra \ -m 256M \ -p 443:8080/tls+http \ --scale-to-zero policy=on,cooldown-time=1000 \ --image /httpserver-rust188-actix-web4:latest ``` The output shows the instance address and other details: ```ansi metro: fra name: httpserver-rust188-actix-web4-3pj27 uuid: 3e729de6-a1fb-5818-63d4-51a905fa6a5d state: starting image: /httpserver-rust188-actix-web4 resources:  memory: 256MiB  vcpus: 1 service:  uuid: bf564711-3ec0-be8c-64ca-e27c5034d3fe  name: autumn-silence-wupu2nus  domains:  - fqdn: autumn-silence-wupu2nus.fra.unikraft.app networks: - uuid: 53417e5b-ae43-6307-d433-9a22c0d249a9  private-ip: 10.0.3.3  mac: 12:b0:58:9f:9e:51 timestamps:  created: just now ``` In this case, the instance name is `httpserver-rust188-actix-web4-3pj27` and the address is `https://autumn-silence-wupu2nus.fra.unikraft.app`. They're different for each run. Use `curl` to query the Unikraft Cloud instance of the Rust-based HTTP web server: ```bash curl https://autumn-silence-wupu2nus.fra.unikraft.app curl https://autumn-silence-wupu2nus.fra.unikraft.app/hey ``` ```text Hello, World! Hey there! ``` You can list information about the instance by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra httpserver-rust188-actix-web4-3pj27 running /httpserver-rust188-actix-web4 256MiB 1 autumn-silence-wupu2nus.fra.unikraft.app 2 minutes ago ``` When done, you can remove the instance: ```bash unikraft instances delete httpserver-rust188-actix-web4-3pj27 ``` ## Customize your app To customize the app, update the files in the repository, listed below: * `src/main.rs`: the actual server implementation * `Cargo.toml`: the Cargo package manager configuration file * `Kraftfile`: the Unikraft Cloud specification * `Dockerfile`: the Docker-specified app filesystem The following options are available for customizing the app: * If you only update the implementation in the `src/main.rs` source file, you don't need to make any other changes. * If you create any new source files, copy them into the app filesystem by using the `COPY` command in the `Dockerfile`. If you add new Rust source code files, be sure to configure required dependencies in the `Cargo.toml` file. * If you build a new executable, update the `cmd` line in the `Kraftfile` and replace `/server` with the path to the new executable. * More extensive changes may require extending the `Dockerfile` ([see `Dockerfile` syntax reference](https://docs.docker.com/engine/reference/builder/)). ## Learn more Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: Rust (Tokio) HTTP Server URL: /docs/guides/httpserver-rust1.75-tokio # Rust (Tokio) HTTP Server import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} This example uses [`Tokio`](https://tokio.rs/), a popular Rust asynchronous runtime. To run this example, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 1. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/httpserver-rust1.75-tokio/` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/httpserver-rust1.75-tokio/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` When done, invoke the following command to deploy this app on Unikraft Cloud: ```bash unikraft build . --output /httpserver-rust175-tokio:latest unikraft run --metro fra \ -m 256M \ -p 443:8080/tls+http \ --scale-to-zero policy=on,cooldown-time=1000 \ --image /httpserver-rust175-tokio:latest ``` The output shows the instance address and other details: ```ansi metro: fra name: httpserver-rust175-tokio-6gxsp uuid: d5719f64-0653-42d7-b2de-aa6dee0ce467 state: starting image: /httpserver-rust175-tokio resources:  memory: 256MiB  vcpus: 1 service:  uuid: b59ce5aa-7dac-5241-386c-be48db955f3f  name: empty-dawn-3coedrce  domains:  - fqdn: empty-dawn-3coedrce.fra.unikraft.app networks: - uuid: 9136307f-2ee6-2a0b-99af-c5a0fc1727f5  private-ip: 10.0.6.3  mac: 12:b0:38:a0:73:f3 timestamps:  created: just now ``` In this case, the instance name is `httpserver-rust175-tokio-6gxsp` and the address is `https://empty-dawn-3coedrce.fra.unikraft.app`. They're different for each run. Use `curl` to query the Unikraft Cloud instance of the Tokio-based HTTP web server: ```bash curl https://empty-dawn-3coedrce.fra.unikraft.app ``` ```text Hello, World! ``` You can list information about the instance by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra httpserver-rust175-tokio-6gxsp running /httpserver-rust175-tokio 256MiB 1 empty-dawn-3coedrce.fra.unikraft.app 2 minutes ago ``` When done, you can remove the instance: ```bash unikraft instances delete httpserver-rust175-tokio-6gxsp ``` ## Customize your app To customize the app, update the files in the repository, listed below: * `src/main.rs`: the actual server * `Cargo.toml`: the Cargo package manager configuration file * `Kraftfile`: the Unikraft Cloud specification * `Dockerfile`: the Docker-specified app filesystem The following options are available for customizing the app: * If you only update the implementation in the `src/main.rs` source file, you don't need to make any other changes. * If you create any new source files, copy them into the app filesystem by using the `COPY` command in the `Dockerfile`. If you add new Rust source code files, be sure to configure required dependencies in the `Cargo.toml` file. * If you build a new executable, update the `cmd` line in the `Kraftfile` and replace `/server` with the path to the new executable. * More extensive changes may require extending the `Dockerfile` ([see `Dockerfile` syntax reference](https://docs.docker.com/engine/reference/builder/)). ## Learn more Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: Rust (Leptos + Trunk) HTTP Server URL: /docs/guides/httpserver-rust-trunkrs-leptos # Rust (Leptos + Trunk) HTTP Server import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} This guide shows how to deploy a Rust HTTP server using Trunk and Leptos on Unikraft Cloud. To run it, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/httpserver-rust-trunkrs-leptos/` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/httpserver-rust-trunkrs-leptos/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` When done, invoke the following command to deploy this app on Unikraft Cloud: ```bash unikraft build . --output /httpserver-rust-trunkrs-leptos:latest unikraft run --metro fra \ -m 256M \ -p 443:8080/tls+http \ --scale-to-zero policy=on,cooldown-time=1000 \ --image /httpserver-rust-trunkrs-leptos:latest ``` The output shows the instance address and other details: ```ansi metro: fra name: httpserver-rust-trunkrs-leptos-n2j7k uuid: 7b8c9d0e-1f2a-3b4c-5d6e-7f8a9b0c1d2e state: starting image: /httpserver-rust-trunkrs-leptos resources:  memory: 256MiB  vcpus: 1 service:  uuid: 8c9d0e1f-2a3b-4c5d-6e7f-8a9b0c1d2e3f  name: cool-wind-by4hq7nm  domains:  - fqdn: cool-wind-by4hq7nm.fra.unikraft.app networks: - uuid: 9d0e1f2a-3b4c-5d6e-7f8a-9b0c1d2e3f4a  private-ip: 10.0.2.3  mac: 12:b0:7d:4f:bc:a6 timestamps:  created: just now ``` In this case, the instance name is `httpserver-rust-trunkrs-leptos-n2j7k` and the address is `https://cool-wind-by4hq7nm.fra.unikraft.app`. They're different for each run. The command will deploy files in the current directory. After deploying, you can query the service using the provided URL. To run locally: ```bash trunk serve ``` You can list information about the instance by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra httpserver-rust-trunkrs-leptos-n2j7k running /httpserver-rust-trunkrs-leptos 256MiB 1 cool-wind-by4hq7nm.fra.unikraft.app 2 minutes ago ``` When done, you can remove the instance: ```bash unikraft instances delete ``` ## Learn more - [leptos](https://leptos.dev) - [trunkrs](https://trunkrs.dev) - [Unikraft Cloud's Documentation](https://unikraft.cloud/docs/) Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: Ruby HTTP Server URL: /docs/guides/httpserver-ruby3.2 # Ruby HTTP Server import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} This guide explains how to create and deploy a simple Ruby-based HTTP web server. To run this example, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/httpserver-ruby3.2/` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/httpserver-ruby3.2/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` When done, invoke the following command to deploy this app on Unikraft Cloud: ```bash unikraft build . --output /httpserver-ruby32:latest unikraft run --metro fra \ -m 256M \ -p 443:8080/tls+http \ --scale-to-zero policy=on,cooldown-time=1000 \ --image /httpserver-ruby32:latest ``` The output shows the instance address and other details: ```ansi metro: fra name: httpserver-ruby32-s6l8n uuid: b1ebbbc0-5efa-476c-adb6-99866773245c state: starting image: /httpserver-ruby32 resources:  memory: 256MiB  vcpus: 1 service:  uuid: bcc07c7c-9289-11f4-6e3d-8f7fdde45256  name: silent-resonance-1jtz5c66  domains:  - fqdn: silent-resonance-1jtz5c66.fra.unikraft.app networks: - uuid: 50203783-cf51-81d1-aa2d-615f939043af  private-ip: 10.0.3.3  mac: 12:b0:0a:4f:7a:84 timestamps:  created: just now ``` In this case, the instance name is `httpserver-ruby32-s6l8n` and the address is `https://silent-resonance-1jtz5c66.fra.unikraft.app`. They're different for each run. Use `curl` to query the Unikraft Cloud instance of the Ruby-based HTTP web server: ```bash curl https://silent-resonance-1jtz5c66.fra.unikraft.app ``` ```text Hello, World! ``` You can list information about the instance by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra httpserver-ruby32-s6l8n running /httpserver-ruby32 256MiB 1 silent-resonance-1jtz5c66.fra.unikraf… 2 minutes ago ``` When done, you can remove the instance: ```bash unikraft instances delete httpserver-ruby32-s6l8n ``` ## Customize your app To customize the app, update the files in the repository, listed below: * `server.rb`: the actual Ruby HTTP server * `Kraftfile`: the Unikraft Cloud specification * `Dockerfile`: the Docker-specified app filesystem The following options are available for customizing the app: * If you only update the implementation in the `server.rb` source file, you don't need to make any other changes. * If you create any new source files, copy them into the app filesystem by using the `COPY` command in the `Dockerfile`. * More extensive changes may require extending the `Dockerfile` ([see `Dockerfile` syntax reference](https://docs.docker.com/engine/reference/builder/)). ## Learn more Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: Python HTTP Server URL: /docs/guides/httpserver-python3.12 # Python HTTP Server import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} This guide explains how to create and deploy a simple Python-based HTTP web server. To run this example, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/httpserver-python3.12/` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/httpserver-python3.12/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` When done, invoke the following command to deploy this app on Unikraft Cloud: ```bash unikraft build . --output /httpserver-python312:latest unikraft run --metro fra \ -m 512M \ -p 443:8080/tls+http \ --scale-to-zero policy=on,cooldown-time=1000 \ --image /httpserver-python312:latest ``` The output shows the instance address and other details: ```ansi metro: fra name: httpserver-python312-ma2i9 uuid: e7389eee-9808-4152-b2ec-1f3c0541fd05 state: starting image: /httpserver-python312 resources:  memory: 512MiB  vcpus: 1 service:  uuid: 51a41f63-7e88-c443-b9bf-83cd7c04d975  name: young-night-5fpf0jj8  domains:  - fqdn: young-night-5fpf0jj8.fra.unikraft.app networks: - uuid: 53da3490-c6f5-3718-25f1-219a65163c73  private-ip: 10.0.3.3  mac: 12:b0:18:0c:cb:aa timestamps:  created: just now ``` In this case, the instance name is `httpserver-python312-ma2i9` and the address is `https://young-night-5fpf0jj8.fra.unikraft.app`. They're different for each run. Use `curl` to query the Unikraft Cloud instance of the Python-based HTTP web server: ```bash curl https://young-night-5fpf0jj8.fra.unikraft.app ``` ```text Hello, World! ``` You can list information about the instance by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra httpserver-python312-ma2i9 running /httpserver-python312 512MiB 1 young-night-5fpf0jj8.fra.unikraft.app 2 minutes ago ``` When done, you can remove the instance: ```bash unikraft instances remove httpserver-python312-ma2i9 ``` ## Customize your app To customize the app, update the files in the repository, listed below: * `server.py`: the actual Python HTTP server implementation * `Kraftfile`: the Unikraft Cloud specification * `Dockerfile`: the Docker-specified app filesystem Lines in the `Kraftfile` have the following roles: * `spec: v0.7`: The current `Kraftfile` specification version is `0.7`. * `runtime: base-compat:latest`: The runtime kernel to use is the base compatibility kernel. * `rootfs`: Build the app root filesystem. `source: ./Dockerfile` means the filesystem is built using the `Dockerfile`. `format: erofs` means the filesystem type is [EROFS](https://erofs.docs.kernel.org/). * `cmd: ["/usr/bin/python3", "/src/server.py"]`: Use `/usr/bin/python3 /src/server.py` as the starting command of the instance. Lines in the `Dockerfile` have the following roles: * `FROM scratch`: Build the filesystem from the [`scratch` container image](https://hub.docker.com/_/scratch/), to [create a base image](https://docs.docker.com/build/building/base-images/). * `COPY ./server.py /src/server.py`: Copy the server implementation file (`server.py`) in the Docker filesystem (in `/src/server.py`). The following options are available for customizing the app: * If you only update the implementation in the `server.py` source file, you don't need to make any other changes. * If you create any new source files, copy them into the app filesystem by using the `COPY` command in the `Dockerfile`. * More extensive changes may require extending the `Dockerfile` ([see `Dockerfile` syntax reference](https://docs.docker.com/engine/reference/builder/)). This includes the use of Python frameworks and the use of `pip`, as shown in the next section. ## Using `pip` [`pip`](https://pip.pypa.io/en/stable/) is a package manager for Python. It's used to install dependencies for Python apps. `pip` uses the `requirements.txt` file to list required dependencies (with versions). The [`httpserver-python3.12-flask3.0`](https://github.com/unikraft-cloud/examples/tree/main/httpserver-python3.12-flask3.0) guide details the use of `pip` to deploy an app using the [`Flask`](https://flask.palletsprojects.com/en/3.0.x/) framework on Unikraft Cloud. Run the command below to deploy the app on Unikraft Cloud: ```bash unikraft build . --output /httpserver-python312-flask30:latest unikraft run --metro fra \ -m 512M \ -p 443:8080/tls+http \ --scale-to-zero policy=on,cooldown-time=1000 \ --image /httpserver-python312-flask30:latest ``` Differences from the `httpserver-python3.12-flask3.0` app are also the steps required to create an `pip`-based app: 1. Add the `requirements.txt` file used by `pip`. 2. Add framework-specific source files. In this case, this means the `server.py` file. 3. Update the `Dockerfile` to: 3.1. `COPY` the local files. 3.2. `RUN` the `pip3 install` command to install dependencies. 3.3. `COPY` of the resulting and required files (`/usr/local/lib/python3.12` and `server.py`) in the app filesystem, using the [`scratch` container](https://hub.docker.com/_/scratch/). The following lists the files: The `requirements.txt` file lists the `flask` dependency. The `Kraftfile` is the same one used for `httpserver-python3.12-flask3.0`. For `Dockerfile` newly added lines have the following roles: * `FROM python:3.12-bookworm AS base`: Use the base image of the `python:3.12-bookworm` container. This provides the `pip3` binary and other Python-related components. Name the current image `base`. * `WORKDIR /app`: Use `/app` as working directory. All other commands in the `Dockerfile` run inside this directory. * `COPY requirements.txt /app`: Copy the package configuration file to the Docker filesystem. * `RUN pip3 install ...`: Install `pip` components listed in `requirements.txt`. * `COPY --from=base ...`: Copy generated Python files in the new `base` image in the `scratch`-based image. Similar actions apply to other `pip3`-based apps. See also the [`httpserver-python3.12-django5.0`](https://github.com/unikraft-cloud/examples/tree/main/httpserver-python3.12-django5.0) example. ## Learn more Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: Flask HTTP Server URL: /docs/guides/httpserver-python3.12-flask3.0 # Flask HTTP Server import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} This guide explains how to create and deploy a [Flask](https://flask.palletsprojects.com/en/3.0.x/) web server. To run this example, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/httpserver-python3.12-flask3.0/` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/httpserver-python3.12-flask3.0/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` When done, invoke the following command to deploy this app on Unikraft Cloud: ```bash unikraft build . --output /httpserver-python312-flask30:latest unikraft run --metro fra \ -m 512M \ -p 443:8080/tls+http \ --scale-to-zero policy=on,cooldown-time=1000 \ --image /httpserver-python312-flask30:latest ``` The output shows the instance address and other details: ```ansi metro: fra name: httpserver-python312-flask30-bxwxm uuid: 3ff1ebad-2639-4214-bab4-ed35c4c32fa4 state: starting image: /httpserver-python312-flask30 resources:  memory: 512MiB  vcpus: 1 service:  uuid: 2021ca1e-5e47-a35c-bde2-7190f3815c07  name: damp-sunset-azd6dtyt  domains:  - fqdn: damp-sunset-azd6dtyt.fra.unikraft.app networks: - uuid: 4dc3b272-4fa2-ddb4-7ce0-5e66e8112a61  private-ip: 10.0.6.5  mac: 12:b0:66:99:b3:3f timestamps:  created: just now ``` In this case, the instance name is `httpserver-python312-flask30-bxwxm` and the address is `https://damp-sunset-azd6dtyt.fra.unikraft.app`. They're different for each run. Use `curl` to query the Unikraft Cloud instance of the Python-based HTTP web server: ```bash curl https://damp-sunset-azd6dtyt.fra.unikraft.app ``` ```text Hello, World! ``` You can list information about the instance by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra httpserver-python312-flask30-bxwxm running /httpserver-python312-flask30 512MiB 1 damp-sunset-azd6dtyt.fra.unikraft.app 2 minutes ago ``` When done, you can remove the instance: ```bash unikraft instances delete httpserver-python312-flask30-bxwxm ``` ## Customize your app To customize the app, update the files in the repository, listed below: * `server.py`: the actual Python HTTP server * `Kraftfile`: the Unikraft Cloud specification * `Dockerfile`: the Docker-specified app filesystem Lines in the `Kraftfile` have the following roles: * `spec: v0.7`: The current `Kraftfile` specification version is `0.7`. * `runtime: base-compat:latest`: The runtime kernel to use is the base compatibility kernel. * `rootfs`: Build the app root filesystem. `source: ./Dockerfile` means the filesystem is built using the `Dockerfile`. `format: erofs` means the filesystem type is [EROFS](https://erofs.docs.kernel.org/). * `cmd: ["/usr/bin/python3", "/src/server.py"]`: Use `/usr/bin/python3 /src/server.py` as the starting command of the instance. Lines in the `Dockerfile` have the following roles: * `FROM scratch`: Build the filesystem from the [`scratch` container image](https://hub.docker.com/_/scratch/), to [create a base image](https://docs.docker.com/build/building/base-images/). * `COPY ./server.py /app/server.py`: Copy the server implementation file (`server.py`) in the Docker filesystem (in `/app/server.py`). The following options are available for customizing the app: * If you only update the implementation in the `server.py` source file, you don't need to make any other changes. * If you create any new source files, copy them into the app filesystem by using the `COPY` command in the `Dockerfile`. * More extensive changes may require extending the `Dockerfile` ([see `Dockerfile` syntax reference](https://docs.docker.com/engine/reference/builder/)). This includes the use of Python frameworks and the use of `pip`, as shown in the next section. ## Using `pip` [`pip`](https://pip.pypa.io/en/stable/) is a package manager for Python. It's used to install dependencies for Python apps. `pip` uses the `requirements.txt` file to list required dependencies (with versions). To create an `pip`-based app: 1. Add the `requirements.txt` file used by `pip`. 2. Add framework-specific source files. In this case, this means the `server.py` file. 3. Update the `Dockerfile` to: 3.1. `COPY` the local files. 3.2. `RUN` the `pip3 install` command to install dependencies. 3.3. `COPY` of the resulting and required files (`/usr/local/lib/python3.12` and `server.py`) in the app filesystem, using the [`scratch` container](https://hub.docker.com/_/scratch/). The following lists the files: The `requirements.txt` file lists the `flask` dependency. The `Kraftfile` is the same one used for `httpserver-python3.12`. For `Dockerfile` newly added lines have the following roles: * `FROM python:3.12-bookworm AS base`: Use the base image of the `python:3.12-bookworm` container. This provides the `pip3` binary and other Python-related components. Name the current image `base`. * `WORKDIR /app`: Use `/app` as working directory. All other commands in the `Dockerfile` run inside this directory. * `COPY requirements.txt /app`: Copy the package configuration file to the Docker filesystem. * `RUN pip3 install ...`: Install `pip` components listed in `requirements.txt`. * `COPY --from=base ...`: Copy generated Python files in the new `base` image in the `scratch`-based image. Similar actions apply to other `pip3`-based apps. ## Learn more Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: Flask and SQLite HTTP Server URL: /docs/guides/httpserver-python3.12-flask3.0-sqlite # Flask and SQLite HTTP Server import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} This guide explains how to create and deploy a Python Flask app using SQLite. To run this example, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/httpserver-python3.12-flask3.0-sqlite/` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/httpserver-python3.12-flask3.0-sqlite/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` When done, invoke the following command to deploy this app on Unikraft Cloud: ```bash unikraft build . --output /httpserver-python312-flask30-sqlite:latest unikraft run --metro fra \ -m 768M \ -p 443:8080/tls+http \ --scale-to-zero policy=on,cooldown-time=1000 \ --image /httpserver-python312-flask30-sqlite:latest ``` The output shows the instance address and other details: ```ansi metro: fra name: httpserver-python312-flask30-sqlite-qodkd uuid: e00e7aca-962d-409c-87c2-c245ca08b54b state: starting image: /httpserver-python312-flask30-sqlite resources:  memory: 768MiB  vcpus: 1 service:  uuid: 1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d  name: lingering-orangutan-840mmdvd  domains:  - fqdn: lingering-orangutan-840mmdvd.fra.unikraft.app networks: - uuid: 2b3c4d5e-6f7a-8b9c-0d1e-2f3a4b5c6d7e  private-ip: 10.0.3.3  mac: 12:b0:8a:4f:2c:91 timestamps:  created: just now ``` In this case, the instance name is `httpserver-python312-flask30-sqlite-qodkd` and the address is `https://lingering-orangutan-840mmdvd.fra.unikraft.app`. They're different for each run. Use `curl` to query the Unikraft Cloud instance of the Python-based HTTP web server: ```bash curl https://young-night-5fpf0jj8.fra.unikraft.app ``` ```text [...]

Welcome to FlaskBlog

First Post

2024-02-15 22:01:07 Edit

Second Post

``` You can list information about the instance by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra httpserver-python312-flask30-sqlite-qodkd running /httpserver-python312-flask30-sqlite 768MiB 1 lingering-orangutan-840mmdvd.fra.... 2 minutes ago ``` When done, you can remove the instance: ```bash unikraft instances delete httpserver-python312-flask30-sqlite-qodkd ``` ## Implementation details The app uses the following files: * `schema.sql`: SQL schema for the database * `init_db.py`: script to initialized the database file from `schema.sql` in `/app/database.db` * `server.py` + `templates/`: the actual Flask-based implementation: Python source code file and HTML template files * `requirements.txt`: `pip` configuration file to install required packages: Flask and SQLite * `Kraftfile`: the Unikraft Cloud specification * `Dockerfile`: the Docker-specified app filesystem Lines in the `Kraftfile` have the following roles: * `spec: v0.7`: The current `Kraftfile` specification version is `0.7`. * `runtime: base-compat:latest`: The runtime kernel to use is the base compatibility kernel. * `rootfs`: Build the app root filesystem. `source: ./Dockerfile` means the filesystem is built using the `Dockerfile`. `format: erofs` means the filesystem type is [EROFS](https://erofs.docs.kernel.org/). * `cmd: ["/usr/bin/python3", "/app/server.py"]`: Use `/usr/bin/python3 /app/server.py` as the starting command of the instance. Lines in the `Dockerfile` have the following roles: * `FROM scratch`: Build the filesystem from the [`scratch` container image](https://hub.docker.com/_/scratch/), to [create a base image](https://docs.docker.com/build/building/base-images/). * `COPY` rules copy required files. The process copies the `requirements.txt` file before running `pip3 install`. The process copies other files (including `schema.sql`, `init_db.py`) for the initialization of the database. * `RUN` commands trigger actions such as installing Python packages and initializing the database. * The new `scratch` image contains the relevant contents required by the app: * the SQLite dynamic library: `/usr/lib/x86_64-linux-gnu/libsqlite3.so.0` * the Python package files: `/usr/local/lib/python3.12` * the `/app` directory ## Customize your app To customize the app, update app files in the repository: * `schema.sql`: Update the database schema. * `server.py`, `templates/`: Update the Flask app.. * `requirements.txt`, `Dockerfile`: Update the list of Python packages used by the app. * `Kraftfile`: Update the command line used to start the app. The following options are available for customizing the app: * If you only update the implementation in the `server.py` source file or `template/` directory, and the database schema in `schema.sql`, you don't need to make any other changes. * If you create any new source files, copy them into the app filesystem by using the `COPY` command in the `Dockerfile`. * If you add new dependencies in `requirements.txt`, the `RUN pip3 install` in the `Dockerfile` command should take care of everything. It may be the case that you need to copy other files, such as the `/usr/lib/x86_64-linux-gnu/libsqlite3.so.0` for SQLite, via `COPY` commands in the `Dockerfile`. More extensive changes may require extending the `Dockerfile` ([see `Dockerfile` syntax reference](https://docs.docker.com/engine/reference/builder/)). * If you add a new Python source file that's running the `main()` function, update the `cmd` line in the `Kraftfile` and replace `server.py` to run that file when creating the instance. ## Learn more Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: FastAPI HTTP Server URL: /docs/guides/httpserver-python3.12-fastapi-0.121.3 # FastAPI HTTP Server import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} This guide explains how to create and deploy a Python FastAPI web app. To run this example, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 1. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/httpserver-python3.12-fastapi-0.121.3/` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/httpserver-python3.12-fastapi-0.121.3/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` When done, invoke the following command to deploy this app on Unikraft Cloud: ```bash unikraft build . --output /httpserver-python312-fastapi-01213:latest unikraft run --metro fra \ -m 512M \ -p 443:8080/tls+http \ --scale-to-zero policy=on,cooldown-time=1000 \ --image /httpserver-python312-fastapi-01213:latest ``` The output shows the instance address and other details: ```ansi metro: fra name: httpserver-python312-fastapi-01213-0n84f uuid: 5d7fc331-3c23-4953-b025-d152a872ea29 state: starting image: /httpserver-python312-fastapi-01213 resources:  memory: 512MiB  vcpus: 1 service:  uuid: ced3df54-64cc-9217-3441-e0b4995319f0  name: dry-water-0oexx89g  domains:  - fqdn: dry-water-0oexx89g.fra.unikraft.app networks: - uuid: 66d1766f-0a03-8b62-eb1a-0adb09b07b1d  private-ip: 10.0.1.69  mac: 12:b0:a1:15:7c:4a timestamps:  created: just now ``` In this case, the instance name is `httpserver-python312-fastapi-01213-0n84f` and the address is `https://dry-water-0oexx89g.fra.unikraft.app`. They're different for each run. Use `curl` to query the Unikraft Cloud instance of the Python FastAPI web app server: ```bash curl https://dry-water-0oexx89g.fra.unikraft.app ``` ```text {"Hello":"World"} ``` You can list information about the instance by running: ```bash unikraft instances list ``` ```ansi METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED fra httpserver-python312-fastapi-01213-0n84f standby /httpserver-python312-fastapi-01213 512MiB 1 dry-water-0oexx89g.fra.unikraft.app 2 minutes ago ``` When done, you can remove the instance: ```bash unikraft instances remove httpserver-python312-fastapi-01213-0n84f ``` ## Customize your app To customize the app, update the files in the repository, listed below: * `server.py`: the entry point for the app * `Kraftfile`: the Unikraft Cloud specification * `Dockerfile`: the Docker-specified app filesystem Lines in the `Kraftfile` have the following roles: * `spec: v0.7`: The current `Kraftfile` specification version is `0.7`. * `runtime: base-compat:latest`: The Unikraft runtime kernel to use is the latest that provides a minimal Linux-compatible environment. * `rootfs`: Build the app root filesystem. `source: ./Dockerfile` means the filesystem is built using the `Dockerfile`. `format: erofs` means the filesystem type is [EROFS](https://erofs.docs.kernel.org/). * `cmd: ["/usr/bin/python3", "-m", "uvicorn", "src.server:app", "--host", "0.0.0.0", "--port", "8080"]`: Use this as the starting command of the instance. Lines in the `Dockerfile` have the following roles: * `FROM scratch`: Build the filesystem from the [`scratch` container image](https://hub.docker.com/_/scratch/), to [create a base image](https://docs.docker.com/build/building/base-images/). The following options are available for customizing the app: * If you only update the implementation in the `server.py` source file, you don't need to make any other changes. * If you create any new source files, copy them into the app filesystem by using the `COPY` command in the `Dockerfile`. * More extensive changes may require extending the `Dockerfile` ([see `Dockerfile` syntax reference](https://docs.docker.com/engine/reference/builder/)). ## Learn more Use the `--help` option for detailed information on using Unikraft Cloud: ```bash unikraft --help ``` Or visit the [CLI Reference](/cli/unikraft). {/* vale on */} --- ## Document: Django HTTP Server URL: /docs/guides/httpserver-python3.12-django5.0 # Django HTTP Server import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" {/* vale off */} {/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} This guide explains how to create and deploy a Python Django web app. To run this example, follow these steps: 1. Install the [unikraft CLI](/cli). You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). 2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/httpserver-python3.12-django5.0/` directory: ```bash git clone https://github.com/unikraft-cloud/examples cd examples/httpserver-python3.12-django5.0/ ``` Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): ```bash unikraft login ``` When done, invoke the following command to deploy this app on Unikraft Cloud: ```bash unikraft build . --output /httpserver-python312-django50:latest unikraft run --metro fra \ -m 1G \ -p 443:80/tls+http \ --scale-to-zero policy=on,cooldown-time=1000,stateful=true \ --image /httpserver-python312-django50:latest ``` The output shows the instance address and other details: ```ansi metro: fra name: httpserver-python312-django50-vt56c uuid: d8469447-fdf6-4caf-9fea-494218ca6f72 state: starting image: /httpserver-python312-django50 resources:  memory: 1024MiB  vcpus: 1 service:  uuid: 109aa11f-da45-8d57-d5e4-6eb509ee3e73  name: dawn-sound-n5wrkxi2  domains:  - fqdn: dawn-sound-n5wrkxi2.fra.unikraft.app networks: - uuid: 95f37ba4-2586-54a2-bf3e-5764c91c4fc1  private-ip: 10.0.6.5  mac: 12:b0:9c:af:65:e7 timestamps:  created: just now ``` In this case, the instance name is `httpserver-python312-django50-vt56c` and the address is `https://dawn-sound-n5wrkxi2.fra.unikraft.app`. They're different for each run. Use `curl` to query the Unikraft Cloud instance of the Django web app server: ```bash curl https://dawn-sound-n5wrkxi2.fra.unikraft.app ``` ```html The install worked successfully! Congratulations!