Scott's Weblog The weblog of an IT pro specializing in virtualization, networking, open source, and cloud computing

Using Docker with Vagrant

As part of my ongoing effort to create tools to assist others in learning some of the new technologies out there, I spent a bit of time today working through the use of Docker with Vagrant. Neither of these technologies should be new to my readers; I’ve already provided quick introductory posts to both (see here and here). However, using these two together may provide a real benefit for users who are new to either technology, so I’d like to take a bit and show you how to use Docker with Vagrant.

Background

Vagrant first started shipping with a Docker provider as part of the core product in version 1.6 (recall that Vagrant uses the concept of providers to support multiple backend virtualization solutions). Therefore, if you’ve installed any recent version of Vagrant, you already have the Docker provider as part of your Vagrant installation.

However, while you may have the Docker provider as part of Vagrant, you still need Docker itself (just like if you have the VMware provider for Vagrant, you still need the appropriate VMware product—VMware Fusion on the Mac or VMware Workstation on Windows/Linux) in order to provide the functionality Vagrant will consume. This presents a bit of a unique challenge because not all platforms support Docker. Unless you’re running Vagrant directly on Linux, you’re going to need an installation of Linux to support running the Docker daemon. The Docker community solved this problem using boot2docker, which is essentially a super-lightweight Linux instance running the Docker daemon, and provided some simple installers to install VirtualBox and this boot2docker VM onto systems running OS X and Windows so that users could more easily use/experiment with Docker.

“OK,” you might reply. “But what does that have to do with using Docker with Vagrant?”

Vagrant’s creator, Mitchell Hashimoto, took the boot2docker VM and created a Vagrant box (recall that Vagrant uses the concept of boxes, which are essentially VM templates created for each back-end provider) with support for the VirtualBox provider as well as the VMware provider. Vagrant’s Docker provider, by default, uses this boot2docker box as the target for its Docker functionality when you are running Vagrant on anything other than Linux.

I took the time to explain all of this because as you move into the next section where I show you how to set up some Vagrantfiles to work with Docker under Vagrant, you’ll understand why things work the way they do.

Setting Up Docker With Vagrant

Based on this background information, you should understand now that using Docker with Vagrant involves 4 different components in addition to Vagrant itself:

  1. The Docker provider for Vagrant
  2. Some sort of Linux instance on which to run the Docker daemon
  3. A virtualization platform, such as VirtualBox, VMware Fusion, or VMware Workstation
  4. The appropriate Vagrant provider for your virtualization platform

The Docker provider is installed when you install Vagrant (any release since 1.6), and I’m going to assume here that you’ve already installed the appropriate virtualization platform and provider (I’m using VMware Fusion 6.0.5 on OS X 10.9.5 with the Vagrant VMware plugin). All that’s left to discuss then, is the Linux instance for the Docker daemon and putting together the appropriate Vagrantfiles to make this all work as expected.

Specifying the Linux Instance for Docker

I mentioned earlier that Mitchell Hashimoto, the creator of Vagrant, took the boot2docker VM and created a Vagrant box from it. You can add that Vagrant box to your system by simply running vagrant box add mitchellh/boot2docker. Vagrant will prompt for the provider, and then download the appropriate version of the box for the selected provider. Then, unless you tell it otherwise, Vagrant will spin up an instance of this boot2docker box anytime it needs to perform Docker provider operations. If you haven’t already added the Vagrant box (using the command above) the first time you use the Docker provider, it will download the box automatically.

But what if you don’t want to use boot2docker? What if you’d rather use Ubuntu or CentOS, so that what you do in Vagrant more closely matches what you might do in the data center?

No problem—you can change this behavior by adding a line in the Vagrantfile that spins up your Docker containers. The specific line would look something like this (I’ll provide more concrete examples later in the post):

docker.vagrant_vagrantfile = "path/to/host/VM/Vagrantfile"

That’s right: all you have to do is create a Vagrantfile that spins up a VM running the Linux distribution of your choice, and then reference that Vagrantfile in the one that creates your Docker containers. Of course, since Vagrant expects the filename to be Vagrantfile, that means the file defining the host VM must be in a different directory than the file defining the Docker containers. Hence, you need to specify the path to the host VM Vagrantfile. (There might be a way to combine the files, but I haven’t figured out how.)

Although Vagrant expects Docker to be running inside this host VM, it does provide a way to simplify that by allowing you to provision (install) Docker into the VM when you run vagrant up (much in the same way Vagrant can provision other things into a VM when it is first instantiated). This is done with a simple command in the host VM Vagrantfile:

config.vm.provision "docker"

Pretty straightforward, right? Let’s take a look at a full example of a Vagrantfile that could be used to define a host VM for the Vagrant Docker provider:

# -*- mode: ruby -*-
# vi: set ft=ruby :

# Specify Vagrant version and Vagrant API version
Vagrant.require_version ">= 1.6.0"
VAGRANTFILE_API_VERSION = "2"

# Create and configure the VM(s)
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|

  # Assign a friendly name to this host VM
  config.vm.hostname = "docker-host"

  # Skip checking for an updated Vagrant box
  config.vm.box_check_update = false

  # Always use Vagrant's default insecure key
  config.ssh.insert_key = false

  # Spin up a "host box" for use with the Docker provider
  # and then provision it with Docker
  config.vm.box = "slowe/ubuntu-trusty-x64"
  config.vm.provision "docker"

  # Disable synced folders (prevents an NFS error on "vagrant up")
  config.vm.synced_folder ".", "/vagrant", disabled: true
end

Most of this Vagrantfile is pretty simple—define a VM, specify the box, disable checking for new versions of the box, etc. A couple of things I want to note:

  • First, note the config.vm.synced_folders command—during my testing I found that if you did not disable synced folders, Vagrant would halt with an NFS error. The only workaround that I found was to disable synced folders both at the host VM and at the Docker container level.
  • Second, note the aforementioned config.vm.provision command. This expects that the VM created by Vagrant will have Internet access; if it doesn’t, it will fail. Please plan accordingly.

You’ll note that this box uses my Ubuntu 14.04 base box for the vmware_desktop provider (works with both Fusion and Workstation), but you could use just about any appropriate Linux box here. (I’m just trying to make it as easy as possible for folks to try this out themselves, so feel free to use my Ubuntu 14.04 base box.)

I like to store this Vagrantfile in a subdirectory of the main project directory. So, if I was storing my Vagrant project in, say, /Users/slowe/Projects/vagrant-docker, then I might put the Vagrantfile that defined the host VM in /Users/slowe/Projects/vagrant-docker/host or similar. Obviously, this is completely up to you; you just need to know the path and specify it in the file that defines your containers.

Specifying your Containers

And speaking of defining your containers…that’s the next topic to discuss. Here’s a sample Vagrantfile that specifies the host VM and builds a single Nginx container:

# -*- mode: ruby -*-
# vi: set ft=ruby :

# Specify Vagrant version and Vagrant API version
Vagrant.require_version ">= 1.6.0"
VAGRANTFILE_API_VERSION = "2"
ENV['VAGRANT_DEFAULT_PROVIDER'] = 'docker'

# Create and configure the Docker container(s)
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|

  # Disable synced folders for the Docker container
  # (prevents an NFS error on "vagrant up")
  config.vm.synced_folder ".", "/vagrant", disabled: true

  # Configure the Docker provider for Vagrant
  config.vm.provider "docker" do |docker|

    # Define the location of the Vagrantfile for the host VM
    # Comment out this line to use default host VM that is
    # based on boot2docker
    docker.vagrant_vagrantfile = "host/Vagrantfile"

    # Specify the Docker image to use
    docker.image = "nginx"

    # Specify port mappings
    # If omitted, no ports are mapped!
    docker.ports = ['80:80', '443:443']

    # Specify a friendly name for the Docker container
    docker.name = 'nginx-container'
  end
end

As with the Vagrantfile that specifies the host VM, this is reasonably straightforward once you’ve worked with Vagrant a little while. This example Vagrantfile disables synced folders, specifies the path to the Vagrantfile defining the host VM (in this case, that file resides in a subdirectory named host), provides the name of the Docker image to use, the ports to map, and a user-friendly name. Note that if you omit the docker.ports statement no ports will be mapped.

The techniques I’ve already shared with you regarding the use of YAML and an external data file could certainly be applied here to create a multi-container Vagrantfile (see this post for more details).

Bringing Up the Environment

As with most uses of Vagrant, all you need to do is run vagrant up from the main directory where the Vagrantfile that defines your containers is located. The main Vagrantfile includes a reference to the Vagrantfile defining your host VM, so Vagrant will automatically turn up the VM (if needed), install Docker (if this host VM is being provisioned for the first time), and create the Docker containers. Once it’s done, you’re ready to roll!

When you go to turn down the environment, it’s a bit trickier. The vagrant halt and vagrant destroy commands will only affect the Docker containers; you’ll have to use the vagrant global-status command to get a reference for the host VM so that you can halt or destroy it separately.

Additional Notes and Resources

I performed my testing using Vagrant 1.7.2, running with the Vagrant VMware plugin against VMware Fusion 6.0.5 on OS X 10.9.5. If you use different versions or different platforms, the specifics might be slightly different (but should not be dramatically different).

I found this post by Nick Weaver to be very helpful as well, providing some additional details and examples that fleshed out the Vagrant documentation.

Finally, I posted resources to help you follow along with this blog post in my “learning-tools” GitHub repository; feel free to clone or download the repository to help with your own studies.

Be social and share this post!