NanoVMs: A Lightweight Unikernel Solution – When Docker Isn’t the Final Destination

Virtualization tutorial - IT technology blog
Virtualization tutorial - IT technology blog

Is Docker Really the Final Destination?

DevOps engineers are likely well-acquainted with Docker. It has become the dominant solution for the classic “it works on my machine but fails on the server” problem. However, after several years of running my own Proxmox homelab (currently carrying about 12 VMs of various sizes), I’ve started to notice the overhead that traditional containers leave behind.

Take a simple Node.js container, for example. You usually have to carry around a stripped-down Linux OS like Debian or Alpine, weighing hundreds of MBs. Regarding security, containers still share the kernel with the host. A single kernel vulnerability could allow an attacker to escape the container and take control of your entire server. That is why I looked into Unikernels, specifically NanoVMs.

What is a Unikernel? Why is NanoVMs Gaining Attention?

Imagine you only want to run a single executable (binary). Instead of installing a bulky Linux OS and then running Docker, a Unikernel packages only the application and the absolute minimum system libraries. The result is a tiny image file that boots directly on Hypervisors like KVM, QEMU, or AWS Firecracker.

What Problems Does NanoVMs Solve?

  • Near-zero Attack Surface: The system has no shell (bash), no SSH, and no user management. Attackers cannot “remote” into the VM simply because there are no tools to type commands into.
  • Extreme Speed: Boot times are measured in milliseconds. The system doesn’t waste resources on unnecessary Linux background processes.
  • Total Isolation: Each application runs on a Unikernel instance with its own separate kernel. It doesn’t share the kernel with the host like Docker does.

Environment Preparation

To get started, you’ll need a machine running Linux (Ubuntu is the best choice). Since NanoVMs leverages KVM for acceleration, make sure your machine supports hardware virtualization. If you’re using a VM on Proxmox like I am, remember to enable “Nested Virtualization.”

Install OPS – the command-line tool (CLI) to control NanoVMs:

curl https://ops.city/get.sh -sSfL | sh

Check the version to ensure everything is ready:

ops version

Hands-on: Deploying a Go Application with NanoVMs

The Go language is an excellent choice here. It compiles into a static binary, which perfectly aligns with the Unikernel philosophy.

Step 1: Write a Simple Web Server

Create a main.go file with the following content:

package main

import (
	"fmt"
	"net/http"
)

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Welcome to Unikernel running on NanoVMs!")
	})

	fmt.Println("Server is running on port 8080...")
	http.ListenAndServe(":8080", nil)
}

Step 2: Compile the Application

We will compile the Go code into a binary for Linux:

GOOS=linux go build main.go

Step 3: Run the Application as a Unikernel

Forget the Dockerfile. With NanoVMs, you only need the ops command. This tool automatically analyzes libraries, packages the image, and launches a tiny KVM virtual machine:

ops run main -p 8080

This command will automatically perform 4 steps:

  1. Create a disk image containing the main binary.
  2. Download the NanoVMs nanos kernel.
  3. Activate QEMU/KVM to boot the image.
  4. Expose port 8080 from the VM to the outside.

Try accessing http://localhost:8080. You’ll see the welcome message immediately. The interesting part is that the application is running inside an independent VM, yet the resource consumption is incredibly low.

Advanced Configuration

In practice, you’ll need to pass environment variables or config files. NanoVMs supports this via a config.json file.

Example configuration file:

{
  "Args": ["main"],
  "Env": {
    "APP_ENV": "production",
    "DB_HOST": "10.0.0.5"
  },
  "Files": ["config.yaml"],
  "MapDirs": {"./static": "/var/www/static"}
}

To run with this configuration, use the command:

ops run main -c config.json

Real-world Comparison: NanoVMs vs. Docker

I performed a quick test on my home Proxmox cluster with the same API application written in Go:

  • Docker: 150MB image (Ubuntu base), uses 20MB RAM at idle. Startup time takes about 1-2 seconds.
  • NanoVMs: Only a 12MB image. RAM consumption is a mere 8MB. Boot time from command execution to receiving requests is only about 300ms.

But security is the real value proposition. With containers, if a port is exposed, an attacker could use ls or cat /etc/passwd for reconnaissance. With NanoVMs, there is no ls command, no cat, and no package manager to download additional malware. The application is the only thing that exists and operates.

Conclusion: When Should You Use NanoVMs?

To be fair, NanoVMs cannot completely replace Docker yet. If your application is a complex monolith that needs to call many subprocesses or depends heavily on specific Linux features, Unikernels will be a significant challenge.

However, NanoVMs truly shines in these cases:

  • Microservices: Small, independent services requiring maximum performance.
  • Serverless Infrastructure: Leveraging lightning-fast boot times to reduce costs.
  • Edge Computing: Locations where hardware resources are extremely limited.

Approaching NanoVMs might feel a bit strange at first. But with the benefits in security and speed, it’s definitely a technology you should experiment with in your lab today.

Share: