# Build and Test Environments

Build and test workloads often need dynamic code execution in a controlled runtime.
You want to swap test logic, keep startup latency low, and avoid rebuilding full images for every test change.

With **Unikraft Cloud**, you can run a reusable Go runtime image and inject test logic via ROMs.
At startup, the instance compiles ROM code into a Go plugin and executes it, giving you fast iteration with strong isolation.

## Why run build and test environments on Unikraft Cloud

### Fast test iteration

Build and test logic changes frequently.
With ROM-based deployment, you package and push a small ROM instead of rebuilding the full runtime image:

* Keep the runtime stable.
* Update only test payloads.
* Roll forward and back by switching ROM image tags.

### Runtime compilation in isolated microVMs

Each environment runs in its own microVM.
The runtime compiles ROM code inside the instance (`go build -buildmode=plugin`) and loads it as a plugin.
This isolates compilation and execution while keeping the runtime reusable.

### Template-based cold start optimization

The base image can create an [instance template](/platform/instances#instance-templates).
New environments launch from that template, reducing startup time for bursty CI-style workloads.

### Scale-to-zero economics

Test environments are often idle between runs.
With scale-to-zero enabled, idle instances consume no active resources while still waking when needed.

## Getting started

This guide uses the [`build-environments`](https://github.com/unikraft-cloud/examples/tree/main/build-environments) example.
The base image contains a Go server (`server.go`) that:

1. Writes to `/uk/libukp/template_instance` to create a template.
2. Reads ROM source from `/rom/rom.go`.
3. Compiles it to `/run/rom.so` using `go build -buildmode=plugin`.
4. Loads the `Handler()` function from the plugin and serves its output over HTTP.

### Prerequisites

1. Install the CLI:
   Use the [unikraft CLI](/docs/cli/unikraft) or the legacy [kraft CLI](https://unikraft.org/docs/cli/install).
   You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way is via [Docker](https://docs.docker.com/engine/install/).

1. Clone the examples repository and enter the example directory:

```bash
git clone https://github.com/unikraft-cloud/examples
cd examples/build-environments/
```

Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you.
This guide uses `fra` (Frankfurt, 🇩🇪):

<CodeTabs syncKey="cli-tool">

```bash title="unikraft"
unikraft login
```

```bash title="kraft"
# Set Unikraft Cloud access token
export UKC_TOKEN=token
export UKC_METRO=fra
```

</CodeTabs>

### Step 1: Package and push the base runtime image

Package and push the base Go runtime image (see `server.go` for the runtime implementation):

<CodeTabs syncKey="cli-tool">

```bash title="unikraft"
unikraft build . --output <my-org>/go-build-env:latest
```

```bash title="kraft"
kraft pkg \
   --name index.unikraft.io/<my-org>/go-build-env:latest \
   --plat kraftcloud \
   --arch x86_64 \
   --rootfs-type erofs \
   --push \
   .
```

</CodeTabs>

The server in `server.go` loads `/rom/rom.go`, compiles it to `/run/rom.so` using `go build -buildmode=plugin`, and invokes `Handler()` from the plugin.

### Step 2: Create a template from the base image

Create a short-lived instance from the base image (without ROM attached).
The server writes to `/uk/libukp/template_instance` and turns the instance into a [template](/platform/instances#instance-templates) before serving requests:

<CodeTabs syncKey="cli-tool">

```bash title="unikraft"
unikraft run --metro fra \
  --name go-build-env \
  -m 512M \
  --image <my-org>/go-build-env:latest
```

```bash title="kraft"
kraft cloud instance create \
  --start \
  --name go-build-env \
  -M 512Mi \
  <my-org>/go-build-env:latest
```

</CodeTabs>

The output shows the instance address and other details:

<CodeTabs syncKey="cli-tool">

```ansi title="unikraft"
metro:        fra
name:         go-build-env
uuid:         650dbbe7-3949-4c93-88e7-6619a9216e0c
state:        [94mstarting[0m
image:        <my-org>/go-build-env
resources:
  memory:     512MiB
  vcpus:      1
networks:
- uuid:       6f7a8b9c-0d1e-2f3a-4b5c-f6a7b8c9d0e1
  private-ip: 10.0.5.4
  mac:        12:b0:6c:3e:ab:95
timestamps:
  created:    just now
```

```ansi title="kraft"
[90m[[0m[92m●[0m[90m][0m Deployed successfully!
 [90m│[0m
 [90m├[0m[90m─────────[0m [90mname[0m: go-build-env
 [90m├[0m[90m─────────[0m [90muuid[0m: 650dbbe7-3949-4c93-88e7-6619a9216e0c
 [90m├[0m[90m────────[0m [90mmetro[0m: https://api.fra.unikraft.cloud/v1
 [90m├[0m[90m────────[0m [90mstate[0m: [92mstarting[0m
 [90m├[0m[90m────────[0m [90mimage[0m: oci://unikraft.io/<my-org>/go-build-env@sha256:1f57e9bb8702d031743acf43164b24cf182158c398f1eda8c5583208ccc9c300
 [90m├[0m[90m───────[0m [90mmemory[0m: 512 MiB
 [90m├[0m[90m─[0m [90mprivate fqdn[0m: go-build-env.internal
 [90m└[0m[90m───[0m [90mprivate ip[0m: 10.0.5.4
```

</CodeTabs>

This instance is short-lived, since right before the server starts, it triggers a conversion into a template.
To check that the template is ready, run:

<CodeTabs syncKey="cli-tool">

```bash title="unikraft"
unikraft instances templates list
```

```bash title="kraft"
kraft cloud instance template list
```

</CodeTabs>

<br/>

<CodeTabs syncKey="cli-tool">

```ansi title="unikraft"
[1mMETRO[0m  [1mNAME[0m          [1mSTATE[0m     [1mIMAGE[0m                  [1mARGS[0m  [1mMEMORY[0m  [1mVCPUS[0m  [1mCREATED[0m
fra    go-build-env  [94mtemplate[0m  <my-org>/go-build-env        512MiB  1      just now
```

```ansi title="kraft"
[1mNAME[0m          [1mIMAGE[0m                                                                                                        [1mARGS[0m  [1mCREATED AT[0m
go-build-env  oci://unikraft.io/<my-org>/go-build-env@sha256:1cbd6474386c9df546820cd0522030a1bd8702c59490548f10ae10fb8b0a6e14         20 seconds ago
```

</CodeTabs>

### Step 3: Package ROM payloads

Each ROM contains a Go function implementation.

<CodeTabs syncKey="cli-tool">

```bash title="unikraft"
unikraft build rom1/ --output <my-org>/go-rom1:latest
unikraft build rom2/ --output <my-org>/go-rom2:latest
```

```bash title="kraft"
kraft pkg \
   --rom ./fs \
   --rom-type erofs \
   --plat kraftcloud \
   --arch x86_64 \
   --name index.unikraft.io/<my-org>/go-rom1:latest \
   --push \
   rom1/
kraft pkg \
   --rom ./fs \
   --rom-type erofs \
   --plat kraftcloud \
   --arch x86_64 \
   --name index.unikraft.io/<my-org>/go-rom2:latest \
   --push \
   rom2/
```

</CodeTabs>

### Step 4: Launch test environments from template

Create an instance with the first ROM:

<CodeTabs syncKey="cli-tool">

```bash title="unikraft"
unikraft run --metro fra \
  --name go-build-env-rom1 \
  -p 443:8080/tls+http \
  --scale-to-zero policy=on,cooldown-time=1000,stateful=true \
  --rom image=<my-org>/go-rom1:latest,at=/rom \
  --template go-build-env
```

```bash title="kraft"
# kraft does not support creating instances with attached ROMs, but you can use the API directly
curl -X POST "$UKC_METRO/instances" \
   -H "Accept: application/json" \
   -H "Authorization: Bearer $UKC_TOKEN" \
   -H "Content-Type: application/json" \
   -d '{
   "name": "go-build-env-rom1",
   "template": {
      "name": "go-build-env"
   },
   "autostart": true,
   "service_group": {
      "services": [
         {
            "port": 443,
            "destination_port": 8080,
            "handlers": ["tls", "http"]
         }
      ]
   },
   "scale_to_zero": {
      "policy": "on",
      "stateful": true,
      "cooldown_time_ms": 1000
   },
   "roms": [
      {
         "name": "go_function",
         "image": "index.unikraft.io/<my-org>/go-rom1:latest",
         "at": "/rom"
      }
   ]
}'
```

</CodeTabs>

The instance will compile the ROM into a plugin on first start, which may take a few seconds.
To check the progress, you can view the instance logs:

<CodeTabs syncKey="cli-tool">

```bash title="unikraft"
unikraft instances logs go-build-env-rom1 -f
```

```bash title="kraft"
kraft cloud instance logs go-build-env-rom1 -f
```

</CodeTabs>

Create another instance with the second ROM:

<CodeTabs syncKey="cli-tool">

```bash title="unikraft"
unikraft run --metro fra \
  --name go-build-env-rom2 \
  -p 443:8080/tls+http \
  --scale-to-zero policy=on,cooldown-time=1000,stateful=true \
  --rom image=<my-org>/go-rom2:latest,at=/rom \
  --template go-build-env
```

```bash title="kraft"
# kraft does not support creating instances with attached ROMs, but you can use the API directly
curl -X POST "$UKC_METRO/instances" \
   -H "Accept: application/json" \
   -H "Authorization: Bearer $UKC_TOKEN" \
   -H "Content-Type: application/json" \
   -d '{
   "name": "go-build-env-rom2",
   "template": {
      "name": "go-build-env"
   },
   "autostart": true,
   "service_group": {
      "services": [
         {
            "port": 443,
            "destination_port": 8080,
            "handlers": ["tls", "http"]
         }
      ]
   },
   "scale_to_zero": {
      "policy": "on",
      "stateful": true,
      "cooldown_time_ms": 1000
   },
   "roms": [
      {
         "name": "go_function",
         "image": "index.unikraft.io/<my-org>/go-rom2:latest",
         "at": "/rom"
      }
   ]
}'
```

</CodeTabs>

The instance will compile the ROM into a plugin on first start, which may take a few seconds.
To check the progress, you can view the instance logs:

<CodeTabs syncKey="cli-tool">

```bash title="unikraft"
unikraft instances logs go-build-env-rom2 -f
```

```bash title="kraft"
kraft cloud instance logs go-build-env-rom2 -f
```

</CodeTabs>

Both instances run the same base Go runtime, but execute different ROM payloads, demonstrating fast iteration with reusable layers.

List the instances and note their FQDN values:

<CodeTabs syncKey="cli-tool">

```bash title="unikraft"
unikraft instances list
```

```bash title="kraft"
kraft cloud instance list
```

</CodeTabs>

<br/>

<CodeTabs syncKey="cli-tool">

```ansi title="unikraft"
[1mMETRO[0m  [1mNAME[0m               [1mSTATE[0m    [1mIMAGE[0m                  [1mARGS[0m  [1mMEMORY[0m  [1mVCPUS[0m  [1mFQDN[0m                                      [1mCREATED[0m
fra    go-build-env-rom2  [94mstandby[0m  <my-org>/go-build-env        512MiB  1      nameless-wood-gw7pbnls.fra.unikraft.app   2 minutes ago
fra    go-build-env-rom1  [94mstandby[0m  <my-org>/go-build-env        512MiB  1      sparkling-dawn-syowlbtj.fra.unikraft.app  3 minutes ago
```

```ansi title="kraft"
[1mNAME[0m               [1mFQDN[0m                                      [1mSTATE[0m    [1mSTATUS[0m   [1mIMAGE[0m                                                     [1mMEMORY[0m   [1mVCPUS[0m  [1mARGS[0m  [1mBOOT TIME[0m
go-build-env-rom2  nameless-wood-gw7pbnls.fra.unikraft.app   [94mstandby[0m  standby  oci://unikraft.io/<my-org>/go-build-env@sha256:1cbd64...  512 MiB  1            6.98 ms
go-build-env-rom1  sparkling-dawn-syowlbtj.fra.unikraft.app  [94mstandby[0m  standby  oci://unikraft.io/<my-org>/go-build-env@sha256:1cbd64...  512 MiB  1            7.86 ms
```

</CodeTabs>

Test both instances using the FQDNs from the listing:

```bash
curl https://sparkling-dawn-syowlbtj.fra.unikraft.app
curl https://nameless-wood-gw7pbnls.fra.unikraft.app
```

```text
Bye, World!
Auf Wiedersehen!
```

## Why this pattern works

This build/test environment uses three reusable layers:

1. **Base runtime image**:
A Go server plus toolchain.
Build once, reuse many times.

1. **ROM payload**:
A source-only image (`rom.go`).
Update per test case.

1. **Template snapshot**:
A pre-initialized runtime used to speed up new instance startup.

## Cleanup

<CodeTabs syncKey="cli-tool">

```bash title="unikraft"
unikraft instances delete go-build-env-rom1 go-build-env-rom2
unikraft instances template delete go-build-env
```

```bash title="kraft"
kraft cloud instance remove go-build-env-rom1 go-build-env-rom2
kraft cloud instance template remove go-build-env
```

</CodeTabs>

## Learn more

<CodeTabs syncKey="cli-tool">

```bash title="unikraft"
unikraft --help
```

```bash title="kraft"
kraft cloud --help
```

</CodeTabs>

Or visit the [CLI Reference](/docs/cli/unikraft) or the [legacy CLI reference](/docs/cli/kraft/overview).
