Skip to content

Creating VMs

This guide covers how to create new VM types in the Ghaf Framework, from simple extensions to entirely new system VMs.

VMs in Ghaf use a three-layer composition model:

  1. Base Module - Core VM configuration (modules/microvm/sysvms/*-base.nix)
  2. Profile - Platform-specific VM setup (modules/profiles/*.nix)
  3. Target - Final customization (targets/*/flake-module.nix)

Create modules/microvm/sysvms/myvm-base.nix:

Apache-2.0
# SPDX-FileCopyrightText: 2022-2026 TII (SSRC) and the Ghaf contributors
#
# My VM base configuration
#
{ config, lib, pkgs, globalConfig, hostConfig, inputs, ... }:
{
_file = ./myvm-base.nix;
imports = [
# Common VM modules
inputs.self.nixosModules.vm-config
inputs.self.nixosModules.givc
];
# VM identification
networking.hostName = hostConfig.vmName;
system.name = hostConfig.vmName;
# Inherit global settings
ghaf = {
profiles.debug.enable = globalConfig.debug.enable;
development = {
debug.tools.enable = globalConfig.development.debug.tools.enable;
ssh.daemon.enable = globalConfig.development.ssh.daemon.enable;
};
givc = {
enable = globalConfig.givc.enable;
inherit (globalConfig.givc) debug;
};
};
# Time zone from global config
time.timeZone = lib.mkDefault globalConfig.platform.timeZone;
# VM-specific configuration
microvm = {
hypervisor = "qemu";
vcpu = 2;
mem = 1024;
# Shared directories
shares = [
{
tag = "ro-store";
source = "/nix/store";
mountPoint = "/nix/.ro-store";
proto = "virtiofs";
}
];
# Network interface
interfaces = [{
type = "tap";
id = "vm-myvm";
mac = hostConfig.networking.thisVm.mac;
}];
};
# Networking
networking.interfaces.ethint0.ipv4.addresses = [{
address = hostConfig.networking.thisVm.ipv4;
prefixLength = 24;
}];
# Your VM's services
services.myService.enable = true;
# Packages
environment.systemPackages = with pkgs; [
htop
vim
];
}

Create modules/microvm/sysvms/myvm.nix:

Apache-2.0
# SPDX-FileCopyrightText: 2022-2026 TII (SSRC) and the Ghaf contributors
{ config, lib, ... }:
let
cfg = config.ghaf.virtualization.microvm.myvm;
in
{
_file = ./myvm.nix;
options.ghaf.virtualization.microvm.myvm = {
enable = lib.mkEnableOption "My VM";
evaluatedConfig = lib.mkOption {
type = lib.types.nullOr lib.types.unspecified;
default = null;
description = ''
Evaluated NixOS configuration for my VM.
Set by the profile using lib.nixosSystem.
'';
};
};
config = lib.mkIf cfg.enable {
assertions = [{
assertion = cfg.evaluatedConfig != null;
message = "ghaf.virtualization.microvm.myvm.evaluatedConfig must be set";
}];
microvm.vms.my-vm = {
evaluatedConfig = cfg.evaluatedConfig;
};
};
}

Add to modules/microvm/flake-module.nix:

{
flake.nixosModules = {
# ... existing modules ...
myvm = ./sysvms/myvm.nix;
myvm-base = ./sysvms/myvm-base.nix;
};
}

Update modules/profiles/laptop-x86.nix (or create a new profile):

{ config, lib, pkgs, inputs, ... }:
let
globalConfig = config.ghaf.global-config;
# Create the VM base with proper specialArgs
myvmBase = lib.nixosSystem {
specialArgs = lib.ghaf.vm.mkSpecialArgs {
inherit lib inputs;
globalConfig = globalConfig;
hostConfig = lib.ghaf.vm.mkHostConfig {
inherit config;
vmName = "my-vm";
};
};
modules = [
inputs.self.nixosModules.myvm-base
# Add target-specific modules here
];
};
# Apply vmConfig (mem, vcpu, extraModules from hardware.definition and vmConfig)
myvmFinal = myvmBase.extendModules {
modules = lib.ghaf.vm.applyVmConfig {
inherit config;
vmName = "myvm";
};
};
in
{
config = lib.mkMerge [
# ... existing config ...
# Wire up my VM
{
ghaf.virtualization.microvm.myvm.evaluatedConfig = myvmFinal;
}
];
}

Update host networking to include your VM’s address. In your hardware definition or profile:

ghaf.common.extraNetworking.hosts.my-vm = {
name = "my-vm";
ipv4 = "192.168.100.10";
mac = "02:00:00:00:00:10";
};

Application VMs use a template pattern for multiple instances.

The mkAppVm function creates application VMs. All values (name, ramMb, borderColor, applications, vtpm) are defined in the mkAppVm call and stored in evaluatedConfig.config.ghaf.appvm.vmDef. Host-level options automatically read from there.

{ config, lib, ... }:
let
mkAppVm = config.ghaf.profiles.laptop-x86.mkAppVm;
in
{
ghaf.virtualization.microvm.appvm.vms.my-app = mkAppVm {
name = "my-app";
applications = [{
name = "My Application";
description = "Description of my app";
packages = [ pkgs.my-app ];
icon = "my-app-icon";
command = "my-app --flag";
}];
extraModules = [
# Additional configuration
{ services.foo.enable = true; }
];
};
}

Use the extensions option to add modules to an existing app VM without modifying its base definition. Extensions are applied via NixOS extendModules:

{ pkgs, ... }:
{
# Add an app to the chrome VM without modifying its base
ghaf.virtualization.microvm.appvm.vms.chrome.extensions = [
({ pkgs, ... }: {
ghaf.appvm.applications = [{
name = "Getting Started";
description = "Introduction guide";
packages = [ pkgs.ghaf-intro ];
command = "ghaf-intro";
}];
})
];
}
OptionTypeDescription
enableboolEnable this VM
evaluatedConfigNixOS systemBase config from mkAppVm
extensionslist of modulesAdditional modules applied via extendModules
extraNetworkingattrsetHost-side networking options
usbPassthroughlistUSB passthrough rules (host-side)
bootPriorityenumBoot priority: “low”, “medium”, “high”

Values like name, ramMb, borderColor, applications, and vtpm are all derived from evaluatedConfig.config.ghaf.appvm.vmDef and should be set in the mkAppVm call, not at the host level.

Each application in the applications list:

{
name = "Firefox";
description = "Web browser";
packages = [ pkgs.firefox ];
icon = "firefox";
command = "firefox";
args = [ "--new-window" ];
}

Extend a VM base without modifying it:

{ config, ... }:
let
baseGuivm = config.ghaf.profiles.laptop-x86.guivmBase;
extendedGuivm = baseGuivm.extendModules {
modules = [
# Add your customizations
({ config, ... }: {
services.myService.enable = true;
environment.systemPackages = [ pkgs.my-tool ];
})
];
};
in
{
# Pass the full system result, not .config
ghaf.virtualization.microvm.guivm.evaluatedConfig = extendedGuivm;
}

For features that might be reused, create a feature module:

modules/microvm/guivm-features/my-feature.nix
{ config, lib, globalConfig, ... }:
let
enabled = lib.ghaf.features.isEnabledFor globalConfig "myFeature" "gui-vm";
in
{
_file = ./my-feature.nix;
config = lib.mkIf enabled {
services.myFeature.enable = true;
};
}

Then include it in the base or via extendModules.


For VMs that need direct hardware access:

# In hardware definition
hardware.definition.myvm.pciPassthrough = [
{ path = "0000:00:1f.3"; } # Audio device
];
# In VM base
microvm.devices = lib.optionals (hostConfig.pciPassthrough or []) (
map (dev: {
bus = "pci";
inherit (dev) path;
}) hostConfig.pciPassthrough
);
microvm.devices = [
{
bus = "usb";
vendorId = "1234";
productId = "5678";
}
];

The vmConfig parameter in mkGhafConfiguration allows per-target resource customization:

# In target flake-module.nix
mkGhafConfiguration {
# ... other params ...
vmConfig = {
guivm = { mem = 4096; vcpu = 4; };
netvm = { mem = 512; vcpu = 1; };
myvm = { mem = 2048; vcpu = 2; };
};
}

This maps to ghaf.virtualization.vmConfig and is applied via lib.ghaf.vm.applyVmConfig, which generates a resource module setting microvm.mem and microvm.vcpu.


Terminal window
# Evaluate VM config
nix eval .#nixosConfigurations.target-name.config.microvm.vms.my-vm.config.networking.hostName
# Build VM system
nix build .#nixosConfigurations.target-name.config.microvm.vms.my-vm.config.system.build.toplevel
Terminal window
# Show all VM options
nix eval .#nixosConfigurations.target-name.config.ghaf.virtualization.microvm --json | jq

VMs can be accessed via serial console when debugging:

microvm.console = "console";
boot.kernelParams = [ "console=ttyS0" ];

  • Create base module (*-base.nix) with _file declaration
  • Create VM module (*.nix) with enable option and evaluatedConfig
  • Export modules in flake-module.nix
  • Add VM base creation to profile
  • Configure networking (IP, MAC)
  • Add to host’s microvm.vms
  • Test build and boot
  • Document the VM’s purpose