I recently tried to get the openFATE client working in a container. I know it may sound stupid, but I didn't want to pollute my Gnome machine with KDE libraries (end of troll) and wanted to use this to seriously play with application containers. My constraints were:

  • minimize the duplication of the root file system. I know docker already does this, but I'm a libvirt hacker after all.
  • get the application show up on the host display for a smooth use.

Creating the root file system

To minimze the file system duplication, I went with a qcow2 disk image with a backing file. The first step is to create the base image with the file system to be reused by other containers.

Create the disk image:

qemu-img create -f qcow2 test-base.qcow2 15G

Create the nbd device from the disk image, format it and mount it. Depending on your linux distribution you may even need to load the nbd kernel module, hence the first line.

modprobe nbd
/usr/bin/qemu-nbd --format qcow2 -n -c /dev/nbd0 $PWD/test-base.qcow2
mkfs.ext3 /dev/nbd0
mount /dev/nbd0 /mnt

Populate the image with the openSUSE 13.2 Minimal_base pattern:

zypper --root /mnt ar http://download.opensuse.org/distribution/13.2/repo/oss/ main
zypper --root /mnt ar http://download.opensuse.org/update/13.2/ updates
zypper --root /mnt in -t pattern Minimal_base

Unmount and clean up the nbd device:

umount /mnt
pkill qemu-nbd

Now, we will create an overlay image on top of the one we just created. In this image, we will only install fate, and create an unprivileged user to run it.

Create the image. Note the backing_file and backing_fmt options as they will actually setup the backing chain of qcow2 images.

qemu-img create -f qcow2 \
                -o backing_fmt=qcow2,backing_file=$PWD/test-base.qcow2 \
                myapp.qcow2

Mount the new image via qemu-nbd again. Note that there is no need to format that new image: it is really only a diff with the test-base.qcow2 file.

/usr/bin/qemu-nbd --format qcow2 -n -c /dev/nbd0 $PWD/myapp.qcow2
mount /dev/nbd0 /mnt

Install the application (fate in this example):

zypper --root /mnt ar http://download.opensuse.org/repositories/FATE/openSUSE_13.2/ FATE
zypper --root /mnt in fate

Add an unprivileged user:

useradd -m myuser

For the application to find the X server display, set the DISPLAY in the user's profile. The localhost:0 value here can be adjusted depending on the network settings of the container. In this case, I will use a container without the netns namespace to simplify, but with the default libvirt network the value would be 192.168.122.1:0.

echo 'export DISPLAY=localhost:0' > ~myuser/.profile

Unmount and clean up the nbd device:

umount /mnt
pkill qemu-nbd

Setting up the container

Sadly, there is no other possible way to create the container than by manually feeding the XML definition to libvirt. Here is a template for the definition:

<domain type='lxc'>
  <name>myapp</name>
  <memory unit='MiB'>256</memory>
  <vcpu placement='static'>1</vcpu>
  <resource>
    <partition>/machine</partition>
  </resource>
  <os>
    <type arch='x86_64'>exe</type>
    <init>/usr/bin/su</init>
    <initarg>-</initarg>
    <initarg>myuser</initarg>
    <initarg>-c</initarg>
    <initarg>/usr/bin/fate</initarg>
  </os>
  <clock offset='utc'/>
  <on_poweroff>destroy</on_poweroff>
  <on_reboot>restart</on_reboot>
  <on_crash>destroy</on_crash>
  <devices>
    <controller type='ide' index='0'/>
    <filesystem type='file'>
      <driver type='nbd' format='qcow2'/>
      <source file='/path/to/myapp.qcow2'/>
      <target dir='/'/>
    </filesystem>
    <filesystem type='mount'>
      <source dir='/home/myuser/.Xauthority'/>
      <target dir='/home/myuser/.Xauthority'/>
    </filesystem>
    <filesystem type='ram'>
      <source usage='10240' units='KiB'/>
      <target dir='/run'/>
    </filesystem>
    <filesystem type='ram'>
      <source usage='102400' units='KiB'/>
      <target dir='/tmp'/>
    </filesystem>
    <console type='pty'/>
  </devices>
  <seclabel type='dynamic' model='apparmor' relabel='yes'/>
</domain>

In this template, the memory, vcpu and paths to images need to be adapted to your setup. Note that the qcow2 image is mounted in the container using the filesystem nbd driver.

The host user's .Xauthority file is mounted in the container's user home. This is needed for the X applications to connect to the host display.

For the application to be automatically launched as the unprivileged user, the definition init command is set to

/usr/bin/su - myuser -c /usr/bin/fate

Running the application

Before being able to connect to the host Xorg server, we need to have it listen for TCP connection. In openSUSE, this can be achieved by changing DISPLAYMANAGER_XSERVER_TCP_PORT_6000_OPEN to yes in /etc/sysconfig/displaymanager.

The application needs to be able to authenticate on the host Xorg server. For my tests I manually crafted the .Xauthority using xauth. Just remember that the cookie will change, so either regenerate it before each start or mount $XAUTHORITY directly to the container's .Xauthority.

xauth extract - $DISPLAY | xauth -f $HOME/.Xauthority merge -

Running the application is as easy as starting the container:

virsh -c lxc:/// start --console myapp

Of course it would be much more convenient to have it wrapped in a script with proper sudo configuration to let normal users run the container without needing to become root.

Limitations

This approach is still pretty complex and not that easy for newcomers.

  • The image creation process will be simplified as part of Google Summer of Code 2015 with libvirt.
  • The container definition and start could be done with virt-sandbox, but the host-image mount parameter should first be able to take qcow2 images.
  • libvirt doesn't stop the qemu-nbd process when the container is stopped, but hey, that's a bug I can work on!