Skip to content

Knative + urunc: Deploying Serverless Unikernels🔗

This guide walks you through deploying Knative Serving with urunc support on Kubernetes. We provide pre-built binaries for quick setup, or you can build Knative from source using ko for custom configurations.

Prerequisites🔗

Install Knative Serving🔗

Apply the pre-built Knative manifests with urunc support:

kubectl apply -f https://s3.nbfc.io/knative/knative-v1.17.0-urunc-5220308.yaml

Note: There are cases where due to the large manifests, kubectl fails. Try a second time, or use kubectl create -f https://s3.nbfc.io/knative/knative-v1.17.0-urunc-5220308.yaml

Option 2: Build Knative from Source🔗

If you prefer to build Knative yourself, follow these steps.

Prerequisites for building from source🔗

  • A Docker-compatible registry (e.g. Harbor, Docker Hub)
  • Ubuntu 20.04 or newer
  • Basic git, curl, and kubectl installed
  • Docker installed (needed for container registry interaction and ko builds)
  • Go (>= 1.23, tested with 1.27.2) installed
  • ko installed

Set your container registry🔗

Note: You should be able to use Docker Hub for this. e.g. <yourdockerhubid>/knative

export KO_DOCKER_REPO='<your-registry>/knative'

Clone urunc-enabled Knative Serving and build🔗

git clone https://github.com/nubificus/serving -b feat_urunc
cd serving/
ko resolve -Rf ./config/core/ > knative-custom.yaml

Apply knative's manifests to the local k8s🔗

kubectl apply -f knative-custom.yaml

Setup Networking (Kourier)🔗

Install kourier, patch ingress and domain configs🔗

kubectl apply -f https://github.com/knative-extensions/net-kourier/releases/latest/download/kourier.yaml

kubectl patch configmap/config-network -n knative-serving --type merge -p '{"data":{"ingress.class":"kourier.ingress.networking.knative.dev"}}'

kubectl patch configmap/config-domain -n knative-serving --type merge -p '{"data":{"127.0.0.1.nip.io":""}}'

Enable RuntimeClass and urunc Support🔗

Install urunc🔗

You can follow the documentation to install urunc from: Installing

Enable runtimeClass for services, nodeSelector and affinity🔗

kubectl patch configmap/config-features --namespace knative-serving --type merge --patch '{"data":{
  "kubernetes.podspec-affinity":"enabled",
  "kubernetes.podspec-runtimeclassname":"enabled",
  "kubernetes.podspec-nodeselector":"enabled"
}}'

Deploy a Sample urunc Service🔗

Create a simple httpreply service, based on a simple C program:

kubectl apply -f https://raw.githubusercontent.com/nubificus/c-httpreply/refs/heads/main/service.yaml

Check Knative Service🔗

kubectl get ksvc -A -o wide 

Output should look like:

$ kubectl get ksvc -A -o wide
NAMESPACE   NAME              URL                                               LATESTCREATED           LATESTREADY             READY   REASON
default     hellocontainerc   http://hellocontainerc.default.127.0.0.1.nip.io   hellocontainerc-00001   hellocontainerc-00001   True

Get the ingress IP🔗

Before testing the service, get the IP address of the Kourier internal service:

kubectl get svc -n kourier-system kourier-internal -o jsonpath='{.spec.clusterIP}'

This command returns the internal ClusterIP (e.g., 10.244.9.220). Use this value in the next curl command.

Test the service🔗

Replace <INGRESS_IP> with the IP address from the previous step:

curl -v -H "Host: hellocontainerc.default.127.0.0.1.nip.io" http://<INGRESS_IP>

Now, let's create a urunc-compatible function. Create a file named unikernel-service.yaml with the following content (based on Unikraft's httpreply example):

apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: hellounikernelqemu
spec:
  template:
    spec:
      runtimeClassName: urunc
      containers:
        - image: ghcr.io/urunc-dev/unikernel-build/chttp-unikraft-qemu:latest
          imagePullPolicy: Always

Note: Specifying a container name is not required because Knative defaults it to user-container automatically. However, do not set a custom container name: urunc specifically checks for user-container to configure static networking (iptables NAT) for Knative compatibility. If a custom name is used, urunc defaults to dynamic networking (tc redirect filters), which redirects all container traffic directly to the VM and breaks the queue-proxy sidecar.

Apply the manifest:

kubectl apply -f unikernel-service.yaml

You should be able to see this being created:

$ kubectl get ksvc -o wide
NAME                 URL                                                  LATESTCREATED                  LATESTREADY                    READY   REASON
hellounikernelqemu   http://hellounikernelqemu.default.127.0.0.1.nip.io   hellounikernelqemu-00001       hellounikernelqemu-00001       True

Once it's in a Ready state, invoke the function using the ingress IP you obtained earlier:

$ curl -v -H "Host: hellounikernelqemu.default.127.0.0.1.nip.io" http://10.244.9.220:80
*   Trying 10.244.9.220:80...
* Connected to 10.244.9.220 (10.244.9.220) port 80 (#0)
> GET / HTTP/1.1
> Host: hellounikernelqemu.default.127.0.0.1.nip.io
> User-Agent: curl/7.81.0
> Accept: */* 
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< content-type: text/plain
< date: Thu, 15 Oct 2026 12:39:11 GMT
< x-envoy-upstream-service-time: 25
< server: envoy
< transfer-encoding: chunked
<
Hello from urunc!
* Connection #0 to host 10.244.9.220 left intact