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.
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 guest||Linux guest||macOS guest|
Virtualized using WSL2 or Hyper-V
Virtualized using hyperkit
In contrast, the table for Vagrant would look somewhat different. Vagrant always uses virtualization, thus, every cell would have a , 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 world||Vagrant 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 images||Vagrant Cloud: registry for Vagrant boxes for many hypervisors / providers|
|Docker image||Vagrant box: virtual disk image + meta-data. Vagrant boxes are immutable. VMs are instantiated from the boxes.|
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 is a good choice for a few specific use cases:
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" endCode 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.
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.