Creating a Custom Ubuntu Server Disk Image for the Raspberry Pi with CustomPiOS
Tags: ubuntu, raspberrypi, linux, devops
Published: 2021-12-16 21:59:17

This article will walk through the creation of a custom disk image for the Raspberry Pi. Possible use cases include...
* Making a Raspberry Pi disk image with all of your favorite apps pre-installed
* Making a distributable file for a product that runs on the Raspberry Pi (something like [OctoPrint](https://octoprint.org/))

All of this is made possible by [CustomPiOS](https://github.com/guysoft/CustomPiOS), a tool that opens, modifies, and repackages pre-existing ARM images (usually a Raspbian / Raspberry Pi OS image downloaded from the official website).

This particular article will focus on making a 64-bit Ubuntu Server spin-off for the Raspberry Pi that runs docker-compose on boot. The process for making a Raspberry Pi OS spin-off is similar, but I would like to focus on the Ubuntu Server because it poses a few unique challenges that are worth covering.


## Getting Started

First, let's clone the CustomPiOS github repo and cd into the *src* folder.
```bash
git clone https://github.com/guysoft/CustomPiOS
cd CustomPiOS/src
```
We'll also want to install these dependancies:
```bash
sudo apt install qemu-user-static p7zip-full
```
Depending on your machine, you may need some other dependencies as well. A full list can be found on the [CustomPiOS github page](https://github.com/guysoft/CustomPiOS#requirements).

Next, we need to run the *make_custom_pi_os* script. This will create a folder for our custom distro's config files.
```bash
./make_custom_pi_os custom_distro
```
If you're creating a spin-off of Raspberry Pi OS, you may also want to add the `-g` flag. This will automatically download the latest version of Rasbian / Raspberry Pi OS.

Next, let's change directory into the folder we just created
```bash
cd custom_distro/src
```
This is the directory we'll be working in from now on. It's structure should be as follows.
```
.
├── build_dist
├── config
├── custompios_path
├── image
│   └── README
├── modules
│   └── custom_server
│       ├── config
│       ├── end_chroot_script
│       ├── filesystem
│       │   ├── boot
│       │   │   └── README
│       │   ├── home
│       │   │   ├── pi
│       │   │   │   └── README
│       │   │   └── root
│       │   │       └── README
│       │   └── root
│       │       └── README
│       └── start_chroot_script
└── vagrant
    ├── run_vagrant_build.sh
    ├── setup.sh
    └── Vagrantfile
```
I'll do my best to explain what these files allow us to do, but I won't be able to touch on everything in detail.

Let's start with...

### The Config File

The *config* file contains distro-wide settings. For those creating an Ubuntu Server spin-off, it should look something like this.
```bash
export DIST_NAME=CustomDisto
export DIST_VERSION=0.0.1
export MODULES="base(network,docker,custom_distro)"


export BASE_DISTRO=ubuntu
export BASE_ARCH=arm64

export BASE_ADD_USER=yes
export BASE_USER=pi
export BASE_USER_PASSWORD=12345

export BASE_IMAGE_ENLARGEROOT=4000
export BASE_IMAGE_RESIZEROOT=200
```

Some of these variables are self-explanatory, but some are not. The `MODULES` variable defines how functionality will be added to the base image (we'll talk more about that in a moment). The `BASE_IMAGE_ENLARGEROOT` and `BASE_IMAGE_RESIZEROOT` variables determine how the size of the image is increased before and after files are copied onto it (these two variables are optional; only use them if you need to).

### The Image Folder

The *image* folder is where the base image for our custom distro should go. In fact, if you're following along, go ahead and download the 64-bit Ubuntu Server disk image file. It can be found [here](https://ubuntu.com/download/raspberry-pi). Don't extract the image, though. Simply place the compressed file in the *image* directory.

CustomPiOS should be able to find most images on its own, but it has trouble with the 64-bit Ubuntu image. If it's having trouble, set the base image path in the config file like so.
```bash
export BASE_ZIP_IMAGE=image/my_base_image.img.xz
```

### The Modules Folder

The *modules* folder contains "modules" that add functionality to the custom distro. By default, a module of the same name as your custom distro folder was created when you ran the *make_custom_pi_os* script. Within this module is a *filesystem* folder, a *start_chroot_script*, an *end_chroot_script*, and another *config* file.

Let's break each of these down.

#### The Filesystem Folder

The *filesystem* folder contains all of the custom files that you would like to be present on your custom distro. It contains three sub-folders, each of which will unpacked at a different mount point: the *root* folder will unpacked at the root directory, or */*; the *home* folder will be unpacked at */home*; and the *boot* directory will be unpacked in the boot partition (which is usually mounted at */boot*).

#### The Start Chroot Script

The *start_chroot_script* runs in a chroot environment on the disk image. Use this to install software, change file permissions, and/or do anything else that would require the use of terminal.

In this script, you should also have access to a few extra commands (courtesy of the auto-generated line `source /common.sh`): the `unpack` command and the `gitclone` command. More information can be found [here](https://github.com/guysoft/CustomPiOS#useful-commands-from-commonsh).

#### The End Chroot Script

The *end_chroot_script* can be used to clean up after the *start_chroot_script*. A lot of modules don't use this, however. Feel free to delete it if you'd like. 

#### The Module Config File

The *config* file within the module folder can used to set environment variables for your own use within the start and end chroot scripts. By default, only an example variable is generated.
```bash
CUSTOM_DISTRO_VAR="This is a module variable"
```
Note that the variable name starts with `CUSTOM_DISTRO`. For a variable to be exported, it's name must begin with the module name. Also, note the slight change in syntax from the global config file. Instead of using the `export` command, these config files use *.env* style syntax.

Although you could hard-code values into the start and end chroot scripts, I would strongly recommend using these module-level config files. The variables exported from these files are also accessible in (and can be overridden by) the main config file. This greatly improves modularity; after you're done creating your custom distro, you could even release your custom_distro module as an add-on to CustomPiOS.



### Including Modules in the Final Disk Image

Now, let's revisit this line in the main config file.
```bash
export MODULES="base(network,docker,custom_distro)"
```
The variable `MODULES` defines which modules to include and how those modules should depend on each other. In the example above, three modules are included: `base`, `network`, `docker`, and `custom_distro`. The `network`, `docker`, and `custom_distro` modules depend on (or extend, we could say) the `base` module. In fact, every module must depend on the `base` module. However, it can get more complicated. Here's another example.
```bash
export MODULES="base(network(octopi, picamera))"
```

In this case, `octopi` and `picamera` depend on `network`, which depends on `base`.

I would also like to point out that we have access to quite a few built-in modules, including...
* admin-toolkit
* auto-hotspot
* auto-mount-removable
* cockpit
* docker
* gui
* usbconsole

Those are a few of my favorites. A full list with descriptions can be found [here](https://github.com/guysoft/CustomPiOS/wiki/Modules#list-of-standard-library-modules).

## Building our Custom Ubuntu Server

In order to create our custom Ubuntu Server, we're just going to make a few changes to the `custom_distro` module.

### Setting a Static IP

In order to set a static IP, we'll create *etc/netplan/00-installer-config.yaml* within the *filesystem/root* directory of our module. Within this file, we'll save the following,

```yml
# Static ip configuration
network:
  ethernets:
    eth0:
      addresses: [10.0.0.2/24]
      gateway4: [10.0.0.1]
      nameservers:
        addresses: [4.2.2.2, 8.8.8.8]
  version: 2
```

where `10.0.0.2` is our static ip.


### Configuring docker-compose

To configure docker-compose, simply create a folder called *docker-compose* within the *root* directory, and then place your *docker-compose.yml* file within that folder along with another configuration files you might need (like a *.env* file).

### First-time Boot Script

It's often helpful to have docker-compose start on boot. A restart flag can be set in the *docker-compose.yml* file, but docker-compose still won't start on the first boot-up. To do that, we'll write first-time boot script.

In the *filesystem/root/etc* directory, create a file named *rc.local*. Then, within that file, save the following contents.
```bash
#!/bin/bash

echo 
echo "==== Starting Docker Compose ===="
echo

pushd /docker-compose
  docker-compose up -d
popd

echo
echo "==== Docker Compose has been started ===="
echo

rm ${BASH_SOURCE[0]} # Remove this line if you want the script to run on every boot-up
```

Finally, we'll need to make this script executable. We can do that easily within the *start_chroot_script* by adding the following line to the end of the script.
```bash
chmod +x /etc/rc.local
```

## Building the Image

After finishing the configuration of our custom distro, we finally get to build our distributable image. This can be done by running the following command (make sure you're in the *custom_distro/src* directory).
```bash
sudo ./build_dist
```

After the command finishes (it can take some time), the resulting distributable image can be found in the newly created *custom_distro/src/workspace* directory. It'll have the same name as the base image.

If you encounter any errors, be sure to review the configuration files. You may also want to try one of the different build methods listed on the [CustomPiOS github page](https://github.com/guysoft/CustomPiOS).

## Conclusion

CustomPiOS is a really powerful tool, and I'm actually surprised there aren't more tutorials for it out there. It's been really useful to me, and I think a lot of other people would appreciate knowing how to use it.

To check out some of the other projects that use CustomPiOS, take a look at [this list](https://github.com/guysoft/CustomPiOS#list-of-distributions-using-custompios).

Thanks so much for taking the time to read this article. I know it's kind of a long one, but I hope it's helpful. If you have any questions or if you find any errors in this article, please let me know in the comments below.

Have a good one,
\- Simon

SDG

.
.
.
.
.
Photo by <a href="https://unsplash.com/@harrisonbroadbent?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Harrison Broadbent</a> on <a href="https://unsplash.com/s/photos/raspberry-pi?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>