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
- 2. Retrieve your developer account id
- 3. Download a model assertion
- 4. Edit the model assertion
- 5. Sign the model assertion
- 6. Build the image
- 7. Write the image
- 8. Boot Ubuntu Core
- 9. Connect to the device
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 isapp
for standard application snaps. -
default-channel
: the channel to install the snap from. -
id
: a unique snap identifier associated with every published snap. This issnap-id
in the output fromsnap 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:
- create a key
- register the key
- 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:
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.
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.
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:
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.