Your submission was sent successfully! Close

Build your own Ubuntu Core image

Ready to try out Ubuntu Core? This tutorial will guide you through the steps required to create your own Ubuntu Core image, with your own selection of snaps, and install it on a Raspberry Pi.

Here’s what you’ll need for the tutorial

  • a basic understanding of Linux and the command line
  • a Raspberry Pi 4 Model B (any model)
    • 4GB+ microSD card
    • keyboard and display (for setup only)
    • Ethernet or wireless network connectivity
  • Ubuntu 20.04 LTS (Focal Fossa) or later
  • 10GB of free storage space
  • a microSD card reader
  • access to the internet

The above requirements are specific to this tutorial. Other distributions can be used, and other platforms are supported. See Supported platforms for details.

Here’s an overview of the steps required:


1. Create an Ubuntu One account

The first step is to create an Ubuntu One account, if you don’t already have one. Ubuntu One is a single sign-on service (SSO) for Ubuntu and its affiliated projects, including snapcraft.io, the central resource for all snap-related publishing.

To create an account, head over to https://snapcraft.io/account and select the “I don’t have an Ubuntu One account” option to start this process. Fill out the form that appears. You will then receive an email asking you to verify your account. Click the verification link in the email and complete the reCAPTCHA challenge that follows. Finally, you should accept the Canonical Terms of Service.

Once you’ve logged in again to accept the terms, your Ubuntu One account is ready to use.

2. Retrieve your developer account ID

You next need to retrieve your developer account identifier. This is part of your Ubuntu One account and is used to link your account to any Ubuntu Core images you create.

The next steps need to be performed in an existing Ubuntu (20.04 or later) environment.

Your developer identifier can be retrieved with the snapcraft command, the tool that’s also used to build and publish snaps. It can be installed by running:

$ sudo snap install snapcraft --classic

With snapcraft installed, log in to the Store with your Ubuntu SSO account:

$ snapcraft login

You will be asked for your Ubuntu One email address and password, and encouraged to enable two-factor authentication (2FA) if you haven’t already done so:

$ snapcraft login
Enter your Ubuntu One e-mail address and password.
If you do not have an Ubuntu One account, you can create one at https://snapcraft.io/account
Email: <Ubuntu-SSO-email-address>
Password: <Ubuntu-SSO-password>

We strongly recommend enabling multi-factor authentication: https://help.ubuntu.com/community/SSO/FAQs/2FA

Login successful.

Following a successful login, the snapcraft whoami command displays your developer-id:

$ snapcraft whoami
email:        <Ubuntu-SSO-email-address>
developer-id: xSfWKGdLoQBoQx88

In the output above, the example developer-id is xSfWKGdLoQBoQx88 – we’ll use this ID for subsequent examples, but you should obviously use your own ID from now on.

3. Download a model assertion

At the heart of Ubuntu Core image creation is the model assertion. An assertion is a recipe that describes the components that comprise a complete image. An assertion is provided as JSON in a text file.

The model assertion contains:

  • identification information, such as the developer-id and model name.
  • which essential snaps make up the device system.
  • other required or optional snaps that implement the device functionality.

The quickest way to create a new model assertion is to edit one that already exists, and you can find the reference model assertions for every supported Ubuntu Core device in the snapcore/models GitHub repository.

For this project, we’re going to modify the 64-bit reference model assertion for the Raspberry Pi: ubuntu-core-20-pi-arm64.json.

Download and save the file locally with the following wget command. We’ve called it my-model.json:

$ wget -O my-model.json https://raw.githubusercontent.com/snapcore/models/master/ubuntu-core-20-pi-arm64.json

4. Edit the model assertion

We now need to edit my-model.json using a text editor:

$ nano my-model.json

The following fields in my-model.json need to be changed:

4.1 "authority-id" and "brand-id"

"authority-id": "canonical",
"brand-id": "canonical",

These properties define the authority responsible for the image. Change both instances of the string “canonical” to your developer id that you retrieved earlier (“xSfWKGdLoQBoQx88”, in our example output). This links the image to your Ubuntu One account and ensures that only you can push image updates to devices using your model.

4.2 "timestamp"

    "timestamp": "2020-03-31T12:00:00.0Z", 

This needs to be provided at the end of the process; we’ll come back to this.

4.3 "snaps"

    "snaps": [
        {
            "name": "pi",
            "type": "gadget",
            "default-channel": "20/stable",
            "id": "YbGa9O3dAXl88YLI6Y1bGG74pwBxZyKg"
        },
        ...

This section lists the snaps to be included in the image. pi (shown above), pi-kernel, core20 and snapd are the four snaps required for a functioning Ubuntu Core device.

Additional snaps are included in the same way, with each snap requiring the following fields:

  • name: simply the snap name.
  • type: the type of snap. This is app for standard application snaps.
  • default-channel: the channel to install the snap from.
  • id: a unique snap identifier associated with every published snap. This is snap-id in the output from snap info <snap-name>.

For this tutorial, we’re going to add the AdGuard Home snap, an open source network-wide blocker for advertising and tracking. It’s an ideal candidate for an Ubuntu Core image like this because it benefits from frequent autonomous updates and a confined environment.

To add the AdGuard Home snap to our image, we need to add the following JSON stanza to the snaps section in our my-model.json (note the comma you need to append to the preceding snap entry to show continuation):

        },
	    {
            "name": "adguard-home",
            "type": "app",
            "default-channel": "latest/edge",
            "id": "UXZIkJfJT2SPCGejjnSjOBqJ71yHk8bw"
        }

Snaps do not have dependencies, but they do require the presence of the base snap they were built on. AdGuard Home is built using a base of core20 (see the output from snap info adguard-home --verbose | grep "base:"), which is the default base for our image, so no further edits are necessary.

4.4 Complete model assertion

After finishing all your edits, the completed my-model.json text file should now contain the following:

{
    "type": "model",
    "series": "16",
    "authority-id": "xSfWKGdLoQBoQx88",
    "brand-id": "xSfWKGdLoQBoQx88",
    "model": "ubuntu-core-20-pi-arm64",
    "architecture": "arm64",
    "timestamp": "2022-02-16T12:50:44+00:00",
    "base": "core20",
    "grade": "signed",
    "snaps": [
        {
            "name": "pi",
            "type": "gadget",
            "default-channel": "20/stable",
            "id": "YbGa9O3dAXl88YLI6Y1bGG74pwBxZyKg"
        },
        {
            "name": "pi-kernel",
            "type": "kernel",
            "default-channel": "20/stable",
            "id": "jeIuP6tfFrvAdic8DMWqHmoaoukAPNbJ"
        },
        {
            "name": "core20",
            "type": "base",
            "default-channel": "latest/stable",
            "id": "DLqre5XGLbDqg9jPtiAhRRjDuPVa5X1q"
        },
        {
            "name": "snapd",
            "type": "snapd",
            "default-channel": "latest/stable",
            "id": "PMrrV4ml8uWuEUDBT8dSGnKUYbevVhc4"
        },
        {
            "name": "adguard-home",
            "type": "app",
            "default-channel": "latest/edge",
            "id": "UXZIkJfJT2SPCGejjnSjOBqJ71yHk8bw"
        }
    ]
}

5. Sign the model assertion

We next need to create a GPG key and use it to sign our my-model.json model assertion. This ensures the model cannot be altered without the key and also links the created image to the signed version of the model we want to build.

This is accomplished in three stages:

  1. create a key
  2. register the key
  3. sign the model assertion

5.1 Create a key

First make sure there are no keys already associated with your account by running the snapcraft keys command (you will only have a key if you’ve previously signed an assertion; if you already have a key, you can use that one):

$ snapcraft keys
No keys have been registered. See 'snapcraft register-key --help' to register a key.

Now use snapcraft to create a key called my-models (the name is arbitrary):

$ snapcraft create-key my-models
Passphrase: <passphrase>
Confirm passphrase: <passphrase>

As shown above, you will be asked for a passphrase. You need to remember this as you’ll be prompted to enter it whenever you use the key, including the very next step.

Note: Rather than creating a key for every device, the same key is typically used across all models or model families.

5.2 Register the key

We now need to upload the key and register it with your Ubuntu One account. This is accomplished with register-key:

$ snapcraft register-key
Enter your Ubuntu One e-mail address and password.
If you do not have an Ubuntu One account, you can create one at https://snapcraft.io/account
Email: <Ubuntu-SSO-email-address>
Password: <Ubuntu-SSO-password>

Registering key ...
Done. The key "my-models" (<key fingerprint>) may be used to sign your assertions.

Regardless of whether you’re logged in with snapcraft, you will be asked for your account and password details. You’ll also need to unlock the key with your passphrase, and when the process is complete, the snapcraft keys command will now list the registered key:

$ snapcraft keys
    Name       SHA3-384 fingerprint
*   my-models  <key fingerprint>

Update the timestamp

As mentioned earlier, the timestamp in the model assertion must be set to a time and date after the creation of our key. This means we need to edit my-model.json to update the timestamp with the current time.

    "timestamp": "2022-02-16T12:55:44+00:00",

This is a UTC-formatted time and date value, used to denote the assertion’s creation time. It needs to be replaced with the current time and date, which can be generated with the following command:

$ date -Iseconds --utc
2022-02-18T08:50:43+00:00

5.3 Sign the model

A model assertion is created and signed by feeding the JSON file into the snap sign command (along with your recently-created key name), and capturing the output in the corresponding model file:

$ snap sign -k my-models < my-model.json > my-model.model

You will again be asked for your key’s passphrase.

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

gpg: signing failed: If you encounter a gpg: signing failed error while signing your assertion from a non-desktop session, such as over SSH, run export GPG_TTY=$(tty) first.

6. Build the image

Images are built from the recipe contained in the model assertion using ubuntu-image, a tool to generate a bootable image. It’s installed using snap:

$ sudo snap install ubuntu-image --classic

The ubuntu-image command requires two arguments; snap to indicate we’re building a snap-based Ubuntu Core image, and the filename of our previously-signed model assertion to build an image:

$  ubuntu-image snap my-model.model
WARNING: proceeding to download snaps ignoring validations, this default will change in the future. For now use --validation=enforce for validations to be taken into account, pass instead --validation=ignore to preserve current behavior going forward
Fetching snapd
Fetching pi-kernel
Fetching core20
Fetching pi
Fetching adguard-home

The entire process should only take a few minutes (depending on your connectivity), with the creation of a pi.img Ubuntu Core image file being the end result.

7. Write the image

The next step is to write the pi.img file to the microSD card. There are many ways to do this, but our recommended way is to use Raspberry Pi Imager. This can be installed from its snap:

$ sudo snap install rpi-imager

After installation, launch Raspberry Pi Imager from the desktop. Click on the first ‘Choose OS’ button under ‘Operating System’ and select ‘Use custom’.

This will open a file requester, and you now need to navigate to, and select, the pi.img file we generated in the previous step.

Next, make sure the microSD card is inserted to a connected microSD card reader and select ‘Choose storage’ under ‘Storage’. Your microSD card will be listed and needs to be selected.

With the microSD card selected, select the final ‘Write’ button to commence the image writing process.

When the process completes, you can safely remove the microSD card.

NOTE: Developers used to writing raw bootable images to SD cards are welcome to simply use the dd command.

8. Boot Ubuntu Core

You can now insert the microSD card into your powered-off Raspberry Pi and power-on the device. For this setup phase, you will also need to have a keyboard and screen connected.

From this point, it can take around five minutes for the system to instantiate itself. You will see typical Linux output on the screen, periods where there’s just a flashing cursor, and messages like Installing the system, please wait for a reboot. When this process has finished, you will see the following:

Press enter to configure.

Press Enter and you will see a small menu with a single item:

Configure the network and setup an administrator account on this all-snap Ubuntu Core system.

Press Enter again and you will be taken to the network setup page:

image

8.1 Configure a network connection

Network access is a requirement to setup Ubuntu Core, and you have a choice about whether to use a wired connection (Ethernet) or Wi-Fi, if your Raspberry Pi supports it.

Use the cursor up key, or tab, to move to the appropriate wlan0 or eth0 option to configure Wi-Fi or Ethernet respectively.

Wi-Fi (recommended)

This is the most common option. If you have a device with Wi-Fi capabilities, such as a Raspberry Pi 3 or 4, it will appear as a separate network device called wlan0 beneath any Ethernet devices.

image

To configure Wi-Fi, press the cursor up key until wlan0 is selected and press Enter. You will see a small menu and you need to select Edit Wifi.

image

After selecting Edit Wifi, you will see the network interface configuration panel for Wi-Fi. If you know the name of the Wi-Fi network you wish to connect to, it can be entered directly, or cursor down to Choose a visible network and select a network from a list of those that have been detected.

Finally, enter the Wi-Fi password and select Save to complete the configuration. You will be returned to the previous menu and your device will attempt to connect to the network. If successful, you will see its IP address to the right of DHCPv4.

For advanced network configuration, such as setting a static IP address, select the wlan0 device again, and choose edit IPv4 from the menu.

When you’ve finished configuring your network settings, select Done and press enter on the Network connections page to move on to the final step.

Ethernet

If an Ethernet cable is connected to your device, a network connection will attempt to be automatically negotiated and, if this is successful, you will see an IP address for the device after the DHCPv4 entry in the Network connections page. In this case, you don’t need to do anything further:

To configure an Ethernet connection manually, select the eth0 device and select Edit IPv4 from the small menu that appears. By default, the network device will be configured to use Automatic (DHCP), which is why the connection attempts to automatically configure itself. Press Enter to reveal two further options, Manual and Disabled:

Selecting Manual will allow you to configure your Ethernet connection manually by entering values for your subnet mask value (using CIDR xx.xx.xx.xx/yy notation), the static IP address of your device, the network gateway, and the name servers you wish to use:

Select Save to apply those changes and for the connection to be attempted. You can now proceed to the next step by pressing Done.

Step 9: Connect to the device

A final configuration step asks for the email address of your account in the store. This can be safely ignored for now. Whenever your device boots, it will automatically connect to the network and requires no further configuration.

Each time the device starts up, if a display connected it will show its various addresses and the account linked to the device:

image

As we’ve built this Ubuntu Core image to include the AdGuard Home snap, we can now connect to this service via the published IP address and its configured port (3001 for setup):

Congratulations! You have successfully built your own image, installed it, and connected to Ubuntu Core 20 on your Raspberry Pi.

See First steps with Ubuntu Core for an introduction to using Ubuntu Core.

Last updated a month ago. Help improve this document in the forum.