Linux Containers (LXD) as an Alternative to VirtualBox for WordPress Development

If you’re using Vagrant for development then you’re already familiar with using virtual machines. And you’re most likely using VirtualBox as the VM provider.

Most people will think of Docker when they hear the word "containers". Docker uses LXC which is a feature of the Linux Kernel. LXD (pronounced Lex-Dee) is a "next-generation" version, also by Canonical (Ubuntu), which builds on top of LXC.

What is LXD?

By combining the speed and density of containers with the security of traditional virtual machines, Canonical’s LXD is the next‐generation of container hypervisor for Linux.

http://www.ubuntu.com/cloud/tools/lxd

LXD relies on features to be found in the Linux Kernel, and is therefore specific to Linux based operating systems. LXD allows you to run any version of Linux inside a container. This means that you can run one distribution on the host machine, and as many other distributions and versions of distributions in containers. Containers run at close to bare metal speeds and are also much more efficient on resource usage.

For more details on LXC and LXD check out the Linux Containers site.

Why use LXD?

So why would we want to use LXC/LXD instead of VirtualBox? As mentioned, it’s much faster and more resource efficient. Instead of an entire virtual machine running on top of your host OS, you can think of a container as a thin layer.

You might also be used to downloading a 400-600MB image which is a full bare Ubuntu OS (for example) to use with Vagrant/VirtualBox. With containers, your images are much smaller and usually under 100MB.

I have been experimenting with using LXC/LXD as an alternative to VirtualBox for working on local, Roots based WordPress projects. This post will show you how to get up and running. I intend to streamline the process by figuring out how to use LXC/LXD with Vagrant, and I intend to cover that in another, future post. For brevity, in this post I will refer to LXC/LXD as simply LXD.

Installation

As of Ubuntu 15.04, LXD is included in the main package repositories. If like me you are using Ubuntu 14.04, you will need to first add the PPA.

sudo add-apt-repository ppa:ubuntu-lxc/lxd-stable
sudo apt-get update

You can now install with.

sudo apt-get install lxd

Images and containers

In LXD land, we talk about containers instead of Virtual Machines. To create (launch) a container, you first need a base image of the operating system that you would like to run inside your container. There is an official repository that contains a variety of Linux based operating system images. To add the repository:

lxc remote add images images.linuxcontainers.org

In the example above, ‘images’ is used as the alias or local name of the remote repository. If you would like to list the remote repositories available to your system you can run

lxc remote list
+--------+-----------------------------------------+--------+
|  NAME  |                   URL                   | PUBLIC |
+--------+-----------------------------------------+--------+
| images | https://images.linuxcontainers.org:8443 | YES    |
| local  | unix://                                 | NO     |
+--------+-----------------------------------------+--------+

To view the operating system images available from the remote repository:

lxc image list images:

This should output a list of the available images on the remote repository.

+--------------------------------+--------------+--------+-------------------------+---------+----------+-------------------------------+
|             ALIAS              | FINGERPRINT  | PUBLIC |       DESCRIPTION       |  ARCH   |   SIZE   |          UPLOAD DATE          |
+--------------------------------+--------------+--------+-------------------------+---------+----------+-------------------------------+
| centos/6/amd64 (1 more)        | 73a93eab117f | yes    | Centos 6 (amd64)        | x86_64  | 49.79MB  | Oct 30, 2015 at 3:17am (GMT)  |
| centos/6/i386 (1 more)         | e376e6ee43fd | yes    | Centos 6 (i386)         | i686    | 49.98MB  | Oct 30, 2015 at 3:20am (GMT)  |
| centos/7/amd64 (1 more)        | ef1b7f41359f | yes    | Centos 7 (amd64)        | x86_64  | 55.80MB  | Oct 30, 2015 at 3:23am (GMT)  |
| debian/jessie/amd64 (1 more)   | 049e46c7d6ec | yes    | Debian jessie (amd64)   | x86_64  | 96.94MB  | Oct 29, 2015 at 11:33pm (GMT) |
| debian/jessie/armel (1 more)   | a0a557a346d8 | yes    | Debian jessie (armel)   | armv7l  | 91.91MB  | Oct 29, 2015 at 11:36pm (GMT) |
| debian/jessie/armhf (1 more)   | dae5930f9401 | yes    | Debian jessie (armhf)   | armv7l  | 92.18MB  | Oct 30, 2015 at 12:21am (GMT) |
| debian/jessie/i386 (1 more)    | 539b2e9c1105 | yes    | Debian jessie (i386)    | i686    | 97.93MB  | Oct 29, 2015 at 11:39pm (GMT) |
| debian/sid/amd64 (1 more)      | f936ab0b4f27 | yes    | Debian sid (amd64)      | x86_64  | 102.49MB | Oct 29, 2015 at 11:42pm (GMT) |
| debian/sid/armel (1 more)      | 02e44d4b2096 | yes    | Debian sid (armel)      | armv7l  | 95.64MB  | Oct 30, 2015 at 1:18am (GMT)  |
| debian/sid/armhf (1 more)      | 84d248cc43bf | yes    | Debian sid (armhf)      | armv7l  | 96.00MB  | Oct 30, 2015 at 2:18am (GMT)  |
| debian/sid/i386 (1 more)       | 1641f1e165e2 | yes    | Debian sid (i386)       | i686    | 103.47MB | Oct 29, 2015 at 11:46pm (GMT) |
| debian/squeeze/amd64 (1 more)  | 971a31211bcd | yes    | Debian squeeze (amd64)  | x86_64  | 89.44MB  | Oct 29, 2015 at 11:18pm (GMT) |
| debian/squeeze/armel (1 more)  | 63a903b6ee5e | yes    | Debian squeeze (armel)  | armv7l  | 86.28MB  | Aug 27, 2015 at 1:17am (BST)  |
| debian/squeeze/i386 (1 more)   | dc897365ddb9 | yes    | Debian squeeze (i386)   | i686    | 88.09MB  | Oct 29, 2015 at 11:21pm (GMT) |
| debian/wheezy/amd64 (1 more)   | c587f195fa2e | yes    | Debian wheezy (amd64)   | x86_64  | 91.58MB  | Oct 29, 2015 at 11:24pm (GMT) |
| debian/wheezy/armel (1 more)   | edd55b691f90 | yes    | Debian wheezy (armel)   | armv7l  | 88.61MB  | Oct 30, 2015 at 12:18am (GMT) |
| debian/wheezy/armhf (1 more)   | d411b6bbba6e | yes    | Debian wheezy (armhf)   | armv7l  | 87.01MB  | Oct 29, 2015 at 11:26pm (GMT) |
| debian/wheezy/i386 (1 more)    | 3e965052907e | yes    | Debian wheezy (i386)    | i686    | 91.57MB  | Oct 29, 2015 at 11:29pm (GMT) |
| gentoo/current/amd64 (1 more)  | 1fd503b62524 | yes    | Gentoo current (amd64)  | x86_64  | 193.53MB | Oct 29, 2015 at 3:49pm (GMT)  |
| gentoo/current/armhf (1 more)  | 2e0030f821d1 | yes    | Gentoo current (armhf)  | armv7l  | 177.89MB | Oct 29, 2015 at 4:23pm (GMT)  |
| gentoo/current/i386 (1 more)   | ac4157ac7b99 | yes    | Gentoo current (i386)   | i686    | 187.29MB | Oct 29, 2015 at 4:58pm (GMT)  |
| oracle/6.5/amd64 (1 more)      | fc0a0b9c505a | yes    | Oracle 6.5 (amd64)      | x86_64  | 141.90MB | Oct 30, 2015 at 12:19pm (GMT) |
| oracle/6.5/i386 (1 more)       | 47da98873bec | yes    | Oracle 6.5 (i386)       | i686    | 136.67MB | Oct 30, 2015 at 12:22pm (GMT) |
| plamo/5.x/amd64 (1 more)       | fb07451595e8 | yes    | Plamo 5.x (amd64)       | x86_64  | 244.36MB | Oct 29, 2015 at 10:25pm (GMT) |
| plamo/5.x/amd64/mini           | 18b8e0d3ee5e | yes    | Plamo 5.x amd64 (mini)  | x86_64  | 486.32MB | Oct 29, 2015 at 10:54pm (GMT) |
| plamo/5.x/i386 (1 more)        | 0b4ac7a85bc0 | yes    | Plamo 5.x (i386)        | i686    | 238.23MB | Oct 29, 2015 at 10:34pm (GMT) |
| plamo/5.x/i386/mini            | e63ca1cbef9e | yes    | Plamo 5.x i386 (mini)   | i686    | 478.92MB | Oct 29, 2015 at 11:14pm (GMT) |
| ubuntu/precise/amd64 (1 more)  | 6a44af6fbed1 | yes    | Ubuntu precise (amd64)  | x86_64  | 65.78MB  | Oct 30, 2015 at 4:18am (GMT)  |
| ubuntu/precise/armel (1 more)  | fead3962dda8 | yes    | Ubuntu precise (armel)  | armv7l  | 49.20MB  | Oct 30, 2015 at 5:17am (GMT)  |
| ubuntu/precise/armhf (1 more)  | 04cb87469355 | yes    | Ubuntu precise (armhf)  | armv7l  | 49.42MB  | Oct 30, 2015 at 6:17am (GMT)  |
| ubuntu/precise/i386 (1 more)   | ba9064320ac1 | yes    | Ubuntu precise (i386)   | i686    | 50.79MB  | Oct 30, 2015 at 4:21am (GMT)  |
| ubuntu/trusty/amd64 (1 more)   | e6106a320067 | yes    | Ubuntu trusty (amd64)   | x86_64  | 63.84MB  | Oct 30, 2015 at 4:24am (GMT)  |
| ubuntu/trusty/arm64 (1 more)   | 87275c954b84 | yes    | Ubuntu trusty (arm64)   | aarch64 | 60.78MB  | Jun 4, 2015 at 7:30am (BST)   |
| ubuntu/trusty/armhf (1 more)   | 6e4622a60051 | yes    | Ubuntu trusty (armhf)   | armv7l  | 64.94MB  | Oct 30, 2015 at 5:20am (GMT)  |
| ubuntu/trusty/i386 (1 more)    | 1f0619e66db4 | yes    | Ubuntu trusty (i386)    | i686    | 62.66MB  | Oct 30, 2015 at 4:28am (GMT)  |
| ubuntu/trusty/ppc64el (1 more) | 843ae9f29d5c | yes    | Ubuntu trusty (ppc64el) | ppc64le | 62.93MB  | Oct 30, 2015 at 4:31am (GMT)  |
| ubuntu/vivid/amd64 (1 more)    | d3c07317f88d | yes    | Ubuntu vivid (amd64)    | x86_64  | 64.28MB  | Oct 30, 2015 at 4:35am (GMT)  |
| ubuntu/vivid/arm64 (1 more)    | 4f4129a1a9aa | yes    | Ubuntu vivid (arm64)    | aarch64 | 60.84MB  | Jun 4, 2015 at 7:45am (BST)   |
| ubuntu/vivid/armhf (1 more)    | abfcce5f34da | yes    | Ubuntu vivid (armhf)    | armv7l  | 61.12MB  | Oct 30, 2015 at 5:24am (GMT)  |
| ubuntu/vivid/i386 (1 more)     | e347b9d05108 | yes    | Ubuntu vivid (i386)     | i686    | 64.56MB  | Oct 30, 2015 at 4:38am (GMT)  |
| ubuntu/vivid/ppc64el (1 more)  | 76bc80391592 | yes    | Ubuntu vivid (ppc64el)  | ppc64le | 63.30MB  | Oct 30, 2015 at 4:42am (GMT)  |
| ubuntu/wily/amd64 (1 more)     | fcca6699a4a9 | yes    | Ubuntu wily (amd64)     | x86_64  | 72.55MB  | Oct 30, 2015 at 4:46am (GMT)  |
| ubuntu/wily/arm64 (1 more)     | ebb96aeaaff0 | yes    | Ubuntu wily (arm64)     | aarch64 | 61.05MB  | Jun 4, 2015 at 7:52am (BST)   |
| ubuntu/wily/armhf (1 more)     | 21adcf26070c | yes    | Ubuntu wily (armhf)     | armv7l  | 71.35MB  | Oct 30, 2015 at 6:21am (GMT)  |
| ubuntu/wily/i386 (1 more)      | 24dfdf3d0d6a | yes    | Ubuntu wily (i386)      | i686    | 72.04MB  | Oct 30, 2015 at 4:49am (GMT)  |
| ubuntu/wily/ppc64el (1 more)   | b7a3d7758dba | yes    | Ubuntu wily (ppc64el)   | ppc64le | 72.20MB  | Oct 30, 2015 at 4:53am (GMT)  |
+--------------------------------+--------------+--------+-------------------------+---------+----------+-------------------------------+

You can now choose to either copy (import) the image(s) first, or you can simply use the launch command to copy the image and create your new container in one go.

To copy an image to your local system:

lxc image copy images:/ubuntu/trusty/amd64 local: --alias=trusty-amd64

Notice how to set the local name of the image using –alias.

Once the image has finished copying, to view your local images:

lxc image list
+-----------------+--------------+--------+------------------------------------+--------+----------+-------------------------------+
|      ALIAS      | FINGERPRINT  | PUBLIC |            DESCRIPTION             |  ARCH  |   SIZE   |          UPLOAD DATE          |
+-----------------+--------------+--------+------------------------------------+--------+----------+-------------------------------+
| trusty-amd64    | e6106a320067 | no     | Ubuntu trusty (amd64)              | x86_64 | 63.84MB  | Oct 30, 2015 at 9:57am (GMT)  |
+-----------------+--------------+--------+------------------------------------+--------+----------+-------------------------------+

To change the alias of an image, use the fingerprint of the image:

lxc image alias create trusty e6106a320067
lxc image alias delete trusty-amd64
lxx image list

You should now see the following output:

+-----------------+--------------+--------+------------------------------------+--------+----------+-------------------------------+
|      ALIAS      | FINGERPRINT  | PUBLIC |            DESCRIPTION             |  ARCH  |   SIZE   |          UPLOAD DATE          |
+-----------------+--------------+--------+------------------------------------+--------+----------+-------------------------------+
| trusty          | e6106a320067 | no     | Ubuntu trusty (amd64)              | x86_64 | 63.84MB  | Oct 30, 2015 at 9:57am (GMT)  |
+-----------------+--------------+--------+------------------------------------+--------+----------+-------------------------------+

Now we have a local image imported, we are ready to create our first container. It is also possible to do this by providing the path to the remote image if you have not copied it first.

lxc launch trusty firstcontainer

This process takes around a second to complete on my laptop. Once complete, you may use the following command to check that the container is indeed running:

lxc list
+----------------+---------+------------+------+-----------+-----------+
|      NAME      |  STATE  |    IPV4    | IPV6 | EPHEMERAL | SNAPSHOTS |
+----------------+---------+------------+------+-----------+-----------+
| firstcontainer | RUNNING | 10.0.3.253 |      | NO        | 0         |
+----------------+---------+------------+------+-----------+-----------+

Access the container with the following command:

lxc exec firstcontainer /bin/bash

This is how to access containers from the host machine, not SSH. You should now be presented with a root prompt:

root@firstcontainer:~#

Type exit at the prompt to return to the command line on the host machine.

To stop the container use:

lxc stop firstcontainer

To start the container:

lxc start firstcontainer

If you shutdown your host machine without first stopping any running containers, LXD will automatically start them again when the host machine is brought back up.

Configure the container

Since I’m not using Vagrant with LXD yet, there’s a few manual configuration steps to replicate functionality that Vagrant usually handles for us. This will also be the first steps to us use the container with a Bedrock/Trellis project.

Synced folders

Vagrant has "synced folders" which shares hosts files to the guest VM via NFS. LXD allows us to mount directories from the host inside our container, which means that we don’t need to use NFS. To make sure that the container can write to the host directory, we need to make the directory world writeable on the host. Issue the following command, substituting the path with the actual path to your Bedrock/Trellis project on the host machine.

sudo chmod -R go+w /home/user/sites/example.com/site

I find that this command will not correctly set permissions of the hidden dot files. Make sure .env in particular is world writeable before continuing. Then run the following command to mount the directory within the container, substituting the paths with the correct paths for your project:

lxc config device add firstcontainer webroot disk path=/srv/www/example.com/current source=/home/user/sites/example.com/site

Hosts file

Vagrant has plugins like vagrant-hostsupdater which automatically updates hosts files for you. Our next task is to do this manually by adding an entry to your host machine’s /etc/hosts file. First, start your container if it is not already running:

lxc start firstcontainer

Then on the host machine run:

sudo nano /etc/hosts

Add the development domain name for your project (example.dev) and use the IP address used by your container, which you can see by running:

lxc list

On the host machine. Which in my case gives the following output:

+----------------+---------+------------+------+-----------+-----------+
|      NAME      |  STATE  |    IPV4    | IPV6 | EPHEMERAL | SNAPSHOTS |
+----------------+---------+------------+------+-----------+-----------+
| firstcontainer | RUNNING | 10.0.3.253 |      | NO        | 0         |
+----------------+---------+------------+------+-----------+-----------+

So I used 10.0.3.253 as the IP for example.dev in my /etc/hosts file.

SSH keys

Another thing Vagrant automatically does is give you easy SSH access via your SSH key. Our next step is to add your SSH key to the root account inside the container.

You can find your key at ~/.ssh/id_rsa.pub on the host machine. Once the key has been added to the /root/.ssh/authorized_keys file inside your container you are ready to provision the box using Ansible, just like you would for staging or production.

Using Ansible with your container

With Trellis, Vagrant automatically provisions the VM by running Ansible. This is another step we’ll need to manually replicate.

To configure Trellis, go to the ansible directory inside your Trellis project, and add the development domain name (example.dev) to the hosts/development file.

Then, open up group_vars/all/main.yml and make sure that admin_user is set to 'root'.

You should now be ready to start the provisioning process by running the following command:

ansible-playbook -i hosts/development server.yml

I find I need to run the script at least twice to get it to work. For some reason swapon always fails first time but is fine second time.

All being well, you should now be able to work on your local dev site using LXC/LXD instead of VirtualBox. Using this example, you should be able to view the site at http://example.dev.

You can see that there’s a big chunk of extra work since I’m not using Vagrant. My next step is to figure out how to integrate LXD with Vagrant to make this process even easier.

Stay tuned for updates and any help will be gratefully received.