EDIT March 2021: Now that I've updated my website stack (using an ODROID HC1, same board as XU4, but with built-in SATA). I have GoatCounter set up for web analytics, and I can see I'm still getting some traffic to this article from google. If you found this and you are looking to use docker with ODROID XU4, you should know that this article is quite old (4 years old now) and these days docker is way better supported on ODROID XU4 and ARMv7 in general.

I'm running Armbian & I believe I used a script roughly like this to install docker and docker-compose. I also set up multi-architecture docker builds for all of my apps.

Original article:

I have been interested in ARM-based SBCs (Single Board Computer)s for a while -- especially as they become more and more capable/compatible. In the world of cloud computing, I think they are especially interesting because application designs are trending towards horizontal scalability, that is, scaling by adding more processor cores rather than by making each core faster. Plus, they're arguably cheaper both in silicon manufacturing and in power consumption / cooling. One user measured this board at 2w idle and 13w under load at the wall. That's really not a lot of power considering what the thing is capable of!

I finally decided to take the plunge and ordered an ODROID XU4 from ameridroid. I'm planning on switching over to ARM as a platform for my self hosted projects in the future, as a fun way to learn more about the platform and prove that it can be awesome in the web server arena.

photograph of an odroid xu4 next to a guitar pick. The xu4 is only about two to three times as long and wide as the guitar pick

Banana was too big, so guitar pick for scale 😊

About the XU4

I chose this piece of hardware since it appeared to have a good balance of low price, compatibility, and performance.

  • Less than $100 including a case, taxes, and shipping.
  • Its 32 bit armhf architecture is older and currently more widely supported compared to aarch64.
  • A handful of different Linux distributions support it.
  • Octa-core Exynos processor is pretty fast and has an insane amount of performance per watt.
    • Compared to an Intel Celeron G3900 (Q4'15) in Geekbench, the Exynos processor has half the multi-threaded performance but over 3x the power efficiency under load. In power efficiency at idle, it probably beats the amd64 chips even harder.
  • It has a dedicated gigabit Ethernet chip as well as a full speed USB 3.0 hub.
    • USB 3.0 is fast enough for most I/O needs, so I can just plug it into a USB hard drive enclosure and forget about mucking around with SD cards or expensive eMMC chips.

Moving the file system to the USB Hard Drive

First step was to move the operating system (minus the boot partition) to my hard drive in its powered USB 3.0 enclosure. Powered is important since we don't want to stress out the 5v power supply trying to spin up the platters! These SBCs usually do not deal well with peripherals that consume large amounts of current all at once.

The following assumes you have just booted the XU4 from the standard Ubuntu SD card image from ODROID and SSHed to it. The default users are odroid/odroid and root/odroid (only root/odroid for the minimal disto). Make sure to change thier passwords to something more secure with passwd and sudo passwd!

The following was required to get aptitude to work after writing the minimal ubuntu xenial image to the sd card:

apt-get update
apt-get -f install

List the disks and details about them:

fdisk -l

If the disk does not have any partitions created yet, it will look something like this:

Disk /dev/sda: 238.5 GiB, 256060514304 bytes, 500118192 sectors
Disk model:                 
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes

If it does have a partition table, it will show up as the Disklabel type:

Disk /dev/sda: 238.5 GiB, 256060514304 bytes, 500118192 sectors
Disk model:                 
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes
Disklabel type: gpt
Disk identifier: B7839DF2-FE12-4C52-B1E8-397F03157800

If you have to create the partition table / partition, you can do that with the parted command.

sudo parted --script "/dev/sda" mklabel gpt
sudo parted --script --align optimal "/dev/sda" mkpart primary ext4 0% 100%

Find out what the drive partition is called and if it is currently mounted with lsblk. (list block storage)


it turned out it was called sda1 (partition 1 on disk a)

if it is currently mounted, you will want to unmount it with sudo umount /dev/sda1

Format the partition we will be using on the HDD as ext4 with label rootfs

sudo mkfs.ext4 /dev/sda1 -L rootfs

Mount the freshly formatted partition at the temporary mount point /mnt

sudo mount /dev/sda1 /mnt

Install rsync and use it to copy the entire filesystem from the SD card to the HDD:

sudo apt-get -y install rsync
sudo rsync -axv / /mnt

go to the boot folder on the SD card

cd /media/boot

install nano ( or use the text editor of your choice )

sudo apt-get -y install nano

edit the boot.ini file to point to the HDD rather than the SD card.

nano boot.ini

This is the part I edited. The changed piece was root=/dev/sda1

# Basic Ubuntu Setup. Don't touch unless you know what you are doing.
# -------------------------------
setenv bootrootfs "console=tty1 console=ttySAC2,115200n8 root=/dev/sda1 rootwait ro fsck.repair=yes net.ifnames=0 "

After that, a restart of the XU4 was required to complete the process. reboot -p over SSH did the trick. After that, lsblk shows that the HDD partition is mounted at /. The SD card will still be required for boot, but it will not slow anything down during normal operation.

Installing Docker

Installing docker through the instructions for Ubuntu 16.04 Xenial on the Docker Website did not work, but installing this older version through the ubuntu ports package repository seemed to work fine:

sudo apt-get install docker.io

We can test docker on someone else's armv7 image from docker hub:

sudo docker run --rm armv7/armhf-ubuntu_core:16.04 cat /etc/lsb-release

Creating a Base Image from Scratch

My method for getting started with a base image was adapted from the user Hominidae on the ODROID forums.

To build a docker base image, Download a minimal version of ubuntu to ~/docker-base-image/ubuntu with debootstrap

mkdir ~/docker-base-image && cd ~/docker-base-image

sudo apt-get install -y debootstrap
sudo debootstrap --verbose --variant=minbase --include=iproute,iputils-ping --arch armhf xenial ./ubuntu http://ports.ubuntu.com/ubuntu-ports/

overwrite the built in sources.list file with the one from ODROID

sudo cp /etc/apt/sources.list ./ubuntu/etc/apt/sources.list

Make a tar of the minimal Ubuntu OS install that can be used in a Dockerfile.

sudo tar -C ubuntu -c . > ubuntu.tar

Create your Dockerfile

nano Dockerfile

Then paste in the following and save:

FROM scratch

ADD ubuntu.tar /

#ripped from https://github.com/tianon/docker-brew-ubuntu-core/blob/master/update.sh

# a few minor docker-specific tweaks
# see https://github.com/docker/docker/blob/9a9fc01af8fb5d98b8eec0740716226fadb3735c/contrib/mkimage/debootstrap
RUN set -xe \
        # https://github.com/docker/docker/blob/9a9fc01af8fb5d98b8eec0740716226fadb3735c/contrib/mkimage/debootstrap#L40-L48
        && echo '#!/bin/sh' > /usr/sbin/policy-rc.d \
        && echo 'exit 101' >> /usr/sbin/policy-rc.d \
        && chmod +x /usr/sbin/policy-rc.d \
        # https://github.com/docker/docker/blob/9a9fc01af8fb5d98b8eec0740716226fadb3735c/contrib/mkimage/debootstrap#L54-L56
        && dpkg-divert --local --rename --add /sbin/initctl \
        && cp -a /usr/sbin/policy-rc.d /sbin/initctl \
        && sed -i 's/^exit.*/exit 0/' /sbin/initctl \
        # https://github.com/docker/docker/blob/9a9fc01af8fb5d98b8eec0740716226fadb3735c/contrib/mkimage/debootstrap#L71-L78
        && echo 'force-unsafe-io' > /etc/dpkg/dpkg.cfg.d/docker-apt-speedup \
        # https://github.com/docker/docker/blob/9a9fc01af8fb5d98b8eec0740716226fadb3735c/contrib/mkimage/debootstrap#L85-L105
        && echo 'DPkg::Post-Invoke { "rm -f /var/cache/apt/archives/*.deb /var/cache/apt/archives/partial/*.deb /var/cache/apt/*.bin || true"; };' > /etc/apt/apt.conf.d/docker-clean \
        && echo 'APT::Update::Post-Invoke { "rm -f /var/cache/apt/archives/*.deb /var/cache/apt/archives/partial/*.deb /var/cache/apt/*.bin || true"; };' >> /etc/apt/apt.conf.d/docker-clean \
        && echo 'Dir::Cache::pkgcache ""; Dir::Cache::srcpkgcache "";' >> /etc/apt/apt.conf.d/docker-clean \
        # https://github.com/docker/docker/blob/9a9fc01af8fb5d98b8eec0740716226fadb3735c/contrib/mkimage/debootstrap#L109-L115
        && echo 'Acquire::Languages "none";' > /etc/apt/apt.conf.d/docker-no-languages \
        # https://github.com/docker/docker/blob/9a9fc01af8fb5d98b8eec0740716226fadb3735c/contrib/mkimage/debootstrap#L118-L130
        && echo 'Acquire::GzipIndexes "true"; Acquire::CompressionTypes::Order:: "gz";' > /etc/apt/apt.conf.d/docker-gzip-indexes \
        # https://github.com/docker/docker/blob/9a9fc01af8fb5d98b8eec0740716226fadb3735c/contrib/mkimage/debootstrap#L134-L151
        && echo 'Apt::AutoRemove::SuggestsImportant "false";' > /etc/apt/apt.conf.d/docker-autoremove-suggests

# delete all the apt list files since they're big and get stale quickly
RUN rm -rf /var/lib/apt/lists/*

# this forces "apt-get update" in dependent images, which is also good
# enable the universe
RUN sed -i 's/^#\s*\(deb.*universe\)$/\1/g' /etc/apt/sources.list

# make systemd-detect-virt return "docker"
# See: https://github.com/systemd/systemd/blob/aa0c34279ee40bce2f9681b496922dedbadfca19/src/basic/virt.c#L434
RUN mkdir -p /run/systemd && echo 'docker' > /run/systemd/container

# overwrite this with 'CMD []' in a dependent Dockerfile
CMD ["/bin/bash"]

Now, build the docker image based on the Dockerfile you just created:

sudo docker build -t ubuntu:xenial-armhf .

examine the resulting base image & test it:

sudo docker images
sudo docker run --rm ubuntu:xenial-armhf cat /etc/lsb-release

That's all for now. I will be posting more detail later after I set up image builds and deploy Let's Encrypt, a reverse proxy, Ghost, Gogs, and more on the XU4.