Vagrant vs. Docker

Updated 2022-09-18 to reflect that Docker for Desktop and Virtualbox can co-exist on Windows, because Hyper-V is no longer a requirement for Docker for Desktop.

Vagrant is a tool that automatically downloads and configures virtual machines, according to a recipe file. Docker does something similar, but can achieve better performance in specific situations. This article explains the technological differences of both tools, and recommends when to choose Vagrant vs. Docker. It also discusses three different ways how Vagrant and Docker can be combined.

Introduction

Vagrant is an automation tool for creating and preparing virtual machines (VMs). Please read my other article, The complete introduction to Vagrant, for a complete introduction to Vagrant. In this article, which is part 3 of my Vagrant series, I compare Vagrant vs. Docker, help you decide which one to pick, and how they can be combined. I’ll assume that you are already familiar with Docker, at least on a basic level.

Let’s take a brief look at the technological basis for each tool, starting with Docker. On Linux hosts, Docker supports running Linux-based Docker containers natively, without virtualization. Natively means that applications run inside the container (guest) use the host’s kernel directly. The performance is great, because no separate operating system (OS) needs to be started, and there are no inefficiencies caused by virtualization. On a Windows host, Docker Desktop for Windows can run Windows-based Docker images (see here for a list) with Hyper-V. To run Linux-based Docker images, Docker uses Windows’ WSL2 (Windows Subsystem for Linux) by default, or alternatively the Hyper-V hypervisor. On a macOS host, Docker Desktop for Mac uses hyperkit, a lightweight hypervisor, to run Linux-based Docker images in a hyperkit VM. The table below illustrates the compatibility between host and guest OSes graphically:

Windows guestLinux guestmacOS guest
Windows hostyes
Hyper-V
yes
Virtualized using WSL2 or Hyper-V
no
Linux hostnoyes
Native support
no
macOS hostnoyes
Virtualized using hyperkit
no
Docker Desktop compatibility between host and guest OS

In contrast, the table for Vagrant would look somewhat different. Vagrant always uses virtualization, thus, every cell would have a yes , with the downside that a hypervisor (or “provider”, in Vagrant-speak) is always involved.

Before I get into the details about when to use Docker vs. Vagrant (or both), I’d like to present a mapping between the concepts and commands of both worlds:

Docker worldVagrant world
Dockerfile contains instructions to build a Docker image;
docker-compose.yml composes multiple Dockerized containers.
Vagrantfile contains instructions how to build a VM from a Vagrant box. It is possible to define multiple boxes, similar to docker-compose.yml.
Docker Hub: registry for Docker imagesVagrant Cloud: registry for Vagrant boxes for many hypervisors / providers
Docker imageVagrant box: virtual disk image + meta-data. Vagrant boxes are immutable. VMs are instantiated from the boxes.
Docker containerVM
docker-compose up/stop -dvagrant up/halt
docker rm <container name/id>vagrant destroy deletes the VM, in particular its meta-data and virtual disk image.
docker commit creates an image from a containervagrant package creates an immutable box from a mutable VM
docker ps lists all running containersvagrant global-status lists all running VMs

For the remainder of this article I’ll focus on using Docker and/or Vagrant on a desktop machine.

Use cases – Vagrant vs. Docker

Let’s take a look when to use either tool.

Vagrant

Vagrant is a good choice for a few specific use cases:

  • You need to run a host + guest OS combination not supported by Docker.
  • You need to run graphical applications in the guest. Docker is not meant for this use case, but should be used for software with a command line interface. There are some efforts, though, to make graphical Linux applications possible, as long as they run in a Linux Docker container. However, this requires an additional X window system server run on the host. See here or here for more information. Microsoft has also added support for Linux GUI applications to WSL 2, see here.
  • You need to prepare virtual disk images, e.g. because your IT department already runs a VM-based infrastructure that only accepts virtual disk images.

Docker

In all other cases I recommend to use Docker, given that your machine fulfills Docker’s requirements. The run-time and boot performance for the guest systems is much faster. Sure, on a Linux or macOS host system, Docker can only run Linux containers. But on recent Windows 10 versions, Docker Desktop for Windows can run both Windows-based Docker containers and Linux containers (but not both at the same time!). Docker Desktop has improved since its initial release in 2016. So if you’ve had a bad time using it during 2016-2018, it might be a good idea to try it again!

Combining Vagrant with Docker

There are several scenarios how Vagrant and Docker can be combined.

Scenario #1: Run Vagrant + Docker side by side

The idea is to build hybrid setups, where some services run in Docker containers, while others (not supported by Docker) run in a VM, configured by Vagrant. This way, you get the best possible performance. The disadvantage is that you have to configure the networking, so that Dockerized applications can connect to virtualized applications and vice versa. The basic approach is that you make the network TCP port(s) of service #1 available on the host, and figure out a way how a Dockerized/virtualized service #2 can access such host ports from within the guest. Achieving the former (exposing service ports on the host) is easy, using built-in features (Docker: publish a port; Vagrant: forward a port). To access host ports from the guest, see here (Linux-only) or here (other host OSes) for Docker, and here for Vagrant.

Scenario #2: Use Docker to provision software

Vagrant supports Docker as provisioner. With just a few line in your Vagrantfile you can instruct Vagrant to install Docker into a Linux-based VM, and run specific Docker images in it. Normally you wouldn’t need to do this, because you could just use Docker (for Desktop) on the host directly, since Docker can run Linux-based containers on every host OS anyway (Linux, macOS, Windows).

However, there is one issue with using Docker directly. As explained above, Docker containers may run in a VM, e.g. on macOS hosts. All Docker containers run in a single VM, which may not be what you want. For instance, the fixed resource allocation (for the number of CPUs and virtual RAM) of that VM does not care whether you are running 1 or 20 Docker containers. This means that you’d typically over-allocate resources for the VM.

Using Vagrant, you can instead create several VMs, and distribute Docker containers on those VMs as you see fit. The downside of this approach is reduced performance and increased complexity for setting up networking and Docker volumes, because you need to take additional care of Vagrant’s port forwarding and synced folders to be able to access data/ports from the host. For instance, you’ll need to use Vagrant’s shared folder mechanism to map a host folder into the VM, and in the VM you need to instruct Docker to bind that folder into the container. This is similar to the Inception movie’s dream inside a dream inside a dream ;).

If you still choose to use this approach, you should know that Vagrant supports Docker out of the box, but not docker-compose! However, there is a plug-in whose documentation is very solid and concise.

With the plug-in being installed, let’s look at a basic example:

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/bionic64"

  config.vm.provision :docker
  config.vm.provision :docker_compose, yml: "/vagrant/docker-compose.yml", run: "always"
end
Code language: Ruby (ruby)

Line 4 installs Docker, on the first start of the VM. Line 5 downloads and installs the docker-compose binary and runs

docker-compose -f /vagrant/docker-compose.yml up -d

"run: always" means that this provision command is always run whenever you vagrant up, not just on the very first boot. The docker-compose.yml file needs to be placed next to your Vagrantfile on your host. By convention, Vagrant VMs are set up such that the directory on the host that contains the Vagrantfile is mapped to /vagrant in the VM, using the synced folder mechanism.

Scenario #3: Use Docker as provider

Vagrant integrates with Docker in one more way: using Docker as provider. Here, Vagrant converts your Vagrantfile instructions to Docker commands. Vagrant uses docker (which must also be installed on the host) to start containers instead of VMs, which offers better performance (on Linux hosts!). Forwarded Vagrant ports become published Docker ports, synced Vagrant folders become Docker volumes, and so on. I’ve never seen a case where you’d need this, given that I’d just use Docker directly.

Conclusion

Both Vagrant and Docker are tools that let you run applications in a defined environment. Because Docker only allows for a limited number of host and guest/container OS combinations, Vagrant can be used, because every host/guest OS combination is supported. The price to pay is slower performance, due to virtualization. However, Docker and Vagrant can be combined in several ways, to get the best out of both worlds.

Leave a Comment