Genesis a CLI project written in Python. It can build Ubuntu images from scratch.

The tool is named genesis (because you start from nothing). And is available as a python package: (it’s also packaged as a deb in a PPA.

A basic example

We are going to create a very minimal image of Ubuntu 23.04 (Lunar Lobster) and try to boot from it using qemu.

Creating a base image

First you want to start by bootstrapping a basic filesystem:

sudo genesis debootstrap \
    --series lunar \
    --mirror '' \
    --output chroot-lunar

Then, with this filesystem, you can create a disk-image:

sudo genesis create-disk \
    --rootfs-dir ./chroot-lunar \
    --disk-image lunar.img

Once this is done, you need to update your system (debootstrap only uses the release pocket). While doing this stage, you can install some extra packages. Here we are going to build a very minimalist image of Ubuntu using only

sudo genesis update-system \
    --disk-image lunar.img \
    --mirror '' \
    --series lunar \
    --extra-package openssh-server --extra-package ca-certificates --extra-package linux-kvm

We still need to install a boot loader and this operation requires its own command:

sudo genesis install-grub --disk-image lunar.img

Final customizations

The image is almost ready but we can (and here we need) customize it by adding extra files directly on the filesystem. This is done with the copy-files command.

Configuring networking

Because we did not install cloud-init in our image, we need to pre-configure it with everything it needs. Here we assume that this image will be run with qemu and a virtual network card attached. We configure netplan accordingly:

sudo genesis copy-files \
    --disk-image lunar.img \
    --file $PWD/netplan.yaml:/etc/netplan/image-default.yaml

with netplan.yaml being the following:

    version: 2
            dhcp4: true
                driver: virtio_net
            set-name: eth0

Configuring the sources

We want to define the Debian packages source. For now there is just a default source file pointing to in the image. Maybe, since I live in France, I want my image to be configured with a local mirror:

deb lunar main universe restricted
deb lunar-updates main universe restricted
deb lunar-security main universe restricted

This is what my sources.list would look like and I can now install it on the live image:

sudo genesis copy-files \
    --disk-image lunar.img \
    --file $PWD/sources.list:/etc/apt/sources.list

User config

Finally, I need to configure a user. Here I create a user with create-user and I copy my public ssh key in .ssh/authorized_keys directory for this user.

sudo genesis create-user \
    --disk-image lunar.img \
    --username ubuntu --sudo
sudo genesis copy-files \
    --disk-image lunar.img \
    --file $HOME/.ssh/ --mod 600 --owner ubuntu

Note that if .ssh does not exist under /home/ubuntu, it will be automatically created by copy-files.

Running the image

Now let’s try to run this image that we have just created. For that we need a bit of qemu black magic:

qemu-system-x86_64 \
    -cpu host -machine type=q35,accel=kvm -m 2048 \
    -nographic -snapshot \
    -netdev id=net00,type=user,hostfwd=tcp::2222-:22 \
    -device virtio-net-pci,netdev=net00 \
    -drive if=virtio,format=raw,file=./lunar.img \
    -drive if=pflash,format=raw,file=/usr/share/OVMF/OVMF_CODE.fd,readonly=on \
    -drive if=pflash,format=raw,file=/usr/share/OVMF/tmp/OVMF_VARS.fd,readonly=on

If you don’t understand what is going on, this is not very important. But, assuming you have all the right dependencies installed, this should start a virtual machine and boot on the disk we’ve just created.

Then you should be able to ssh (by opening another terminal):

ssh [email protected] -p 2222

And we can check the boot time:

ubuntu@ubuntu:~$ sudo systemd-analyze critical-chain
The time when unit became active or started is printed after the "@" character.
The time the unit took to start is printed after the "+" character. @740ms
└─ @739ms
  └─systemd-logind.service @681ms +43ms
    └─ @663ms
      └─ @663ms
        └─ssh.socket @662ms
          └─ @636ms
            └─systemd-resolved.service @548ms +86ms
              └─systemd-tmpfiles-setup.service @536ms +9ms
                └─ @528ms
                  └─boot-efi.mount @506ms +21ms
                    └─dev-vda15.device @476ms

Because the image is so minimal, the system boots in less than a second.

Is it usable for building production-ready images of Ubuntu?