Create your own Ubuntu Core 20 image

1. Overview

Building a bespoke image for a supported platform enables an Ubuntu Core device to be customised at the point of deployment. Customisation options include configuration for both hardware and software, specific kernels, and which snap packages to pre-install.

We are going to create an image of the latest Ubuntu Core release, UC20, first by generating our own authority keys, then making the snap store aware of them, then creating and signing a model assertion before building the image.

This document will walk you through all the steps to build an image for an x86 device, but the same instructions will work for other platforms.

What you’ll learn

  • Different fundamental snap notions on the board, like gadget, kernel and core snap
  • Assembling a kernel and gadget snaps
  • Creating and using your authority keys
  • Create a model assertion for your target device
  • Compose and build a custom image using the ubuntu-image command

What you’ll need

  • Ubuntu 20.04 LTS desktop. You can’t do this on an Ubuntu Core device directly as creating your image can take quite some disk spaces. A VM can work as well.
  • A Snap Store account to register your authority keys. See Create a developer account for details.
  • An SSH public key associated with your Snap Store account. See Adding SSH keys to your account.
  • Some very basic knowledge of command line use, know how to edit files.

How will you use this tutorial?

What is your current level of experience?

2. Getting started

To build a custom image, first use the snapcraft command to login to the Snap Store:

$ snapcraft login

Snapcraft can be installed with sudo snap install snapcraft --classic, see Snapcraft overview for further details, and visit Create a developer account if you don’t yet have an account. Make sure you have an SSH public key associated with your account.

Before creating a custom model assertion, you will need to retrieve your developer ID and generate a properly formatted timestamp. The snapcraft command can be used to retrieve your developer id:

$ snapcraft whoami
email: <email>
developer-id: bJzr2XzZ56Qv6Z51HIeziXvxtn1XItIq

You will need to use the following date command to output the correctly formatted timestamp for the model assertion, but not yet! The date must be more recent than any key you need to generate in a later step.

$ date -Iseconds --utc

3. Custom model assertion

The model assertion is a text-based document that contains the fundamental definition of a snap-based device. It describes what the system image includes and is signed by the brand account owning the device definition.

The model assertion contains:

  • Identification information, such as brand account id and model name.
  • Which essential snaps make up the device system, including the gadget snap, kernel snap and the boot base snap with the root filesystem.
  • Other required or optional snaps that implement the device functionality.
  • Additional options for the defined device, such as grade for a UC 20 device.

The model assertion is central to both the creation of the device image and the deployed device’s lifecycle, in particular it:

  • drives image creation via ubuntu-image. The resultant system seed includes the set of snaps and assertions specified in the model assertion.
  • allows first boot verification of the system seed before its snaps are turned into an installed system.
  • conveys model information, through registration from either the factory or in the field, which is used for cross-checking and registration affecting options, such as which account can issue a serial assertion for the device.

The following is a tweaked JSON-formatted custom model assertion based on

    "type": "model",
    "series": "16",
    "authority-id": "bJzr2XzZ56Qv6Z51HIeziXvxtn1XItIq",
    "brand-id": "bJzr2XzZ56Qv6Z51HIeziXvxtn1XItIq",
    "model": "ubuntu-core-20-amd64",
    "architecture": "amd64",
    "timestamp": "2021-01-25T10:40:41+00:00",
    "base": "core20",
    "grade": "signed",
    "snaps": [
            "name": "pc",
            "type": "gadget",
            "default-channel": "20/stable",
            "id": "UqFziVZDHLSyO3TqSWgNBoAdHbLI4dAH"
            "name": "pc-kernel",
            "type": "kernel",
            "default-channel": "20/stable",
            "id": "pYVQrBcKmBa0mZ4CCN7ExT6jH8rY1hza"
            "name": "core20",
            "type": "base",
            "default-channel": "latest/stable",
            "id": "DLqre5XGLbDqg9jPtiAhRRjDuPVa5X1q"
            "name": "snapd",
            "type": "snapd",
            "default-channel": "latest/stable",
            "id": "PMrrV4ml8uWuEUDBT8dSGnKUYbevVhc4"
            "name": "htop",
            "type": "app",
            "default-channel": "latest/stable",
            "id": "hJmReLmgXSUj4SF7WhyTVRV6IzUa4QUZ"

We’ve saved the above example to a file called my-model.json, and it contains the following modified properties:

  • base: provides the run-time environment. core20 is the newest base, built from Ubuntu 20.04 LTS. core18 is the current standard base and is built from Ubuntu 18.04 LTS. See Base snaps for more details.
  • authority-id, brand-id: defines the authority signing the assertion
    reference assertions are signed by canonical. Non-reference assertions are signed by their brand store. For a custom model assertion, this needs to be the developer ID.
  • timestamp: UTC formatted time and date
    used to denote the assertion’s creation time.
  • snaps: “pc”, “pc-kernel”, “core20” and “snapd” are all required for a functioning core20 device. In addition to those, the example shows the arbitrary addition of a snap called ‘htop’ from the Snap Store. This is purely for illustration purposes. The id field is from the output of snap info <snap-name> | grep snap-id to ensure the correct snap is references.

For a complete list of model assertion keywords, see Model assertion.

4. Signing a model assertion

The difference between building an image from a reference model assertion and
building from a modified model assertion is that the modified model assertion needs to be digitally signed. This is accomplished in four stages:

  1. create a key
  2. export/register the key
  3. sign the model assertion
  4. build the image

First, sign in to the Snap Store (snap login) and check whether there is already a published key available. You can list any published snaps with the snap keys command:

$ snap login
Login successful

$ snap keys
No keys registered, see `snapcraft create-key`

If you have no registered keys, create one as follows:

$ snap create-key my-key-name
Confirm passphrase: <passphrase>

$ snap keys
Name         SHA3-384
my-key-name  E-n0AOKPFjIyy4S_i9JxTT4tkuaZf7rP9D2ARCmBNXjlgTGDjL8euFSlb87U0NPl

With a key created, use the snapcraft command to upload and register it with the store:

$ snapcraft register-key
Registering key...

You can safely generate the date string, as described in the Getting started section, and insert it into the model definition (the key we just created needs to be older than any generated date).

A custom model assertion is signed by piping the assertion through the snap sign command with the key name as its sole argument:

$ cat my-model.json | snap sign -k my-key-name > my-model.model

The resulting my-model.model file contains the signed model assertion and can now be used to build the image.

5. Building the image

With a signed model assertion, the Ubuntu Core image can now be built just like a reference image, using the ubuntu-image command. First, install ubuntu-image if it isn’t already installed:

$ sudo snap install ubuntu-image --classic

You can now use ubuntu-image to build the image:

$ ubuntu-image snap my-model.model
ubuntu-image snap my-model.model
Fetching snapd
Fetching pc-kernel
Fetching core20
Fetching pc
Fetching htop

The output includes the img file itself, alongside _seed.manifest. The manifest file simply list the specific revision numbers for the snapd, htop, pc, pc-kernel and core20 snaps built into the image.

6. Testing the image

To test your UC20 image with QEMU (, first install the OVMF package (eg. sudo apt install ovmf ) then run the following command to boot a UC20 image (renamed pc.img) within a virtual machine:

$ sudo qemu-system-x86_64 -smp 2 -m 2048 \
 -net nic,model=virtio -net user,hostfwd=tcp::8022-:22 \
 -drive file=/usr/share/OVMF/OVMF_CODE.fd,if=pflash,format=raw,unit=0,readonly=on \
 -drive file=pc.img,cache=none,format=raw,id=disk1,if=none \
 -device virtio-blk-pci,drive=disk1,bootindex=1 -machine accel=kvm

Older OVMF packages can cause QEMU boot problems. Either build them manually, or consider extracting the files from a more recent package. OVMF for Ubuntu 20.04 LTS (Focal Fossa)]( is known to work.

After running through the Ubuntu Core network setup and entering your account details, you will be able to SSH to your new Ubuntu Core deployment:

$ ssh <username>@localhost -p 8022

You are now connected to the Ubuntu Core virtual machine, from where you can configure and install whatever apps you need.

From within a running session on a custom image, you can run the pre-installed
snaps, such as htop:

$ htop

Use snap list to see which snaps are installed:

snap list
Name       Version        Rev    Tracking       Publisher   Notes
core20     20201210       904    latest/stable  canonical✓  base
htop       3.0.5          2069   latest/stable  maxiberta   -
pc         20-0.4         115    20/stable      canonical✓  gadget
pc-kernel  5.4.0-64.72.1  708    20/stable      canonical✓  kernel
snapd      2.48.2         10707  latest/stable  canonical✓  snapd

The snap known model command will show the read-only custom model assertion used to build the image:

$ snap known model
type: model
authority-id: bJzr2XzZg6Qv6Z53HIeziXyxtn1XItIq
series: 16
brand-id: bJzr2XzZg6Qv6Z53HIeziXyxtn1XItIq
model: ubuntu-core-20-amd64
architecture: amd64
base: core20
grade: signed
    default-channel: 20/stable
    id: UqFziVZDHLSyO3TqSWgNBoAdHbLI4dAH
    name: pc
    type: gadget
    default-channel: 20/stable
    id: pYVQrBcKmBa0mZ4CCN7ExT6jH8rY1hza
    name: pc-kernel
    type: kernel
    default-channel: latest/stable
    id: DLqre5XGLbDqg9jPtiAhRRjDuPVa5X1q
    name: core20
    type: base
    default-channel: latest/stable
    id: PMrrV4ml8uWuEUDBT8dSGnKUYbevVhc4
    name: snapd
    type: snapd
    default-channel: latest/stable
    id: hJmReLmgXSUj4SF7WhyTVRV6IzUa4QUZ
    name: htop
    type: app
timestamp: 2021-01-25T10:40:41+00:00
sign-key-sha3-384: 9aZR3b1UX9kqiVVxzfUrK

7. Congratulations!

You now have your own device image file for your specific device. This image is easily flashable on any SDCard or eMMC and can be booted right away.

You should by now be familiar with the various snaps composing an Ubuntu image: Core snap, kernel, gadget. You know that snapd is using a model assertion to define all pieces composing an image and this is what is used to build the image via the ubuntu-image tool.

Finally, you know also that you can change those default snaps, and add more applications snaps as you require them. If you produce your own gadget or kernel snap, you can swap as well default ones and enable a new board that way.

Next steps