# 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
[1mmetro[22m: fra
[1mname[22m: node-playwright-chromium-4rrxr
[1muuid[22m: aeff2d28-d718-4068-9e24-4e2bb203022c
[1mstate[22m: [38;2;144;161;185mstarting[0m
[1mimage[22m: /node-playwright-chromium
[1mresources[22m:
[3m memory[23m: 1000MiB
[3m vcpus[23m: 1
[1mservice[22m:
[3m name[23m: dry-fire-vhmh9z8u
[3m uuid[23m: 28762473-9e16-4921-bee5-e57c9e5dff34
[3m domains[23m:
[3m - fqdn[23m: dry-fire-vhmh9z8u.fra.unikraft.app
[1mnetworks[22m:
[3m- uuid[23m: dd2581ff-f7e8-4aac-b7e6-73b1d64c5cb7
[3m private-ip[23m: 10.0.3.133
[3m mac[23m: 12:b0:0a:00:00:19
[1mtimestamps[22m:
[3m created[23m: 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
[1mmetro[22m: fra
[1mname[22m: node-playwright-chromium-b0nyn
[1muuid[22m: e4d00b20-4fc9-4cba-a29d-846fa733c358
[1mstate[22m: [38;2;144;161;185mstarting[0m
[1mimage[22m: /node-playwright-chromium
[1mresources[22m:
[3m memory[23m: 2700MiB
[3m vcpus[23m: 1
[1mservice[22m:
[3m name[23m: fragrant-dawn-v42h579x
[3m uuid[23m: e9424066-5c71-4142-94ee-44acc5ed3d09
[3m domains[23m:
[3m - fqdn[23m: fragrant-dawn-v42h579x.fra.unikraft.app
[1mnetworks[22m:
[3m- uuid[23m: 762a85e2-e6ad-435a-867e-4139a5fda69f
[3m private-ip[23m: 10.0.3.157
[3m mac[23m: 12:b0:0a:00:00:9d
[1mtimestamps[22m:
[3m created[23m: 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=""
[1mNAME[0m [1mACTIVE[0m [1mMETROS[0m
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"
[1mMETRO[0m [1mNAME[0m [1mSTATE[0m [1mIMAGE[0m [1mARGS[0m [1mMEMORY[0m [1mVCPUS[0m [1mFQDN[0m [1mCREATED[0m
fra httpserver-go121-qdrlz [38;2;236;0;63mstopped[0m /httpserver-go121 256MiB 1 summer-star-huyfslqm.fra.unikraft.app 20 hours ago
fra httpserver-go121-bhjvm [38;2;236;0;63mstopped[0m /httpserver-go121 256MiB 1 restless-gorilla-dalzi9bs.fra.unikraft.app 20 hours ago
was httpserver-go121-xjsli [38;2;43;127;255mstandby[0m /httpserver-go121 256MiB 1 bitter-bonobo-20plsjfu.was.unikraft.app 2 minutes ago
```
```ansi title="kraft"
[1mNAME[0m [1mFQDN[0m [1mSTATE[0m [1mSTATUS[0m [1mIMAGE[0m [1mMEMORY[0m [1mVCPUS[0m [1mARGS[0m [1mBOOT TIME[0m
httpserver-go121-qdrlz summer-star-huyfslqm.fra.unikraft.app [0;31mstopped[0m /httpserver-go121@sha256:7785ebdf6c1ffaf05c3be... 256 MiB 1 90.20 ms
httpserver-go121-bhjvm restless-gorilla-dalzi9bs.fra.unikraft.app [0;31mstopped[0m /httpserver-go121@sha256:7785ebdf6c1ffaf05c3be... 256 MiB 1 89.33 ms
#-------------------------
httpserver-go121-xjsli bitter-bonobo-20plsjfu.was.unikraft.app [0;36mstandby[0m 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"]`:
DockerfileKraftfile
```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
[1mmetro[22m: fra
[1mname[22m: my-volume
[1muuid[22m: 4d5576f7-315a-4eab-98b8-6c18fec076e0
[1mstate[22m: available
[1msize[22m: 100MiB
[1mfilesystem[22m: ext4
[1mquota-policy[22m: static
[1mpersistent[22m: true
[1maccess-mode[22m: rwo
[1mtimestamps[22m:
[3m created[23m: just now
```
You can list the volume with:
```bash
unikraft volumes list
```
```ansi
[1mMETRO[22m [1mNAME[22m [1mSTATE[22m [1mSIZE[22m [1mCREATED[22m
fra my-volume [38;2;0;188;125mavailable[0m 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
[1mmetro[22m: fra
[1mname[22m: httpserver-python312-flask30-bxwxm
[1muuid[22m: 3ff1ebad-2639-4214-bab4-ed35c4c32fa4
[1mstate[22m: [38;2;144;161;185mstarting[0m
[1mimage[22m: /httpserver-python312-flask30
[1mresources[22m:
[3m memory[23m: 512MiB
[3m vcpus[23m: 1
[1mservice[22m:
[3m uuid[23m: 2021ca1e-5e47-a35c-bde2-7190f3815c07
[3m name[23m: damp-sunset-azd6dtyt
[3m domains[23m:
[3m - fqdn[23m: damp-sunset-azd6dtyt.fra.unikraft.app
[1mvolumes[22m:
[3m- name[23m: my-volume
[3m uuid[23m: 4d5576f7-315a-4eab-98b8-6c18fec076e0
[3m at[23m: /mnt
[1mnetworks[22m:
[3m- uuid[23m: 4dc3b272-4fa2-ddb4-7ce0-5e66e8112a61
[3m private-ip[23m: 10.0.6.5
[3m mac[23m: 12:b0:66:99:b3:3f
[1mtimestamps[22m:
[3m created[23m: just now
```
To confirm that the platform attached the volume, run:
```bash
unikraft volumes get my-volume
```
You should see output like:
```ansi
[1mMETRO[22m [1mNAME[22m [1mSTATE[22m [1mSIZE[22m [1mCREATED[22m
fra my-volume [38;2;0;188;125mmounted[0m 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
[1mmetro[22m: fra
[1mname[22m: httpserver-python312-flask30-0s9c7
[1muuid[22m: 59d06415-84b8-4bf5-85c7-997960382011
[1mstate[22m: [38;2;144;161;185mstarting[0m
[1mimage[22m: /httpserver-python312-flask30
[1mresources[22m:
[3m memory[23m: 512MiB
[3m vcpus[23m: 1
[1mservice[22m:
[3m uuid[23m: ca8ae7e9-3767-85f6-3d70-b77bcf894a7c
[3m name[23m: winter-field-v6m37jgs
[3m domains[23m:
[3m - fqdn[23m: winter-field-v6m37jgs.fra.unikraft.app
[1mvolumes[22m:
[3m- name[23m: my-volume
[3m uuid[23m: 4d5576f7-315a-4eab-98b8-6c18fec076e0
[3m at[23m: /mnt
[1mnetworks[22m:
[3m- uuid[23m: 935ba4ef-39c2-07e7-d2ae-8cd3c9aae07c
[3m private-ip[23m: 10.0.6.5
[3m mac[23m: 12:b0:66:99:b3:3f
[1mtimestamps[22m:
[3m created[23m: 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
[1mmetro[22m: fra
[1mname[22m: my-volume
[1muuid[22m: 4d5576f7-315a-4eab-98b8-6c18fec076e0
[1mstate[22m: [38;2;43;127;255mtemplate[0m
[1msize[22m: 100MiB
[1mfilesystem[22m: ext4
[1mpersistent[22m: true
[1mtimestamps[22m:
[3m created[23m: just now
```
You can list all volume templates with:
```bash
unikraft volume template list
```
```ansi
[1mMETRO[22m [1mNAME[22m [1mSTATE[22m [1mSIZE[22m [1mCREATED[22m
fra my-volume [38;2;43;127;255mtemplate[0m 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
[1mmetro[22m: fra
[1mname[22m: my-volume-clone
[1muuid[22m: 3f0625b4-cad3-4875-8b61-113aad4b02c7
[1mstate[22m: available
[1msize[22m: 100MiB
[1mfilesystem[22m: ext4
[1mquota-policy[22m: static
[1mpersistent[22m: true
[1maccess-mode[22m: rwo
[1mtimestamps[22m:
[3m created[23m: 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
[1mmetro[22m: fra
[1mname[22m: node-playwright-chromium-nwa95
[1muuid[22m: 59cd6c94-611c-4ca8-911f-78c7db95ad12
[1mstate[22m: [38;2;236;0;63mstopped[0m
[1mimage[22m: /node-playwright-chromium
[1mresources[22m:
[3m memory[23m: 2.441GiB
[3m vcpus[23m: 1
[1mservice[22m:
[3m name[23m: divine-dust-73sxgnb6
[3m uuid[23m: 3c7ce2b4-ae86-4a21-8562-55033d5409d5
[3m domains[23m:
[3m - fqdn[23m: divine-dust-73sxgnb6.fra0-fe-test.unikraft.app
[1mnetworks[22m:
[3m- uuid[23m: 62982265-6521-401c-878a-d7e31804c63c
[3m private-ip[23m: 10.0.0.109
[3m mac[23m: 12:b0:0a:00:00:6d
[1mtimestamps[22m:
[3m created[23m: 2 days ago
[1mscale-to-zero[22m:
[3m enabled[23m: true
[3m policy[23m: on
[3m cooldown-time[23m: 1s
[1mstop[22m:
[3m reason[23m: 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:
InstanceInstance TemplateVolumeVolume 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):
InstanceInstance TemplateVolumeVolume 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"`):
InstanceInstance TemplateVolumeVolume 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"`):
InstanceInstance TemplateVolumeVolume 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.
InstanceInstance TemplateVolumeVolume 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
[1mmetro[22m: fra
[1mname[22m: nginx-67zbu
[1muuid[22m: 8a8bc1b9-0af6-420e-a426-190dc2da9eaa
[1mstate[22m: [38;2;144;161;185mstarting[0m
[1mimage[22m: /nginx
[1mresources[22m:
[3m memory[23m: 256MiB
[3m vcpus[23m: 1
[1mservice[22m:
[3m uuid[23m: a942b9b5-ad17-3ffe-dcd2-ef4331f9087a
[3m name[23m: nameless-fog-0tvh1uov # <---- service name
[3m domains[23m:
[3m - fqdn[23m: nameless-fog-0tvh1uov.fra.unikraft.app # <---- service domain
[1mnetworks[22m:
[3m- uuid[23m: 62d9bbf0-aec8-61f6-7bdb-86edf63dd068
[3m private-ip[23m: 10.0.3.3
[3m mac[23m: 12:b0:c6:23:ed:15
[1mtimestamps[22m:
[3m created[23m: 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
[1mmetro[22m: fra
[1mname[22m: my-service
[1muuid[22m: 9bfe76ea-2679-482a-954e-577de7d122b3
[1mpersistent[22m: true
[1mlimits[22m:
[3m soft[23m: 1
[3m hard[23m: 65535
[1mtimestamps[22m:
[3m created[23m: just now
[1mdomains[22m:
[3m- fqdn[23m: my-service-pc4xjplc.fra.unikraft.app
[1mservices[22m:
[3m- source[23m: 443
[3m destination[23m: 8080
[3m handlers[23m: ["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
[1mmetro[22m: fra
[1mname[22m: httpserver-go121-9a2wv
[1muuid[22m: 8bb34040-9434-4a28-bd1e-c24ee532e2da
[1mstate[22m: [38;2;144;161;185mstarting[0m
[1mimage[22m: /httpserver-go121
[1mresources[22m:
[3m memory[23m: 256MiB
[3m vcpus[23m: 1
[1mservice[22m:
[3m uuid[23m: 9bfe76ea-2679-482a-954e-577de7d122b3
[3m name[23m: my-service
[3m domains[23m:
[3m - fqdn[23m: my-service-pc4xjplc.fra.unikraft.app
[1mnetworks[22m:
[3m- uuid[23m: 51f79dc1-e989-908d-894b-bdf0a87e7901
[3m private-ip[23m: 10.0.3.3
[3m mac[23m: 12:b0:57:91:bb:a5
[1mtimestamps[22m:
[3m created[23m: 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:
tlshttpredirect
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"
[48;5;99m [1m[38;5;231mfra[0m[48;5;99m [49m
[1muser uuid[0m[49m:
[1muser name[0m[49m:
[0;1;39mimage storage[0m: [0;33m████████████████████████[0m[38;5;243m████████████[0m 702 MiB/1.0 GiB
[1mactive instances[0m[49m: [38;5;105m██[39m[38;5;243m██████████████████████████████████[0m 5/64
[1mtotal instances[0m[49m: [38;5;105m██[39m[38;5;243m██████████████████████████████████[0m 5/64
[1mactive vcpus[0m[49m: [38;5;105m██[39m[38;5;243m██████████████████████████████████[0m 5/64
[1mvcpu limit[0m[49m: 1-1
[1mactive used memory[0m[49m: [38;5;105m██[39m[38;5;243m██████████████████████████████████[0m 2.5GiB/32GiB
[1mmemory size limits[0m[49m: 16MiB-8GiB
[1mexposed services[0m[49m: [38;5;105m█[39m[38;5;243m███████████████████████████████████[0m 10/253
[1mservices[0m[49m: [38;5;243m████████████████████████████████████[0m 5/253
[1mvolumes[0m[49m: [38;5;105menabled[39m
[1mactive volumes[0m[49m: [38;5;243m████████████████████████████████████[0m 0/253
[1mused volume space[0m[49m: [38;5;243m████████████████████████████████████[0m 0B/20GiB
[1mvolume size limits[0m[49m: 8MiB-20GiB
[1mautoscale[0m[49m: [38;5;105menabled[39m
[1mautoscale limit[0m[49m: 0-16
[1mscale-to-zero[0m[49m: [38;5;105menabled[39m
[2m Tab/←→: switch metro q: quit[0m[49m
```
```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
...
[1mnetworks[22m:
[3m- uuid[23m: afbd8ce2-daf5-4c47-888a-96bbf1c4a347
[3m private-ip[23m: 10.0.0.49
[3m mac[23m: 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 outputPrometheus 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
[1mmetro[22m: fra
[1mname[22m: httpserver-go121-taud8
[1muuid[22m: bd281cc4-7640-4cbc-af48-5a44c2bd7b74
[1mstate[22m: [38;2;43;127;255mstandby[0m
[1mimage[22m: /httpserver-go121
[1mresources[22m:
[3m memory[23m: 256MiB
[3m vcpus[23m: 1
[1mservice[22m:
[3m name[23m: floral-sun-54ixkmi6
[3m uuid[23m: d20c8a02-53a8-4917-8c25-94b99ab59a88
[3m domains[23m:
[3m - fqdn[23m: floral-sun-54ixkmi6.fra.unikraft.app
[1mnetworks[22m:
[3m- uuid[23m: 16ca6566-a368-45d3-b759-3abf0cc37d03
[3m private-ip[23m: 10.0.0.37
[3m mac[23m: 12:b0:0a:00:00:25
[1mtimestamps[22m:
[3m created[23m: 6 days ago
[1mscale-to-zero[22m:
[3m enabled[23m: true
[3m policy[23m: on
[3m cooldown-time[23m: 1s
[1mstop[22m:
[3m reason[23m: platform stop
[3m exit-code[23m: 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
[1mmetro[22m: fra
[1mname[22m: httpserver-go121-taud8
[1muuid[22m: bd281cc4-7640-4cbc-af48-5a44c2bd7b74
[1mstate[22m: [38;2;43;127;255mtemplate[0m
[1mimage[22m: /httpserver-go121
[1mresources[22m:
[3m memory[23m: 256MiB
[3m vcpus[23m: 1
[1mtimestamps[22m:
[3m created[23m: 6 days ago
[1mscale-to-zero[22m:
[3m enabled[23m: true
[3m policy[23m: on
[3m cooldown-time[23m: 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
[1mMETRO[22m [1mNAME[22m [1mSTATE[22m [1mIMAGE[22m [1mARGS[22m [1mMEMORY[22m [1mVCPUS[22m [1mCREATED[22m
fra httpserver-go121-taud8 [38;2;43;127;255mtemplate[0m /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
[1mmetro[22m: fra
[1mname[22m: httpserver-go121-clone
[1muuid[22m: 91796fdb-bd41-4c7b-856f-9e60356c96bd
[1mstate[22m: [38;2;43;127;255mstandby[0m
[1mimage[22m: acioc/httpserver-go121
[1mresources[22m:
[3m memory[23m: 256MiB
[3m vcpus[23m: 1
[1mnetworks[22m:
[3m- uuid[23m: 8c778b8b-6ede-4e87-a825-159a2269fd19
[3m private-ip[23m: 10.0.0.85
[3m mac[23m: 12:b0:0a:00:00:55
[1mtimestamps[22m:
[3m created[23m: just now
[1mscale-to-zero[22m:
[3m enabled[23m: true
[3m policy[23m: on
[3m cooldown-time[23m: 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
[1mmetro[22m: fra
[1mname[22m: httpserver-python312-ma2i9
[1muuid[22m: e7389eee-9808-4152-b2ec-1f3c0541fd05
[1mstate[22m: [38;2;0;188;125mrunning[0m
[1mimage[22m: /httpserver-python312
[1mresources[22m:
[3m memory[23m: 512MiB
[3m vcpus[23m: 1
[1mservice[22m:
[3m uuid[23m: 51a41f63-7e88-c443-b9bf-83cd7c04d975
[3m name[23m: young-night-5fpf0jj8
[3m domains[23m:
[3m - fqdn[23m: young-night-5fpf0jj8.fra.unikraft.app
[1mnetworks[22m:
[3m- uuid[23m: 53da3490-c6f5-3718-25f1-219a65163c73
[3m private-ip[23m: 10.0.3.3
[3m mac[23m: 12:b0:18:0c:cb:aa
[1mtimestamps[22m:
[3m created[23m: 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
[1mREF[22m [1mDIGEST[22m
/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
[1mmetro[22m: fra
[1mname[22m: httpserver-python312-lscmn
[1muuid[22m: 1121225d-a678-45a2-a8ce-335a9a6a2683
[1mstate[22m: [38;2;0;188;125mrunning[0m
[1mimage[22m: /httpserver-python312
[1mresources[22m:
[3m memory[23m: 512MiB
[3m vcpus[23m: 1
[1mservice[22m:
[3m name[23m: restless-orangutan-1lklj6z5
[3m uuid[23m: 16f4b86d-df8f-452c-b189-41ec34250e58
[3m domains[23m:
[3m - fqdn[23m: restless-orangutan-1lklj6z5.fra.unikraft.app
[1mnetworks[22m:
[3m- uuid[23m: da9445a3-c7b7-4327-8499-3bd7001cd829
[3m private-ip[23m: 10.0.6.37
[3m mac[23m: 12:b0:0a:00:06:25
[1mtimestamps[22m:
[3m created[23m: just now
[1mmetro[22m: fra
[1mname[22m: httpserver-python312-xfgvc
[1muuid[22m: d021379d-9e03-4d0b-92a6-6fdf038e076a
[1mstate[22m: [38;2;0;188;125mrunning[0m
[1mimage[22m: /httpserver-python312
[1mresources[22m:
[3m memory[23m: 512MiB
[3m vcpus[23m: 1
[1mservice[22m:
[3m name[23m: icy-violet-hce1qwm5
[3m uuid[23m: 4cd63083-d538-44df-92b9-74bd6eb54ec0
[3m domains[23m:
[3m - fqdn[23m: icy-violet-hce1qwm5.fra.unikraft.app
[1mnetworks[22m:
[3m- uuid[23m: 51c147cc-8f8d-4347-b628-d2e0f4f9b3be
[3m private-ip[23m: 10.0.0.121
[3m mac[23m: 12:b0:0a:00:00:79
[1mtimestamps[22m:
[3m created[23m: just now
[1mmetro[22m: fra
[1mname[22m: httpserver-python312-7lha2
[1muuid[22m: 0706c648-0c56-4c74-8dda-5a1aeff7d8b8
[1mstate[22m: [38;2;0;188;125mrunning[0m
[1mimage[22m: /httpserver-python312
[1mresources[22m:
[3m memory[23m: 512MiB
[3m vcpus[23m: 1
[1mservice[22m:
[3m name[23m: hidden-silence-l3g520dw
[3m uuid[23m: 38703973-ccbd-47ae-b4c0-f9f59c1751e7
[3m domains[23m:
[3m - fqdn[23m: hidden-silence-l3g520dw.fra.unikraft.app
[1mnetworks[22m:
[3m- uuid[23m: 6c20f9b2-dc39-41d1-a4bd-250a52a75b19
[3m private-ip[23m: 10.0.9.185
[3m mac[23m: 12:b0:0a:00:09:b9
[1mtimestamps[22m:
[3m created[23m: just now
```
Check that it worked by listing all instances with:
```bash
unikraft instances list
```
```ansi
[1mMETRO[22m [1mNAME[22m [1mSTATE[22m [1mIMAGE[22m [1mARGS[22m [1mMEMORY[22m [1mVCPUS[22m [1mFQDN[22m [1mCREATED[22m
fra httpserver-python312-7lha2 [38;2;0;188;125mrunning[0m /httpserver-python312 512MiB 1 hidden-silence-l3g520dw.fra.unikraft.app just now
fra httpserver-python312-xfgvc [38;2;0;188;125mrunning[0m /httpserver-python312 512MiB 1 icy-violet-hce1qwm5.fra.unikraft.app just now
fra httpserver-python312-lscmn [38;2;0;188;125mrunning[0m /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
[1mmetro[22m: fra
[1mname[22m: nginx-67zbu
[1muuid[22m: 8a8bc1b9-0af6-420e-a426-190dc2da9eaa
[1mstate[22m: [38;2;144;161;185mstarting[0m
[1mimage[22m: /nginx
[1mresources[22m:
[3m memory[23m: 256MiB
[3m vcpus[23m: 1
[1mservice[22m:
[3m uuid[23m: a942b9b5-ad17-3ffe-dcd2-ef4331f9087a
[3m name[23m: nameless-fog-0tvh1uov
[3m domains[23m:
[3m - fqdn[23m: mydomain.com
[3m certificate[23m:
[3m name[23m: mydomain.com-savtl
[3m uuid[23m: 51efebaf-4858-450c-8ba5-8bd1df96c20b
[1mnetworks[22m:
[3m- uuid[23m: 62d9bbf0-aec8-61f6-7bdb-86edf63dd068
[3m private-ip[23m: 10.0.3.3
[3m mac[23m: 12:b0:c6:23:ed:15
[1mtimestamps[22m:
[3m created[23m: 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
[1mMETRO[22m [1mNAME[22m [1mCOMMON-NAME[22m [1mSTATE[22m [1mCREATED[22m [1mNOT-AFTER[22m
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:
InstanceInstance TemplateVolumeVolume 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`:
InstanceInstance TemplateVolumeVolume 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
[1mMETRO[22m [1mNAME[22m [1mCOMMON-NAME[22m [1mSTATE[22m [1mCREATED[22m [1mNOT-AFTER[22m
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
[1mmetro[22m: fra
[1mname[22m: mydomain-cert
[1muuid[22m: 9fabfc61-a168-439e-89e2-0d5308ee9f13
[1mcommon-name[22m: demo.unikraft.dev
[1msubject[22m: CN=demo.unikraft.dev
[1missuer[22m: CN=demo.unikraft.dev
[1mserial-number[22m: 43C149067F939CD41100A74905B80560C315E92F
[1mstate[22m: valid
[1mtimestamps[22m:
[3m created[23m: just now
[3m not-before[23m: 1 minute ago
[3m not-after[23m: 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
[1mmetro[22m: fra
[1mname[22m: nginx-67zbu
[1muuid[22m: 8a8bc1b9-0af6-420e-a426-190dc2da9eaa
[1mstate[22m: [38;2;144;161;185mstarting[0m
[1mimage[22m: /nginx
[1mresources[22m:
[3m memory[23m: 256MiB
[3m vcpus[23m: 1
[1mservice[22m:
[3m uuid[23m: a942b9b5-ad17-3ffe-dcd2-ef4331f9087a
[3m name[23m: nameless-fog-0tvh1uov
[3m domains[23m:
[3m - fqdn[23m: demo.unikraft.dev
[3m certificate[23m:
[3m name[23m: mydomain-cert
[3m uuid[23m: 9fabfc61-a168-439e-89e2-0d5308ee9f13
[1mnetworks[22m:
[3m- uuid[23m: 62d9bbf0-aec8-61f6-7bdb-86edf63dd068
[3m private-ip[23m: 10.0.3.3
[3m mac[23m: 12:b0:c6:23:ed:15
[1mtimestamps[22m:
[3m created[23m: 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 14Earlier 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=""
[1mNAME[0m [1mREADY[0m [1mSTATUS[0m [1mRESTARTS[0m [1mAGE[0m
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=""
[1mNAME[0m [1mSTATUS[0m [1mROLES[0m [1mAGE[0m [1mVERSION[0m
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=""
[1mNAME[0m [1mREADY[0m [1mSTATUS[0m [1mRESTARTS[0m [1mAGE[0m
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
[1mMETRO[22m [1mNAME[22m [1mSTATE[22m [1mIMAGE[22m [1mARGS[22m [1mMEMORY[22m [1mVCPUS[22m [1mFQDN[22m [1mCREATED[22m
fra pfCUcp2sg0toDx3N60IerjgiXdqj8BqTzRPfgIX7M8R [38;2;0;188;125mrunning[0m /nginx 128MiB 1 MFyBmK4Hm5BTcjtMvVTYs2RXn3BzCzSPToyTBvjdVIO-orjlyrac.fra.unikraft.app 2 minutes ago
fra N1z7fNY4CmhOP6UhqIQsbeRmu8a5Ti8zzhk20okCTwW [38;2;0;188;125mrunning[0m /nginx 128MiB 1 MFyBmK4Hm5BTcjtMvVTYs2RXn3BzCzSPToyTBvjdVIO-orjlyrac.fra.unikraft.app 2 minutes ago
fra O830nhB9LeT1sLGh3SiqbjiTmM3u26gOZcvA2eKtOVt [38;2;0;188;125mrunning[0m /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
[1mMETRO[22m [1mNAME[22m [1mAUTOSCALE[22m [1mCREATED[22m [1mFQDN[22m [1mSOURCE[22m [1mDESTINATION[22m [1mHANDLERS[22m
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=""
[1mNAME[0m [1mSTATUS[0m [1mVOLUME[0m [1mCAPACITY[0m [1mACCESS MODES[0m [1mSTORAGECLASS[0m [1mVOLUMEATTRIBUTESCLASS[0m [1mAGE[0m
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=""
[1mLAST SEEN[0m [1mTYPE[0m [1mREASON[0m [1mOBJECT[0m [1mMESSAGE[0m
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 */}
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"
[1mNAME[0m [1mVERSION[0m [1mSIZE[0m
alice/http-python312 latest 77 MB
alice/my-app latest 42 MB
```
```ansi title="local registry"
[1mNAME[0m [1mVERSION[0m [1mSIZE[0m
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
[90m$[0m unikraft instances get httpserver-rust175-tokio
[90mmetro:[0m fra
[90mname:[0m httpserver-rust175-tokio
[90muuid:[0m 78ad8c05-ed7a-4886-abd3-07613dce7c28
[90mstate:[0m [94mstandby[0m
[90mimage:[0m /httpserver-rust175-tokio
[90mresources:[0m
[90mmemory:[0m 256MiB
[90mvcpus:[0m 1
[90mservice:[0m
[90mname:[0m nameless-silence-donj1yl2
[90muuid:[0m cd2910d2-b709-410c-9e04-f5539a7d020a
[90mdomains:[0m
[90m- fqdn:[0m nameless-silence-donj1yl2.fra.unikraft.app
[90mnetworks:[0m
[90m- uuid:[0m d8700d21-63c0-448a-806d-54014584b082
[90mprivate-ip:[0m 10.0.0.25
[90mmac:[0m 12:b0:0a:00:00:19
[90mtimestamps:[0m
[90mcreated:[0m 1 day ago
[90mscale-to-zero:[0m
[90menabled:[0m true
[90mpolicy:[0m on
[90mcooldown-time:[0m 1s
[90mstop:[0m
[90mreason:[0m platform stop
[90mexit-code:[0m 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
[90m$[0m unikraft instances list --field service
[1mNAME UUID FQDN[0m
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
[90m$[0m unikraft instances get httpserver-rust175-tokio --field service
[90mservice:[0m
[90mname:[0m nameless-silence-donj1yl2
[90muuid:[0m cd2910d2-b709-410c-9e04-f5539a7d020a
[90mdomains:[0m
[90m- fqdn:[0m nameless-silence-donj1yl2.fra.unikraft.app
[90mcertificate:[0m
[90mname:[0m
[90muuid:[0m
```
Pass a specific field path to display only that value.
```ansi
[90m$[0m unikraft instances list --field service.name
[1mNAME[0m
falling-sky-k20dl7a7
nameless-silence-donj1yl2
```
```ansi
[90m$[0m unikraft instances get httpserver-rust175-tokio --field service.name
[90mservice:[0m
[90mname:[0m nameless-silence-donj1yl2
```
You can pass fields as a comma-separated list, or repeat `--field` several times.
```ansi
[90m$[0m unikraft instances list --field service.name,resources.memory
[1mNAME MEMORY[0m
falling-sky-k20dl7a7 256MiB
nameless-silence-donj1yl2 256MiB
```
```ansi
[90m$[0m unikraft instances get httpserver-rust175-tokio --field service.name,resources.memory
[90mservice:[0m
[90mname:[0m nameless-silence-donj1yl2
[90mresources:[0m
[90mmemory:[0m 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
[90m$[0m unikraft instances get httpserver-rust175-tokio --field +timing,-networks
[90mmetro:[0m fra
[90mname:[0m httpserver-rust175-tokio
[90muuid:[0m 78ad8c05-ed7a-4886-abd3-07613dce7c28
[90mstate:[0m [94mstandby[0m
[90mimage:[0m /httpserver-rust175-tokio
[90mresources:[0m
[90mmemory:[0m 256MiB
[90mvcpus:[0m 1
[90mservice:[0m
[90mname:[0m nameless-silence-donj1yl2
[90muuid:[0m cd2910d2-b709-410c-9e04-f5539a7d020a
[90mdomains:[0m
[90m- fqdn:[0m nameless-silence-donj1yl2.fra.unikraft.app
[90mtimestamps:[0m
[90mcreated:[0m 1 day ago
[90mscale-to-zero:[0m
[90menabled:[0m true
[90mpolicy:[0m on
[90mcooldown-time:[0m 1s
[90mstop:[0m
[90mreason:[0m platform stop
[90mexit-code:[0m 1
[90mtiming:[0m
[90muptime:[0m 4m22.163s
[90mboot-time:[0m 78.247ms
[90mnet-time:[0m 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
[90m$[0m unikraft instances list --field +timing,-networks
[1mMETRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED UPTIME BOOT-TIME NET-TIME[0m
fra httpserver-rust175-tokio [94mstandby[0m /httpserver-rust175-tokio 256MiB 1 nameless-silence-donj1yl2.fra.unikraft.app 1 day ago 4m22.163s 78.247ms 85.486ms
fra nginx-fj704 [92mrunning[0m /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
[90m$[0m unikraft instances list --sort timestamps.created
[1mMETRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED[0m
fra httpserver-rust175-tokio [94mstandby[0m /httpserver-rust175-tokio 256MiB 2 nameless-silence-donj1yl2.fra.unikraft.app 1 day ago
fra nginx-fj704 [92mrunning[0m /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
[90m$[0m unikraft instances list --filter state==standby
[1mMETRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED[0m
fra httpserver-rust175-tokio [94mstandby[0m /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
[90m$[0m unikraft instances edit httpserver-rust175-tokio --set runtime.env=MYSECRET=secret
[90mmetro:[0m fra
[90mname:[0m httpserver-rust175-tokio
[90muuid:[0m 78ad8c05-ed7a-4886-abd3-07613dce7c28
[90mstate:[0m [94mstandby[0m
[90mimage:[0m /httpserver-rust175-tokio
[92m+ runtime:[0m
[92m+ env:[0m
[92m+ MYSECRET: *[0m
[90mresources:[0m
[90mmemory:[0m 256MiB
[90mvcpus:[0m 1
[90m ...[0m
```
### 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
[90m$[0m unikraft instances edit httpserver-rust175-tokio --del runtime.env=MYSECRET
[90mmetro:[0m fra
[90mname:[0m httpserver-rust175-tokio
[90muuid:[0m 78ad8c05-ed7a-4886-abd3-07613dce7c28
[90mstate:[0m [94mstandby[0m
[90mimage:[0m /httpserver-rust175-tokio
[91m- runtime:[0m
[91m- env:[0m
[91m- MYSECRET: *[0m
```
The `--add` flag works the same way, appending the given value instead of removing it.
```ansi
[90m$[0m unikraft instances edit httpserver-rust175-tokio --add runtime.env=MYSECRET=secret
[90mmetro:[0m fra
[90mname:[0m httpserver-rust175-tokio
[90muuid:[0m 78ad8c05-ed7a-4886-abd3-07613dce7c28
[90mstate:[0m [94mstandby[0m
[90mimage:[0m /httpserver-rust175-tokio
[92m+ runtime:[0m
[92m+ env:[0m
[92m+ MYSECRET: *[0m
[90mresources:[0m
[90mmemory:[0m 256MiB
[90mvcpus:[0m 1
[90m ...[0m
```
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
[90m$[0m unikraft instances edit httpserver-rust175-tokio --visual
[90mmetro:[0m fra
[90mname:[0m httpserver-rust175-tokio
[90muuid:[0m 78ad8c05-ed7a-4886-abd3-07613dce7c28
[90mstate:[0m [94mstandby[0m
[90mimage:[0m demo/httpserver-rust175-tokio
[90mruntime:[0m
[90menv:[0m
[90mMYSECRET:[0m *
[90mresources:[0m
[90mmemory:[0m 256MiB
[91m- vcpus: 1[0m
[92m+ vcpus: 2[0m
[90m ...[0m
```
## 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
[90m$[0m 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
[1mmetro[22m: fra
[1mname[22m: nginx-67zbu
[1muuid[22m: 8a8bc1b9-0af6-420e-a426-190dc2da9eaa
[1mstate[22m: [38;2;144;161;185mstarting[0m
[1mimage[22m: /nginx
[1mresources[22m:
[3m memory[23m: 256MiB
[3m vcpus[23m: 1
[1mservice[22m:
[3m uuid[23m: a942b9b5-ad17-3ffe-dcd2-ef4331f9087a
[3m name[23m: nameless-fog-0tvh1uov
[3m domains[23m:
[3m - fqdn[23m: nameless-fog-0tvh1uov.fra.unikraft.app
[1mnetworks[22m:
[3m- uuid[23m: 62d9bbf0-aec8-61f6-7bdb-86edf63dd068
[3m private-ip[23m: 10.0.3.3
[3m mac[23m: 12:b0:c6:23:ed:15
[1mtimestamps[22m:
[3m created[23m: just now
[1mscale-to-zero[22m:
[3m enabled[23m: true
[3m policy[23m: on
[3m cooldown-time[23m: 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
[1mMETRO[0m [1mNAME[0m [1mSTATE[0m [1mIMAGE[0m [1mARGS[0m [1mMEMORY[0m [1mVCPUS[0m [1mFQDN[0m [1mCREATED[0m
fra nginx-67zbu [38;2;43;127;255mstandby[0m /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
[1mmetro[22m: fra
[1mname[22m: nginx-67zbu
[1muuid[22m: 8a8bc1b9-0af6-420e-a426-190dc2da9eaa
[1mstate[22m: [38;2;43;127;255mstandby[0m
[1mimage[22m: /nginx
[1mresources[22m:
[3m memory[23m: 256MiB
[3m vcpus[23m: 1
[1mservice[22m:
[3m uuid[23m: a942b9b5-ad17-3ffe-dcd2-ef4331f9087a
[3m name[23m: nameless-fog-0tvh1uov
[3m domains[23m:
[3m - fqdn[23m: nameless-fog-0tvh1uov.fra.unikraft.app
[1mnetworks[22m:
[3m- uuid[23m: 62d9bbf0-aec8-61f6-7bdb-86edf63dd068
[3m private-ip[23m: 10.0.3.3
[3m mac[23m: 12:b0:c6:23:ed:15
[1mtimestamps[22m:
[3m created[23m: just now
[1mscale-to-zero[22m:
[3m enabled[23m: true
[3m policy[23m: on
[3m cooldown-time[23m: 1s
[1mstop[22m:
[3m reason[23m: platform stop
[3m exit-code[23m: 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.
DockerfileKraftfileserver.pywrapper.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
[1mmetro[22m: fra
[1mname[22m: nginx-67zbu
[1muuid[22m: 8a8bc1b9-0af6-420e-a426-190dc2da9eaa
[1mstate[22m: [38;2;144;161;185mstarting[0m
[1mimage[22m: /nginx
[1mresources[22m:
[3m memory[23m: 256MiB
[3m vcpus[23m: 1
[1mservice[22m:
[3m uuid[23m: a942b9b5-ad17-3ffe-dcd2-ef4331f9087a
[3m name[23m: nameless-fog-0tvh1uov
[3m domains[23m:
[3m - fqdn[23m: nameless-fog-0tvh1uov.fra.unikraft.app
[1mnetworks[22m:
[3m- uuid[23m: 62d9bbf0-aec8-61f6-7bdb-86edf63dd068
[3m private-ip[23m: 10.0.3.3
[3m mac[23m: 12:b0:c6:23:ed:15
[1mtimestamps[22m:
[3m created[23m: 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
[1mmetro[22m: fra
[1mname[22m: nginx-0s295
[1muuid[22m: 9b1c8e5c-7a0d-4f1e-9c3a-2b8d9f0e5a1f
[1mstate[22m: [38;2;144;161;185mstarting[0m
[1mimage[22m: /nginx
[1mresources[22m:
[3m memory[23m: 256MiB
[3m vcpus[23m: 1
[1mservice[22m:
[3m uuid[23m: a942b9b5-ad17-3ffe-dcd2-ef4331f9087a
[3m name[23m: nameless-fog-0tvh1uov
[3m domains[23m:
[3m - fqdn[23m: nameless-fog-0tvh1uov.fra.unikraft.app
[1mnetworks[22m:
[3m- uuid[23m: adf1f17d-bad1-49b2-a126-ed730e06379a
[3m private-ip[23m: 10.0.1.4
[3m mac[23m: 12:b0:0a:00:01:05
[1mtimestamps[22m:
[3m created[23m: 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
[1mmetro[22m: fra
[1mname[22m: nameless-fog-0tvh1uov
[1muuid[22m: a942b9b5-ad17-3ffe-dcd2-ef4331f9087a
[1mpersistent[22m: true
[1mlimits[22m:
[3m soft[23m: 1
[3m hard[23m: 65535
[1mtimestamps[22m:
[3m created[23m: just now
[1mdomains[22m:
[3m- fqdn[23m: nameless-fog-0tvh1uov.fra.unikraft.app
[1minstances[22m:
[3m- name[23m: nginx-0s295
[3m uuid[23m: 9b1c8e5c-7a0d-4f1e-9c3a-2b8d9f0e5a1f
[3m- name[23m: nginx-67zbu
[3m uuid[23m: 8a8bc1b9-0af6-420e-a426-190dc2da9eaa
[1mservices[22m:
[3m- source[23m: 443
[3m destination[23m: 8080
[3m handlers[23m: ["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
[1mmetro[22m: fra
[1mname[22m: nginx-67zbu
[1muuid[22m: 8a8bc1b9-0af6-420e-a426-190dc2da9eaa
[1mstate[22m: [38;2;144;161;185mstarting[0m
[1mimage[22m: /nginx
[1mresources[22m:
[3m memory[23m: 256MiB
[3m vcpus[23m: 1
[1mservice[22m:
[3m uuid[23m: a942b9b5-ad17-3ffe-dcd2-ef4331f9087a
[3m name[23m: nameless-fog-0tvh1uov
[3m domains[23m:
[3m - fqdn[23m: nameless-fog-0tvh1uov.fra.unikraft.app
[1mnetworks[22m:
[3m- uuid[23m: 62d9bbf0-aec8-61f6-7bdb-86edf63dd068
[3m private-ip[23m: 10.0.3.3
[3m mac[23m: 12:b0:c6:23:ed:15
[1mtimestamps[22m:
[3m created[23m: 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
[1mMETRO[22m [1mNAME[22m [1mSTATE[22m [1mSIZE[22m [1mCREATED[22m
fra wordpress-db-data [38;2;0;188;125mavailable[0m 512MiB just now
fra wordpress-wordpress-data [38;2;0;188;125mavailable[0m 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
[1mmetro[22m: fra
[1mname[22m: mariadb
[1muuid[22m: 3af0eefb-29a8-4634-9b94-abf62d2fb90a
[1mstate[22m: [38;2;144;161;185mstarting[0m
[1mimage[22m: /mariadb
[1mruntime[22m:
[3m env[23m:
MARIADB_ROOT_PASSWORD: *
[1mresources[22m:
[3m memory[23m: 1GiB
[3m vcpus[23m: 1
[1mservice[22m:
[3m name[23m: snowy-glitter-3uylzbqk
[3m uuid[23m: 0b3303d0-1a2e-4353-a9c7-29151263ef9f
[3m domains[23m:
[3m - fqdn[23m: wordpress-mariadb.internal
[1mvolumes[22m:
[3m- name[23m: wordpress-db-data
[3m uuid[23m: 7b232d0f-dfbf-4ff7-9208-106b5d92bbe3
[3m at[23m: /var/lib/mysql
[1mnetworks[22m:
[3m- uuid[23m: 8cad0682-a700-4303-af8c-ebd18465ed32
[3m private-ip[23m: 10.0.1.73
[3m mac[23m: 12:b0:0a:00:01:49
[1mtimestamps[22m:
[3m created[23m: just now
[1mscale-to-zero[22m:
[3m enabled[23m: true
[3m policy[23m: idle
[3m stateful[23m: true
[3m cooldown-time[23m: 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
[1mmetro[22m: fra
[1mname[22m: wordpress
[1muuid[22m: ee9f5599-b33b-44f1-ab57-3871b440e810
[1mstate[22m: [38;2;144;161;185mstarting[0m
[1mimage[22m: /wordpress
[1mruntime[22m:
[3m env[23m:
WORDPRESS_DB_HOST: wordpress-mariadb.internal
[1mresources[22m:
[3m memory[23m: 2GiB
[3m vcpus[23m: 1
[1mservice[22m:
[3m name[23m: damp-lake-n09gzguc
[3m uuid[23m: 7f21d778-487f-4130-a94a-34b86862c3dd
[3m domains[23m:
[3m - fqdn[23m: damp-lake-n09gzguc.fra.unikraft.app
[1mvolumes[22m:
[3m- name[23m: wordpress-wordpress-data
[3m uuid[23m: 0bec8b93-0691-43b6-b188-4ac170a3d0c7
[3m at[23m: /var/www/html
[1mnetworks[22m:
[3m- uuid[23m: e0f13623-fd04-4a69-9caf-47142ce47c4c
[3m private-ip[23m: 10.0.0.33
[3m mac[23m: 12:b0:0a:00:00:21
[1mtimestamps[22m:
[3m created[23m: just now
[1mscale-to-zero[22m:
[3m enabled[23m: true
[3m policy[23m: on
[3m cooldown-time[23m: 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
[1mMETRO[22m [1mNAME[22m [1mSTATE[22m [1mIMAGE[22m [1mARGS[22m [1mMEMORY[22m [1mVCPUS[22m [1mFQDN[22m [1mCREATED[22m
fra mariadb [38;2;43;127;255mstandby[0m /mariadb 1GiB 1 wordpress-mariadb.internal 5 minutes ago
fra wordpress [38;2;0;188;125mrunning[0m /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
[1mmetro[22m: fra
[1mname[22m: wordpress-fx5rb
[1muuid[22m: bfb9d151-1604-452a-b2e0-f737486744df
[1mstate[22m: [38;2;144;161;185mstarting[0m
[1mimage[22m: /wordpress
[1mresources[22m:
[3m memory[23m: 4096MiB
[3m vcpus[23m: 1
[1mservice[22m:
[3m uuid[23m: 398fe5bb-e172-465e-8f74-56ffcfb24a3d
[3m name[23m: cool-silence-h5c1es4z
[3m domains[23m:
[3m - fqdn[23m: cool-silence-h5c1es4z.fra.unikraft.app
[1mnetworks[22m:
[3m- uuid[23m: 26c200e0-43eb-dd46-e4be-e9505ff677d1
[3m private-ip[23m: 10.0.3.1
[3m mac[23m: 12:b0:4e:20:b3:e7
[1mtimestamps[22m:
[3m created[23m: 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
[1mMETRO[22m [1mNAME[22m [1mSTATE[22m [1mIMAGE[22m [1mARGS[22m [1mMEMORY[22m [1mVCPUS[22m [1mFQDN[22m [1mCREATED[22m
fra wordpress-fx5rb [38;2;0;188;125mrunning[0m /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
[1mmetro[22m: fra
[1mname[22m: wazero-import-go-r4dx8
[1muuid[22m: a763e1c3-bb38-475f-95b6-1e78d8ca74fc
[1mstate[22m: [38;2;144;161;185mstarting[0m
[1mimage[22m: /wazero-import-go
[1mresources[22m:
[3m memory[23m: 512MiB
[3m vcpus[23m: 1
[1mservice[22m:
[3m uuid[23m: f38a171d-e283-24ca-1158-c7907948071e
[3m name[23m: cool-morning-camrrhsa
[3m domains[23m:
[3m - fqdn[23m: cool-morning-camrrhsa.fra.unikraft.app
[1mnetworks[22m:
[3m- uuid[23m: bc4bb64c-8185-3b83-8693-b1464ab4723e
[3m private-ip[23m: 10.0.6.7
[3m mac[23m: 12:b0:05:55:02:fe
[1mtimestamps[22m:
[3m created[23m: 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
[1mMETRO[22m [1mNAME[22m [1mSTATE[22m [1mIMAGE[22m [1mARGS[22m [1mMEMORY[22m [1mVCPUS[22m [1mFQDN[22m [1mCREATED[22m
fra wazero-import-go-r4dx8 [38;2;0;188;125mrunning[0m /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
[1mmetro[22m: fra
[1mname[22m: vsftpd
[1muuid[22m: 186a46a0-7c89-4bfd-83a8-649bcc60a96e
[1mstate[22m: [38;2;144;161;185mstarting[0m
[1mimage[22m: /vsftpd
[1mresources[22m:
[3m memory[23m: 1024MiB
[3m vcpus[23m: 1
[1mservice[22m:
[3m uuid[23m: 4814b43a-c1d3-48f0-ef3e-9dba8bcaba25
[3m name[23m: broken-orangutan-jypu2z53
[3m domains[23m:
[3m - fqdn[23m: broken-orangutan-jypu2z53.fra.unikraft.app
[1mnetworks[22m:
[3m- uuid[23m: 6adc6c29-5c9b-e472-70ff-fc3f3816d5a2
[3m private-ip[23m: 10.0.0.109
[3m mac[23m: 12:b0:17:ff:e4:c7
[1mtimestamps[22m:
[3m created[23m: 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
[1mMETRO[22m [1mNAME[22m [1mSTATE[22m [1mSIZE[22m [1mCREATED[22m
fra vsftpd-workspace [38;2;0;188;125mmounted[0m 1.0GiB 9 minutes ago
```
You can list information about the instance by running:
```bash
unikraft instances list
```
```ansi
[1mMETRO[22m [1mNAME[22m [1mSTATE[22m [1mIMAGE[22m [1mARGS[22m [1mMEMORY[22m [1mVCPUS[22m [1mFQDN[22m [1mCREATED[22m
fra vsftpd [38;2;43;127;255mstandby[0m /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
[1mmetro[22m: fra
[1mname[22m: code-server-6gxsp
[1muuid[22m: c1a619a0-e222-4042-94b8-ba4b39353417
[1mstate[22m: [38;2;144;161;185mstarting[0m
[1mimage[22m: /visual-studio-code-server
[1mresources[22m:
[3m memory[23m: 2048MiB
[3m vcpus[23m: 1
[1mservice[22m:
[3m uuid[23m: 4a0866e4-3bec-3666-c4aa-61672de542e2
[3m name[23m: blue-shape-chmxf1g4
[3m domains[23m:
[3m - fqdn[23m: blue-shape-chmxf1g4.fra.unikraft.app
[1mnetworks[22m:
[3m- uuid[23m: dcec209f-31a6-b355-a88e-f5ac3edfa20e
[3m private-ip[23m: 10.0.0.49
[3m mac[23m: 12:b0:ec:d2:df:1c
[1mtimestamps[22m:
[3m created[23m: 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
[1mMETRO[22m [1mNAME[22m [1mSTATE[22m [1mSIZE[22m [1mCREATED[22m
fra code-workspace [38;2;0;188;125mmounted[0m 1.0GiB 13 minutes ago
```
You can list information about the instance by running:
```bash
unikraft instances list
```
```ansi
[1mMETRO[22m [1mNAME[22m [1mSTATE[22m [1mIMAGE[22m [1mARGS[22m [1mMEMORY[22m [1mVCPUS[22m [1mFQDN[22m [1mCREATED[22m
fra code-server-6gxsp [38;2;43;127;255mstandby[0m /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
[1mmetro[22m: fra
[1mname[22m: redis-6vgvc
[1muuid[22m: 63b86d17-06ca-4f95-b921-56e5b3245554
[1mstate[22m: [38;2;144;161;185mstarting[0m
[1mimage[22m: /redis
[1mresources[22m:
[3m memory[23m: 256MiB
[3m vcpus[23m: 1
[1mservice[22m:
[3m name[23m: snowy-water-wivk1i4r
[3m uuid[23m: 6612de91-d639-4b5a-95d1-b7ae6e91e3c1
[3m domains[23m:
[3m - fqdn[23m: tyk-redis.internal
[1mnetworks[22m:
[3m- uuid[23m: 19e4b80a-9501-448d-99a6-ec3f7b90805e
[3m private-ip[23m: 10.0.0.29
[3m mac[23m: 12:b0:0a:00:01:49
[1mtimestamps[22m:
[3m created[23m: just now
[1mscale-to-zero[22m:
[3m enabled[23m: true
[3m policy[23m: idle
[3m stateful[23m: true
[3m cooldown-time[23m: 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
[1mmetro[22m: fra
[1mname[22m: tyk-s9ixd
[1muuid[22m: 4e8a5e56-2d0b-4ca4-88b4-aa816129a66d
[1mstate[22m: [38;2;144;161;185mstarting[0m
[1mimage[22m: /tyk
[1mresources[22m:
[3m memory[23m: 256MiB
[3m vcpus[23m: 1
[1mservice[22m:
[3m name[23m: icy-haze-8ph4u8cz
[3m uuid[23m: 89079646-353a-4a19-99ac-5498c7d626ad
[3m domains[23m:
[3m - fqdn[23m: icy-haze-8ph4u8cz.fra.unikraft.app
[1mnetworks[22m:
[3m- uuid[23m: 8085804f-0fe0-4847-ad6e-8518edba126e
[3m private-ip[23m: 10.0.0.1
[3m mac[23m: 12:b0:0a:00:0e:b1
[1mtimestamps[22m:
[3m created[23m: 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
[1mMETRO[22m [1mNAME[22m [1mSTATE[22m [1mIMAGE[22m [1mMEMORY[22m [1mVCPUS[22m [1mFQDN[22m [1mCREATED[22m
fra tyk-s9ixd [92mstandby /tyk[0m 256MiB 1 icy-haze-8ph4u8cz.fra.unikraft.app just now
fra redis-6vgvc [92mrunning /redis[0m 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
[1mmetro[22m: fra
[1mname[22m: traefik-wqe7e
[1muuid[22m: 69d25b0b-1813-4a3f-88e6-64abbc78b359
[1mstate[22m: [38;2;144;161;185mstarting[0m
[1mimage[22m: /traefik
[1mresources[22m:
[3m memory[23m: 1024MiB
[3m vcpus[23m: 1
[1mservice[22m:
[3m uuid[23m: ec7570da-5700-01b5-aaa1-0734498c11eb
[3m name[23m: holy-cherry-rye39b1x
[3m domains[23m:
[3m - fqdn[23m: holy-cherry-rye39b1x.fra.unikraft.app
[1mnetworks[22m:
[3m- uuid[23m: b59f4362-dc72-7efe-477b-1efe227e1b08
[3m private-ip[23m: 10.0.28.16
[3m mac[23m: 12:b0:31:58:d7:d0
[1mtimestamps[22m:
[3m created[23m: 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
[1mMETRO[22m [1mNAME[22m [1mSTATE[22m [1mIMAGE[22m [1mARGS[22m [1mMEMORY[22m [1mVCPUS[22m [1mFQDN[22m [1mCREATED[22m
fra traefik-wqe7e [38;2;0;188;125mrunning[0m /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