Friday, June 13, 2014

How to run Docker on a Raspberry Pi

Introduction

The easy way to just run docker on a RPi is just to follow these instructions, however, this requires you to create a dedicated SD card which runs Arch Linux. Since that distribution isn't my cup of tea I decided I wanted to run Docker on the standard Raspbian Debian Wheezy distribution. Although the docker executable can be downloaded in binary form for the RPi it won't run on the standard Raspbian kernel because it requires LXC containers and AUFS which aren't there. This blog post will explain how you can enable these features.
To prevent you or me having to perform the steps below I have copied the resultant kernel onto github. I did notice that the CPU utilisation was a little high, so I added the follwing line to the file: /boot/config.txt to resolve that:
cgroup_disable=memory
Since this requires making changes to the kernel configuration and building a linux kernel I suggest that you first read my other blog on how to do this. Once you have managed to build a linux kernel then the steps below explain how to customise it to support docker.

Downloading AUFS

Assuming you already have the linux source code for the RPi downloaded, in the vagrant ubuntu VM then follow the following steps.
sudo su
cd /opt/raspberrypi/linux
git clone git://aufs.git.sourceforge.net/gitroot/aufs/aufs3-standalone.git
I had a number of challenges getting AUFS to compile with the Linux kernel and I found the path of least resistance was to use the following versions:
  • Linux rpi-3.10.y
  • AUFS  aufs3.10
Therefore set the appropriate git branches:
git checkout rpi-3.10.y
cd aufs3-standalone
git checkout origin/aufs3.10
Now we need to actually patch the AUFS code into the appropriate linux kernel source code:
cp -rp *.patch ../
cp -rp fs ../
cp -rp Documentation/ ../
cp -rp include/ ../
cd ..

patch -p1 < aufs3-base.patch
patch -p1 < aufs3-mmap.patch
patch -p1 < aufs3-standalone.patch
patch -p1 < aufs3-kbuild.patch
patch -p1 < aufs3-loopback.patch
You should have seen a lot of 'Hunk #x succeeded' messages and two failures. We need to fix one of these as the 2nd can be ignored.
patching file mm/fremap.c 
Hunk #1 FAILED at 202. 1 out of 1 hunk FAILED -- saving rejects to file mm/fremap.c.rej
..
patching file include/uapi/linux/Kbuild
Hunk #1 FAILED at 56.
1 out of 1 hunk FAILED -- saving rejects to file include/uapi/linux/Kbuild.rej
Ok, so now we need to edit mm/fremap.c to fix the issue as follows. I use vi (which shows my age) but with your favourite editor first look at the mm/fremap.c.rej file:
--- mm/fremap.c
+++ mm/fremap.c
@@ -202,11 +202,12 @@
        */
        if (mapping_cap_account_dirty(mapping)) {
                        unsigned long addr;
-                       struct file *file = get_file(vma->vm_file);
+                       struct file *file = vma->vm_file;
+                       vma_get_file(vma);
                        addr = mmap_region(file, start, size,
                                        vma->vm_flags, pgoff);
-                       fput(file);
+                       vma_fput(vma);
                        if (IS_ERR_VALUE(addr)) {
                                err = addr;<
                        } else {
This is a diff format file. The important points are that the changes start at line 202 in the original file and that the lines that start with a '+' need to be added to the original source code and the lines that start with a '-' need to be removed.
Now we understand what we need to do we cn actually edit mm/fremap.c and look at the code at line 202
*/
                if (mapping_cap_account_dirty(mapping)) {
                        unsigned long addr;
                        struct file *file = get_file(vma->vm_file);
                        /* mmap_region may free vma; grab the info now */
                        vm_flags = vma->vm_flags;

                        addr = mmap_region(file, start, size, vm_flags, pgoff);
                        fput(file);
                        if (IS_ERR_VALUE(addr)) {
                                err = addr;
                        } else {
                                BUG_ON(addr != start);
                                err = 0;
                        }
                        goto out_freed;
                }
We can then change this to be:
*/
                if (mapping_cap_account_dirty(mapping)) {
                        unsigned long addr;
                        // Remove struct file *file = get_file(vma->vm_file);
                        struct file *file = vma->vm_file;
                        /* mmap_region may free vma; grab the info now */
                        vm_flags = vma->vm_flags; /* Add */

                        addr = mmap_region(file, start, size, vm_flags, pgoff);
                        // Remove fput(file);
                        vma_fput(vma); /* Add */
                        if (IS_ERR_VALUE(addr)) {
                                err = addr;
                        } else {
                                BUG_ON(addr != start);
                                err = 0;
                        }
                        goto out_freed;
                }
Now we are ready to configure the kernel. I am going to only summarise the changes here since Ken Cochran has provided good screen shots in his blog.
As I mentioned in my last post on how to build a kernel you should first start from the existing RPi configuration file (.config). Assuming you have done this then to configure the linux kernel you need to run:
ARCH=arm CROSS_COMPILE=${CCPREFIX} make menuconfig
The configuration parameters you need to set are as follows:
  1. General -> Control Group Support -> Memory Resource Controller for Control Groups (and its three child options)
    1. To reach the Control Group Support just scroll down and then press enter when on it. Whilst here also enable Cpu set Support (see next point). To set press space bar and you will see an asterisk appear next to the option. The escape key brings you up one level of menu.
  2. General -> Control Group Support -> cpuset support
  3. Device Drivers -> Character Devices -> Support multiple instances of devpts
    1. You will need to hit the escape key several time to reach the main screen to see the original screen and then scroll down to see the Device devices section. As before press space bar with this entry highlighted and scroll down to Character Devices and press space again. The go up one level using escape for the next entry
  4. Device Drivers -> Network Device Support -> Virtual ethernet pair device
  5. File Systems --> Miscellaneous filesystems ->select "Aufs (Advanced multi layered unification filesystem) support (NEW)" (mine was the the very bottom)
  6. Now save the configuration and exist the tool.
I tend to go back into the tool and check that the above configuration items are actually set before kicking off the kernel build as described in my previous blog post, don't forget to ensure that you have set CCPREFIX.
Assuming all is well you will now have a kernel which you should copy to the RPi as I described in my other blog post. Once successfully running on this kernel  you will need to install the LXC libraries used by docker.
I have been able to test the above parts of my blog using a new vagrant image. Now as I don't have a space SD card I am having to rely on the notes that I took when I completed my installation on my RPi. The original blog post that I followed is still useful.
On the RPI:
sudo su
apt 
mkdir /opt/lxc
cd /opt/lxc
git clone https://github.com/lxc/lxc.git
apt-get install automake libcap-dev
cd lxc
./autogen.sh && ./configure && make && make install
Now to check that LXC is working correctly on the RPi type:
pi@raspberrypi /opt $ lxc-checkconfig
--- Namespaces ---
Namespaces: enabled
Utsname namespace: enabled
Ipc namespace: enabled
Pid namespace: enabled
User namespace: missing
Network namespace: enabled
Multiple /dev/pts instances: enabled

--- Control groups ---
Cgroup: enabled
Cgroup clone_children flag: enabled
Cgroup device: enabled
Cgroup sched: enabled
Cgroup cpu account: enabled
Cgroup memory controller: enabled

--- Misc ---
Veth pair device: enabled
Macvlan: enabled
Vlan: enabled
File capabilities: enabled

Note : Before booting a new kernel, you can check its configuration
usage : CONFIG=/path/to/config /usr/local/bin/lxc-checkconfig
This will show that the kernel is ready for docker to be installed from here. I installed docker by downloading the tar file from resin (https://github.com/resin-io/lxc-docker-PKGBUILD/releases) & extract as root from /
sudo su /
tar xvf docker*.tar.xz
Since I have a hard drive on my RPi and the docker images can be large I symbolically linked /var/lib/docker to /usbmnt1 (my hard drive mount) having first copied the contents of /var/lib/docker to a directory on the hard drive. Whilst this appeared to work the -v flag to mount local directories across to the docker images did't work. Therefore, I removed the symbolic link and repopulated the /var/lib/docker directory and then used the -g flag when starting docker to store the images on /usbmnt1.
Then you can start docker and pull down a Raspbian image to base images from
sudo su -
export LD_LIBRARY_PATH=/usr/local/lib
nohup docker -d &
docker pull  resin/rpi-raspbian
docker run -i -t rpi-raspbian /bin/bash
Finally, here is a screen shot of docker working on my RPi.
docker






Happy dockering on the RPi!
If you want to download an image with Java & Tomcat already installed I prepared one earlier, which you can pull with the tag seahope/rpidockerjavatomcat from my original trying out of docker on the RPi.

No comments:

Post a Comment