Assigning IP addresses to Docker containers via DHCP

Warning
This guide is based on an old version of Docker. The instructions you find below may be already removed or changed in the Docker codebase.

In my last blog post I explained how to run a Docker installation across multiple hosts. In the comments I was asked if it would be possible to use a DHCP server to assign IPs to the containers. I thought immediately — why not? In fact, assigning IPs using DHCP is a nice way to overcome the IP assignment issue I talked about before.

Set up

The set up is pretty similar to my earlier work.

The difference is that I want to run a DHCP server on each of the hosts. It will be responsible for assigning the IPs to all of the containers connected to the docker0 bridge on that host.

You may ask why I want to run so many DHCP servers? Of course one server would do the job of assigning the IP’s (in fact, this was my initial set-up). Then I realized that it would be easier to manage routing of the container’s traffic if we have a DHCP server on every host.

Since we want to have the containers run on the same network, there will be many DHCP servers working on that network. The trick is to reserve a bunch of IP’s for each host so as not to interfere with other hosts (in my case it’s 10 addresses per host) and drop the DHCP requests on each br0 Open vSwitch bridge.

New scripts

Additionally I rewrote my earlier script that prepares the virtual machines for me. I decided to drop the use of cloud-init entirely in favor of libguestfs — a great Swiss Army knife written by Rich. If you’re not familiar with it — it’s a tool for offline manipulation of disk images. And it does the job damn well.

You can install libguestfs tools on your system by running this command:

yum -y install libguestfs-tools

Install liguestfs-tools before you use my scripts.

Note
The first run of guestfish can take a bit, since it creates the supermin appliance. Also, if you run it in a virtualized environment it will perform a bit worse compared to bare metal.

The new scripts are avilable in the docker-dhcp repository on GitHub.

The logic of preparing and registering the image as a libvirt domain was split into two scripts:

The split made it possible to create the image once and create as many domains as you want in seconds.

I placed a lot of comments in these scripts, so I hope everything is self-explaining. Let me know if that’s not the case!

Base image

To create a VM with everything preinstalled to make the setup easier the only thing you need to do is to download the QCOW2 image from cloud.fedoraproject.org and run the script providing the cloud image as the parameter, like this:

$ ./prepare-image.sh Fedora-x86_64-20-20131211.1-sda.qcow2
Wed, 29 Jan 2014 12:20:57 +0100 Cleaniung up...
Wed, 29 Jan 2014 12:20:58 +0100 Modifying the image...
Wed, 29 Jan 2014 12:25:15 +0100 Resizing the disk...
Wed, 29 Jan 2014 12:26:06 +0100 Image 'image.qcow2' created!
Note
Executing the above script can take some time. It took about 5 minutes for me to prepare the image. Please keep in mind that it does the full system update and it installs some additional software as well.

This script injects the network.sh script which will be used later to configure the network inside the hosts.

Host domains

Now we have the base image prepared: image.qcow2. Time to make use of it and register two domains based on it. For this purpose I use the register-domain.sh script:

$ ./register-domain.sh image.qcow2 host1
Wed, 29 Jan 2014 13:52:51 +0100 Installing the domain and adjusting the configuration...
Wed, 29 Jan 2014 13:52:51 +0100 Domain host1 registered!
Wed, 29 Jan 2014 13:52:51 +0100 Launching the host1 domain...
Wed, 29 Jan 2014 13:52:52 +0100 Domain started, waiting for the IP...
Wed, 29 Jan 2014 13:53:30 +0100 You can ssh to the 192.168.122.144 host using 'fedora' username and 'fedora' password or use the 'virsh console host1' command to directly attach to the console

You can log-in using the fedora / fedora credentials.

Note
If you don’t want to run the domain immediately after creation — use the RUN_AFTER environment variable and set it to false.

Run the register-domain.sh script twice with host1 and host2 as the arguments.

DHCP configuration explained

The DHCP server will be run on every host and listen only for requests on the docker0 interface since it’s configured to look for the 172.16.42.0/24 network only. The first 9 IP addresses from this network are reserved (can be assigned manually to additional hosts), the rest will be available to the DHCP clients.

Every DHCP server will only be responsible for a part of the network. For example, the server on host1 will assign addresses from 172.16.42.10 to 172.16.42.19, whereas host2 will will assign addresses from 172.16.42.20 to 172.16.42.29.

Note
This is just an example — you can expand the default values to run more than 10 containers on one host.

The host1 docker0 network interface will have the 172.16.42.1 address assigned, host2 will have 172.16.42.2, and so on.

Make it work!

I assume that we have already started host1 and host2 as explained above.

Networking

Now it’s time to prepare the networking on both hosts. Log-in to both hosts and get the IP addresses of the eth0 network interfaces. Now run the network.sh script (it’s located in the /home/fedora directory) on both hosts.

Note
I assume that host1 has 192.168.122.31 IP and host2 has 192.168.122.2.

On host1 run the script with the IP of host2:

$ sudo ./network.sh 1 192.168.122.2

And do the opposite on host2:

$ sudo ./network.sh 2 192.168.122.31
Note
When you run the network.sh script for the first time, you may see messages similar to bridge docker0 doesn’t exist — don’t worry, this is normal.

The GRE tunnel should now be established and a DHCP server should be running on host1. You can confirm this by pinging the docker0 bridge addresses on each host.

Containers

There is one requirement for the container image — it needs to have a DHCP client installed. Sadly the default fedora image does not have the dhclient package installed. To make things easy I prepared the goldmann/fedora-dhcp image. The only difference between fedora image is the addition of dhclient.

Download this image on both hosts:

docker pull goldmann/fedora-dhcp

If you run the goldmann/fedora-dhcp image you’ll see that there is no network interfaces beside the loopback. This is because Docker is run with the -b=none flag and it does not know about any network interfaces to bind to, so it does not create the ethernet adapter in the container.

But we still want to have networking. The only option at the moment is to use the -lxc-conf flag when running the image, like this:

docker run -i -t \
-lxc-conf="lxc.network.type = veth" \
-lxc-conf="lxc.network.link = docker0" \
-lxc-conf="lxc.network.flags = up" \
goldmann/fedora-dhcp /bin/bash

This will start a new container with a virtual ethernet adapter which is attached to the docker0 bridge. Sweet!

Obtaining the IP address

Since the Docker container does not run anything besides the command you specify (in our case /bin/bash) — it does not run the scripts that configures the network too. We need to do it by hand.

Note
I hope this will change in the near future. One option is to make systemd run well in the Docker containers.

After you get the prompt from the container, you can simply run the dhclient command. This will obtain the address from the DHCP server, exit and leave a shell just for you.

bash-4.2# ip a s dev eth0
17: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 1e:d4:13:c7:9d:fd brd ff:ff:ff:ff:ff:ff
    inet6 fe80::1cd4:13ff:fec7:9dfd/64 scope link
       valid_lft forever preferred_lft forever
bash-4.2# dhclient
bash-4.2# ip a s dev eth0
17: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 1e:d4:13:c7:9d:fd brd ff:ff:ff:ff:ff:ff
    inet 172.16.42.14/24 brd 172.16.42.255 scope global dynamic eth0
       valid_lft 43197sec preferred_lft 43197sec
    inet6 fe80::1cd4:13ff:fec7:9dfd/64 scope link
       valid_lft forever preferred_lft forever
bash-4.2# ping -c 1 google.com
PING google.com (173.194.65.139) 56(84) bytes of data.
64 bytes from ee-in-f139.1e100.net (173.194.65.139): icmp_seq=1 ttl=39 time=55.6 ms

--- google.com ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 55.672/55.672/55.672/0.000 ms
Note
You can also use the /etc/init.d/network restart command to obtain the IP.

You can (should!) try it on host1 and host2. You should get the same result with no IP conflict and be able to access the Internet as well as other containers on the network.

Enjoy!

Things to improve

There are of course some things that could be improved to make this setup easier.

  1. Make systemd available in the container — this would boot the networking and get the IP address automatically for us.

  2. Stop Docker from (blindly) assigning IP addresses when we specify the -b=BRIDGE flag. Docker currently assumes that it manages the container network and nothing else is allowed to do so. I hope this will change in the future.

Creative Commons License