This project aims at creating a minimal NetBSD 🚩 based BSD UNIX virtual machine that's able to boot and start a service in a couple milliseconds.
Previous NetBSD installation is not required, using the provided tools the microvm can be
created and started from any NetBSD, GNU/Linux, macOS system and probably more.
PVH boot and various optimizations enable NetBSD/amd64 and NetBSD/i386 to directly boot from a PVH capable VMM (QEMU or Firecracker) in about 10 milliseconds on 2025 mid-end x86 CPUs.
As of June 2025, most of these features are integrated in NetBSD's current kernel, and NetBSD 11 releases those still pending are available in my NetBSD development branch.
Pre-built 64 bits kernel at https://smolbsd.org/assets/netbsd-SMOL and a 32 bits kernel at https://smolbsd.org/assets/netbsd-SMOL386
aarch64 netbsd-GENERIC64 kernels are able to boot directly to the kernel with no modification
In any case, the bmake kernfetch will take care of downloading the correct kernel.
- A GNU/Linux, NetBSD or macOS operating system (might work on more systems, but not CPU accelerated)
- The following tools installed
curlgitbmakeif running on Linux or macOS,makeon NetBSDqemu-system-x86_64,qemu-system-i386orqemu-system-aarch64sudoordoasnm(not used on macOS)bsdtaron Linux (install withlibarchive-toolson Debian and derivatives,libarchiveon Arch)
- A x86 VT-capable, or ARM64 CPU is recommended
mkimg.shcreates a root filesystem image
$ ./mkimg.sh -h
Usage: mkimg.sh [-s service] [-m megabytes] [-i image] [-x set]
[-k kernel] [-o] [-c URL]
Create a root image
-s service service name, default "rescue"
-r rootdir hand crafted root directory to use
-m megabytes image size in megabytes, default 10
-i image image name, default rescue-[arch].img
-x sets list of NetBSD sets, default rescue.tgz
-k kernel kernel to copy in the image
-c URL URL to a script to execute as finalizer
-o read-only root filesystem
-u non-colorful outputstartnb.shstarts a NetBSD virtual machine usingqemu-system-x86_64orqemu-system-aarch64
$ ./startnb.sh -h
Usage: startnb.sh -f conffile | -k kernel -i image [-c CPUs] [-m memory]
[-a kernel parameters] [-r root disk] [-h drive2] [-p port]
[-t tcp serial port] [-w path] [-x qemu extra args]
[-b] [-n] [-s] [-d] [-v] [-u]
Boot a microvm
-f conffile vm config file
-k kernel kernel to boot on
-i image image to use as root filesystem
-c cores number of CPUs
-m memory memory in MB
-a parameters append kernel parameters
-r root disk root disk to boot on
-l drive2 second drive to pass to image
-t serial port TCP serial port
-n num sockets number of VirtIO console socket
-p ports [tcp|udp]:[hostaddr]:hostport-[guestaddr]:guestport
-w path host path to share with guest (9p)
-x arguments extra qemu arguments
-b bridge mode
-s don't lock image file
-d daemonize
-v verbose
-u non-colorful output
-h this helsetscontains NetBSD "sets" by architecture, i.e.amd64/base.tgz,evbarm-aarch64/rescue.tgz...pkgsholds optional packages to add to a microvm, it has the same format assets.
A service is the base unit of a smolBSD microvm, it holds the necesary pieces to build a BSD system from scratch.
servicestructure:
service
├── base
│ ├── etc
│ │ └── rc
│ ├── postinst
│ │ └── dostuff.sh
│ ├── options.mk # Service-specific defaults
│ └── own.mk # User-specific overrides (not in git)
├── common
│ └── basicrc
└── rescue
└── etc
└── rcA microvm is seen as a "service", for each one:
- There COULD be a
postinst/anything.shwhich will be executed bymkimg.shat the end of root basic filesystem preparation. This is executed by the build host at build time - If standard NetBSD
initis used, there MUST be anetc/rcfile, which defines what is started at vm's boot. This is executed by the microvm. - Image specifics COULD be added in
make(1)format inoptions.mk, i.e.
$ cat service/nbakery/options.mk
# size of resulting inage in megabytes
IMGSIZE=1024
# as of 202510, there's no NetBSD 11 packages for !amd64
.if defined(ARCH) && ${ARCH} != "amd64"
PKGVERS=10.1
.endif- User-specific overrides COULD be added in
own.mkfor personal development settings (not committed to repository)
In the service directory, common/ contains scripts that will be bundled in the
/etc/include directory of the microvm, this would be a perfect place to have something like:
$ cat common/basicrc
export HOME=/
export PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/pkg/bin:/usr/pkg/sbin
umask 022
mount -a
if ifconfig vioif0 >/dev/null 2>&1; then
# default qemu addresses and routing
ifconfig vioif0 10.0.2.15/24
route add default 10.0.2.2
echo "nameserver 10.0.2.3" > /etc/resolv.conf
fi
ifconfig lo0 127.0.0.1 up
export TERM=dumbAnd then add this to your rc:
. /etc/include/basicrcYou can build a system using 2 methods:
- using the
baseMakefiletarget, this will build directly on the host, it will useext2as the filesystem if building on a Linux host, andffsif building on a NetBSD host - using the recommended
buildMakefiletarget, this will fire up a NetBSD builder microvm that will use its own root filesystem and will format the image usingffs.
Warning
When using the base target, bmake(1) will directly use your host to build images, and as postinst operations are run as root in the build host: only use relative paths in order not to impair your host's filesystem.
Again, it is highly recommended to use the build target for any other service than the builder image.
For the microvm to start instantly, you will need a kernel that is capable of "direct booting" with the qemu -kernel flag.
For amd64/PVH and i386/PVH
Download the SMOL kernel
$ bmake kernfetchFor aarch64
Download a regular netbsd-GENERIC64.img kernel
$ bmake ARCH=evbarm-aarch64 kernfetchIn the following examples, replace bmake by make if you are using NetBSD as the host.
- Either create the builder image if you are running GNU/Linux or NetBSD
$ bmake buildimg- Or by simply fetch it if you are running systems that do not support
ext2orffssuch as macOS
$ bmake fetchimgBoth methods will create an images/build-<arch>.img disk image that you'll be able to use to build services.
To create a service image using the builder, execute the following:
$ bmake SERVICE=nitro buildThis will spawn a microvm running the build image, and will in turn build the requested service.
Note
You can use the ARCH variable to specify an architecture to build your image for, default is amd64.
$ bmake SERVICE=rescue buildWill create a rescue-amd64.img file for use with an amd64 kernel.
$ bmake SERVICE=rescue MOUNTRO=y buildWill also create a rescue-amd64.img file but with read-only root filesystem so the VM can be stopped without graceful shutdow. Note this is the default for rescue as set in service/rescue/options.mk
$ bmake SERVICE=rescue ARCH=i386 buildWill create a rescue-i386.img file for use with an i386 kernel.
$ bmake SERVICE=rescue ARCH=evbarm-aarch64 buildWill create a rescue-evbarm-aarch64.img file for use with an aarch64 kernel.
Start the microvm
$ ./startnb.sh -k kernels/netbsd-SMOL -i images/rescue-amd64.img$ bmake SERVICE=base build
$ ./startnb.sh -k kernels/netbsd-SMOL -i images/base-amd64.imgServices are build on top of the base image, this can be overriden with the BASE make(1) variable.
Service name is specified with the SERVICE make(1) variable.
$ make ARCH=evbarm-aarch64 SERVICE=bozohttpd build
$ ./startnb.sh -k kernels/netbsd-GENERIC64.img -i images/bozohttpd-evbarm-aarch64.img -p ::8080-:80
[ 1.0000000] NetBSD/evbarm (fdt) booting ...
[ 1.0000000] NetBSD 10.99.11 (GENERIC64) Notice: this software is protected by copyright
[ 1.0000000] Detecting hardware...[ 1.0000040] entropy: ready
[ 1.0000040] done.
Created tmpfs /dev (1359872 byte, 2624 inodes)
add net default: gateway 10.0.2.2
started in daemon mode as `' port `http' root `/var/www'
got request ``HEAD / HTTP/1.1'' from host 10.0.2.2 to port 80Try it from the host
$ curl -I localhost:8080
HTTP/1.1 200 OK
Date: Wed, 10 Jul 2024 05:25:04 GMT
Server: bozohttpd/20220517
Accept-Ranges: bytes
Last-Modified: Wed, 10 Jul 2024 05:24:51 GMT
Content-Type: text/html
Content-Length: 30
Connection: close$ bmake SERVICE=mport MOUNTRO=y build
$ ./startnb.sh -n 1 -i images/mport-amd64.img
host socket 1: s885f756bp1.sockOn the guest, the corresponding socket is /dev/ttyVI0<port number>, here /dev/ttyVI01
guest$ echo "hello there!" >/dev/ttyVI01host$ socat ./s885f756bp1.sock -
hello there!$ bmake live # or make ARCH=evbarm-aarch64 live
$ ./startnb.sh -f etc/live.confThis will fetch a directly bootable kernel and a NetBSD "live", ready-to-use, disk image. Login with root and no password. To extend the size of the image to 4 more GB, simply do:
$ dd if=/dev/zero bs=1M count=4000 >> NetBSD-amd64-live.imgAnd reboot.
The following Makefile variables change mkimg.sh behavior:
ADDPKGSwill fetch and untar the packages paths listed in the variable, this is done inpostinststage, on the build host, wherepkginmight not be availableADDSETSwill add the sets paths listed in the variableMINIMIZEif set toy, will invoke sailor in order to minimize the produced image
The following environment variables change startnb.sh behavior:
QEMUwill use customqemuinstead of the one in user's$PATH
A simple virtual machine manager is available in the app/ directory, it is a
python/Flask application and needs the following requirements:
Flaskpsutil
Start it in the app/ directory like this: python3 app.py and a GUI like
the following should be available at http://localhost:5000:
