Architecture
Table of contents
Overview
Uni is structured as a client–daemon system, the same model used by Docker:
┌─────────────────────────────────────────────────────────┐
│ uni (CLI — short-lived process) │
│ │
│ build · run · ps · logs · stop · rm · inspect · exec · cp │
│ compose up · compose down · compose ps · compose logs │
│ volume create · volume ls · volume rm · volume inspect │
│ pkg list · pkg search · pkg get · pkg remove │
│ kernel check · kernel update · kernel list · kernel use│
│ upgrade · upgrade check · upgrade list │
└──────────────────────────┬──────────────────────────────┘
│
│ JSON-RPC 2.0 over Unix domain socket
│ /var/run/unid.sock
│
┌──────────────────────────▼──────────────────────────────┐
│ unid (daemon — long-running background process) │
│ │
│ ┌──────────────────┐ ┌──────────────────────────────┐ │
│ │ VM Manager │ │ Image Registry (HTTP) │ │
│ │ │ │ │ │
│ │ QEMUManager │ │ GET /v2/images │ │
│ │ ┌────────────┐ │ │ POST /v2/images │ │
│ │ │ VM #1 │ │ │ GET /v2/images/{ref} │ │
│ │ │ qemu-sys.. │ │ │ GET /v2/images/{ref}/disk │ │
│ │ └────────────┘ │ │ DELETE /v2/images/{ref} │ │
│ │ ┌────────────┐ │ └──────────────────────────────┘ │
│ │ │ VM #2 │ │ │
│ │ │ qemu-sys.. │ │ ┌──────────────────────────────┐ │
│ │ └────────────┘ │ │ Image Store │ │
│ └──────────────────┘ │ ~/.uni/images/ │ │
│ │ <sha256>/manifest.json │ │
│ │ <sha256>/disk.img │ │
│ │ refs.json │ │
│ └──────────────────────────────┘ │
└──────────────────────────┬──────────────────────────────┘
│ spawns
┌──────────────────────────▼──────────────────────────────┐
│ QEMU processes (one per running VM) │
│ │
│ qemu-system-x86_64 │
│ -m 256M │
│ -drive file=disk.img,format=raw,if=virtio │
│ -nographic -serial stdio -no-reboot │
└──────────────────────────┬──────────────────────────────┘
│ boots
┌──────────────────────────▼──────────────────────────────┐
│ Nanos Kernel (C + ASM fork) │
│ Loads and runs the static ELF application │
└─────────────────────────────────────────────────────────┘
Components
uni CLI (cmd/uni/)
The command-line interface. It is a thin client — it does no VM management itself. Every command translates directly into a JSON-RPC call to unid.
- One
.gofile per subcommand (run.go,ps.go,stop.go, …) - Zero business logic — just argument parsing and formatting
- Cobra framework for command routing
unid daemon (cmd/unid/)
The long-running background process that owns everything:
- Listens on a Unix domain socket (JSON-RPC 2.0)
- Manages the VM registry (in-memory
Store) - Spawns and monitors QEMU processes
- Optionally serves the HTTP image registry
VM Manager (internal/vm/)
Manages the lifecycle of individual VMs:
State machine:
created → starting → running → stopping → stopped
Every transition is atomic (protected by sync.RWMutex) and logged with slog.
Key types:
VM— represents one virtual machine (ID, config, state, timestamps, log buffer)QEMUManager— implements theManagerinterface by spawningqemu-system-x86_64Store— thread-safe in-memory registry of all known VMs
QEMU command built per VM:
qemu-system-x86_64 \
-m 256M \
-drive file=/path/to/disk.img,format=raw,if=virtio \
-nographic \
-serial stdio \
-no-reboot \
-net none
Serial console output (stdout + stderr from QEMU) is captured into a thread-safe buffer, accessible via uni logs. When a VM is started with --attach, the output is simultaneously streamed through an io.Pipe so the CLI can read it in real-time via the VM.Attach RPC method.
Kernel Tools Cache (internal/tools/)
The kernel artifacts (kernel.img, boot.img, mkfs, dump) are downloaded from GitHub releases and cached in ~/.uni/tools/. They are versioned independently from the CLI using semver (kernel/VERSION in the repo).
Download flow:
uni buildcallstools.ResolveMkfs()uni cpcallstools.ResolveDump()- If tools are absent →
DownloadVersion("latest")fetches all artifacts + saveskernel-version.txt - If tools are present → checks remote version via GitHub API; if newer, prompts
[y/N]before replacing
Versioned releases: each kernel release is tagged kernel-vX.Y.Z on GitHub and is immutable. A rolling latest release always points to the most recent build. uni kernel use <v> downloads from the specific versioned tag.
Image System (internal/image/)
Content-addressable store — images are stored by their SHA256 digest:
~/.uni/images/
refs.json ← maps "name:tag" → "sha256hex"
abc123def456.../
manifest.json ← image metadata
disk.img ← raw VM disk
Manifest format (manifest.json):
{
"schemaVersion": 1,
"name": "hello",
"tag": "latest",
"created": "2026-04-19T10:00:00Z",
"config": {
"memory": "256M",
"cpus": 1
},
"diskDigest": "sha256:abc123...",
"diskSize": 12582912
}
Builder pipeline (image.Builder):
- Validate ELF magic bytes on the binary
- Run
mkfs(Nanos tool) to create a raw disk image containing the binary - Compute SHA256 of the disk
- Write manifest + disk to the store
API (internal/api/)
JSON-RPC 2.0 over a Unix domain socket.
Methods:
| Method | Description |
|---|---|
VM.Run | Create + start a VM |
VM.Stop | Graceful or forced stop |
VM.Kill | Immediate SIGKILL |
VM.Signal | Send arbitrary signal |
VM.Remove | Delete a stopped VM |
VM.List | List all VMs |
VM.Get | Get one VM by ID |
VM.Logs | Get captured serial output (snapshot) |
VM.Attach | Stream serial console output in real-time |
VM.Inspect | Full VM details |
Compose (internal/compose/)
Parses compose YAML files and resolves startup order:
- Parser — validates schema (version, service images, dependency refs, network refs)
- Graph — Kahn’s topological sort algorithm with cycle detection
Package System (internal/package/)
Manages pre-packaged files that can be included in images at build time:
- Store — local cache at
~/.uni/packages/<name>/<version>/holding:files.tar.gz— the downloaded package archivefiles/— extracted contents of the archivemeta.json— package metadata (name, version, SHA256, etc.)
- FetchIndex — retrieves the remote package index listing available packages and versions
- Download — fetches the package archive from its URL and stores it locally (with size verification)
- Extract — decompresses
files.tar.gzinto thefiles/subdirectory - ExtractedFiles — lists all regular files inside the extracted package
- Search — queries the remote index by name, description, or runtime
- Get — downloads a package (optionally a specific version) to the local store
- Remove — deletes a specific version; RemoveAll — deletes all versions of a package
Packages are included at build time via uni build --pkg <name>[:<version>]. The build pipeline:
resolvePackages()fetches the remote index and resolves each--pkgreference- Downloads the archive (
files.tar.gz) if not already cached - Extracts the archive into
files/if not already extracted - Collects all individual file paths from
files/viaExtractedFiles() - Passes the file list to
buildManifest()which includes each file in the Nanos manifest
Environment Variable Injection
Environment variables passed via uni run -e KEY=VALUE reach the guest through QEMU’s fw_cfg device — no disk rebuild required.
Flow:
uni run -e KEY=VAL→ daemon builds-fw_cfg name=opt/uni/env,string=KEY=VAL\n- QEMU exposes this as a named file on the fw_cfg device (I/O ports
0x510/0x511) - At boot,
env_inject_from_fw_cfg()in the kernel readsopt/uni/envand merges entries into the process environment tuple beforeexec_elfbuilds the user-space stack
This is x86-64 only; the function compiles to a no-op stub on aarch64.
Network Configuration Injection
Static IP configuration passed via uni run --ip reaches the guest through QEMU’s fw_cfg device, the same mechanism used for environment variables.
Flow:
uni run --ip 10.0.0.2 --network tap0→ daemon builds-fw_cfg name=opt/uni/network,string=10.0.0.2/24,10.0.0.1- QEMU exposes this as a named file on the fw_cfg device (I/O ports
0x510/0x511) - At boot,
net_inject_from_fw_cfg()in the kernel readsopt/uni/network, parses the IP/CIDR and gateway, and injects them into the root tuple init_network_iface()picks up the injected values to configure the first ethernet interface with a static IP instead of DHCP
The format is IP/CIDR,GATEWAY (e.g. 10.0.0.2/24,10.0.0.1). This is x86-64 only.
Image Registry
When started with --registry-addr :5000, unid serves an HTTP registry:
GET /v2/images list all images
GET /v2/images/{ref} get manifest (name:tag or sha256:hex)
GET /v2/images/{ref}/disk download raw disk image
POST /v2/images push image (multipart: manifest + disk)
DELETE /v2/images/{ref} remove image
This is intentionally simple — not OCI-compliant, designed for internal use between uni instances on a local network.
File Copy (uni cp)
uni cp copies files to and from stopped VM disk images using the dump and mkfs tools from the Nanos kernel toolchain. The tools read and write the TFS (Tiny File System) filesystem directly on the raw disk image.
Copy FROM a VM — the dump tool extracts the entire filesystem to a temporary directory, then the requested file is copied to the destination.
Copy TO a VM — the dump tool extracts the filesystem, the new file is injected, then mkfs rebuilds the disk image with the updated content.
Download flow:
uni cpcallstools.ResolveDump()(andtools.ResolveMkfs()for copy-to-VM)- If tools are absent from
~/.uni/tools/→downloadArtifact()fetches them from the latest kernel release - For copy-from: extract filesystem, copy file to host
- For copy-to: extract filesystem, copy file in, rebuild disk with
mkfs
This requires the VM to be in stopped state because the disk image must not be in use by a running QEMU process.
Networking
Each VM can use one of two networking modes:
SLIRP user-mode (default for -p): QEMU’s built-in user-mode networking with port forwarding via hostfwd rules. Works on any platform without root access. Does not support inbound ICMP (ping).
TAP + bridge: A TAP interface is created and bridged on the Linux host, giving the VM full network access including its own IP address. Requires Linux and elevated permissions. When port mappings (-p) are used together with --network, iptables DNAT rules are automatically configured so that traffic arriving at the host is forwarded to the guest’s static IP. The bridge is created via internal/network/bridge_linux.go, the TAP is attached, and iptables rules (with interface filtering via -i tapName) are applied for port forwarding. When --ip is specified, the guest-side static IP is configured via fw_cfg (opt/uni/network) — no DHCP required.
TAP networking requires Linux and elevated permissions. It is not available on Windows. See internal/network/tap.go (Linux-only build tag).
Security Model
unidruns as root (or a privileged user) to spawn QEMU and manage TAP interfaces- The Unix socket is the trust boundary — only processes that can access the socket file can manage VMs
- Each VM runs in full KVM hardware isolation — a compromised unikernel cannot escape to the host or other VMs
- No shell, no SSH, no dynamic linking inside the unikernel — attack surface is minimal by design