🧠 Introduction
"What if you could magically spin up as many machines as you want for your test environments? No wand, just Vagrant."
Test environments can quickly become a nightmare: bad installs, version conflicts, etc. What if we could change that with a tool that makes you feel like a DevOps wizard? 🎩✨
In this article, we will:
Discover what Vagrant is (without falling asleep) 😴❌
Use it to create two Ubuntu VMs 🖥️🖥️ and prepare a test environment for our series on incremental backups with Postgres + Barman
And test our setup like pros 🧪
🪄 What is Vagrant?
"Vagrant is kind of like Docker, but for VMs. It's your conductor for VirtualBox, VMware, Hyper-V, and other hypervisors. It's a super reliable tool to manage complete environments with real virtual machines in a simple and automated way."
It's a tool developed by Hashicorp that makes it easy to manage VMs with fully isolated dependencies and configurations, all in a unique and disposable environment.
You write a Vagrantfile
, type vagrant up
, and boom 💥: your machine(s) is(are) ready to use. No need to click through 27 buttons in VirtualBox or fight with Hyper-V.
Vagrant saves you from:
❌ Downloading an ISO
❌ Clicking through the installer manually
❌ Struggling with networking and scripts
✅ You can do everything automatically, and reset your environment anytime, like a quick save in your dev life.
🔍 Why is it useful?
🔄 Easy testing: want to test a Postgres config? SSH? A cluster? A network lab? Boom, spin up a VM.
💥 Safe: easily prepare demos or tutorials without polluting your main machine. If you break everything, just run
vagrant destroy && vagrant up
.🤝 Shareable: working in a team? Commit your
Vagrantfile
and everyone gets the same environment.🧪 Closer to production: you can create environments that really mimic a real server (unlike Docker). Perfect for learning sysadmin or DevOps.
🛠️ How does it work?
Vagrant relies on:
A provider (usually VirtualBox or Hyper-V) to run the machines
Boxes: pre-built system images deployable in any Vagrant environment. They're essentially packaged distributions for uniform deployment. You can explore official boxes here
A Vagrantfile (your recipe): the file where the magic is defined
Scripts or tools to provision the machine (install software, configure base services)
Vagrant will provision the machine based on what's described in the Vagrantfile (usually in the project root). A Vagrantfile is a Ruby file, but it doesn’t require deep Ruby knowledge. It defines the machine specs (storage, RAM, network, etc.) and sets up the required tools for it to run properly.
🎓 Getting Started with Vagrant
Next, we’ll get hands-on with Vagrant and set up a lab for our incremental backup series with Postgres.
In the end, we’ll boot two VMs: one with Postgres, and another with Barman.
🚀 Installing Vagrant
You can install Vagrant via the CLI or by using an executable from the official documentation.
You’ll also need a working VirtualBox setup, which is the provider we’ll use.
🛠️ Using Vagrant
Let's create our first Vagrantfile
with the minimal config to run a VM. Create a folder and inside it, a Vagrantfile
like this:
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/focal64"
config.vm.hostname = "vagrantbox"
end
Vagrant provides a wide variety of commands to manage the different components of the Vagrant ecosystem. For example, vagrant box list
and vagrant box add box_name
respectively list the boxes available locally and add a box to the local collection.
$ vagrant box add ubuntu/focal64
==> box: Loading metadata for box 'ubuntu/focal64'
box: URL: https://vagrantcloud.com/api/v2/vagrant/ubuntu/focal64
This box can work with multiple providers! The providers that it
can work with are listed below. Please review the list and choose
the provider you will be working with.
1) hyperv
2) libvirt
3) virtualbox
4) vmware_desktop
Enter your choice: 3
==> box: Adding box 'ubuntu/focal64' (v2004.01) for provider: virtualbox
box: Downloading: https://vagrantcloud.com/ubuntu/boxes/focal64/versions/20240821.0.1/providers/virtualbox/unknown/vagrant.box
$ vagrant box list
ubuntu/bionic64 (virtualbox, 20230607.0.0)
ubuntu/focal64 (virtualbox, 20240220.0.0)
More info on box management here.
Next, let's verify that the syntax of the file is correct using vagrant validate
.
If there are no errors, you will see the following message: Vagrantfile validated successfully.
Then launch your VM:
$ vagrant up
Bringing machine 'default' up with 'virtualbox' provider...
==> default: Importing base box 'ubuntu/focal64'...
==> default: Matching MAC address for NAT networking...
==> default: Checking if box 'ubuntu/focal64' version '20240220.0.0' is up to date...
==> default: A newer version of the box 'ubuntu/focal64' for provider 'virtualbox' is
==> default: available! You currently have version '20240220.0.0'. The latest is version
==> default: '20240821.0.1'. Run `vagrant box update` to update.
==> default: Setting the name of the VM: ocr_default_1749933808776_2967
==> default: Clearing any previously set network interfaces...
[...]
$ vagrant status
Current machine states:
default running (virtualbox)
[...]Check the status:
Once the VM is up and running, we can connect to it via SSH to perform various operations using the command vagrant ssh machine_name
.
Vagrant can also be effectively used in a context that requires a multi-tier architecture — i.e., multiple machines (sometimes with different operating systems) at once, as is often the case in production. You simply need to define as many VM configurations as needed.
Let’s now connect to the machine we’ve prepared so far.
$ vagrant ssh default
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-172-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/pro
[...]
vagrant@vagrantbox:~$
The diagram below illustrates the typical lifecycle of a Vagrant virtual machine, from initialization to complete removal.
Main Steps
vagrant init
creates a Vagrantfile-
Configure the Vagrantfile
This is an important step in setting up the
Vagrantfile
.
Here, we define everything needed to prepare the machine (provisioning and more). Then, using thevagrant up
command, we start the virtual machine. If it doesn’t exist yet, it will be created and provisioned. Running
The machine is now running. From this point on, you have several options available to you:
* `vagrant reload` : restart the VM with the updated config
* `vagrant suspend` / `vagrant resume` : to suspend and resume the VM
* `vagrant halt` : to shut down the VM properly
* `vagrant provision` : rerun the provisioning scripts
-
Destroy
Withvagrant destroy
, the VM is completely removed. You then return to a clean state.
🚀 Provisioning Vagrant VMs
Now that our base environment is defined in the Vagrantfile, it's time to prepare each machine so they are ready to use that is, install everything necessary to operate them. This is the provisioning step.
Vagrant provides several interfaces to provision our VMs: shell, file, Chef, Docker, Ansible, etc.
Next, we will explore some of these provisioners and prepare what’s needed to provision our VMs for the Postgres and Vagrant lab.
1. Shell Provisioner
It allows you to specify a series of instructions to execute in order to provision the machine. This interface supports two options by default: inline
and path
.
These let you specify either a command directly within the Vagrantfile or the path to a ready-to-use bash script to set up the machine, as shown in the example below.
Vagrant.configure("2") do |config|
# ... others configuration
config.vm.provision "shell",
inline: "echo Hello, World"
end
Vagrant.configure("2") do |config|
# ... others configuration
config.vm.provision "shell", path: /path/to/my/bash/script
end
2. File Provisioner
It allows you to mount a file or folder from the host machine to the virtual machine.
This mechanism is especially useful for sharing provisioning scripts, configuration files, or datasets between the host and the VM without having to manually copy them each time.
For example, to copy a file to the host machine, the syntax is as follows:
Vagrant.configure("2") do |config|
# ... others configuration
config.vm.provision "file", source: "~/path/to/host/folder", destination: "$HOME/remote/newfolder"
end
This copy is one-time and does not synchronize the file state.
To synchronize the file state between the host and the VM, the configuration should be as follows:
Vagrant.configure("2") do |config|
# ... others configuration
config.vm.synced_folder "src/", "/srv/website"
end
For more information about synchronization, please consult the official documentation here.
3. Ansible Provisioner
Unlike traditional shell scripts, the Ansible provisioner allows you to describe the desired state of a system in a declarative way.
This means you write "playbooks" (YAML files) that define what you want to achieve (e.g., PostgreSQL installed, a user created, a service started), and Ansible takes care of making the necessary changes to reach that state, without unnecessarily repeating tasks that have already been done.
Why use Ansible with vagrant?
✅ More readable, structured, and reusable code.
🔁 Idempotence: running a playbook again doesn’t change anything if everything is already in place.
⚙️ Easy to maintain, especially for complex or multi-machine environments.
Vagrant can run Ansible as either a local or remote provisioner, depending on whether Ansible is installed on the host or on the VM itself.
Here is a minimal example in a Vagrantfile:
config.vm.provision "ansible" do |ansible|
ansible.playbook = "playbook.yml"
ansible.inventory_path = "inventory.ini"
end
🧪 Practical: Building the PostgreSQL + Barman Lab
It’s time to get hands-on and build our test environment.
The goal of this phase is to create two virtual machines using Vagrant:
🐘 A first VM with PostgreSQL installed; it will simulate our source database,
📦 A second VM with Barman installed; it will represent an administrator workstation or a control machine from which we can manage our backups.
We will automate the installation of each component using Bash provisioning scripts.The Vagrantfile
will define the architecture of our lab and execute the scripts automatically when the machines start.
Let's start by creating the Vagrantfile and the scripts needed to install Postgres and Barman.
Vagrantfile
# Vagrantfile
Vagrant.configure("2") do |config|
config.vm.define "vm1_postgres" do |vm1|
vm1.vm.box = "ubuntu/jammy64"
vm1.vm.hostname = "postgres"
vm1.vm.network "private_network", ip: "192.168.56.10"
vm1.vm.provision "shell", path: "scripts/setup_postgres.sh"
end
config.vm.define "vm2_vagrant" do |vm2|
vm2.vm.box = "ubuntu/jammy64"
vm2.vm.hostname = "vagrant"
vm2.vm.network "private_network", ip: "192.168.56.11"
vm2.vm.provision "shell", path: "scripts/setup_vagrant.sh"
end
end
scripts/setup_postgres.sh
#!/bin/bash
set -e
echo "[INFO] Installing PostgreSQL..."
sudo apt-get update
sudo apt-get install -y postgresql postgresql-contrib
psql --version && echo "[OK] PostgreSQL installed"
scripts/setup_barman.sh
#!/bin/bash
set -e
echo "[INFO] Installing Barman..."
sudo apt-get update
sudo apt-get install -y barman
barman --version && echo "[OK] Barman installed"
🧩 Conclusion
We have just discovered Vagrant and, in practice, laid the foundation for a test environment by setting up two virtual machines: one with PostgreSQL installed, and the other with Barman, our backup tool in the incremental backups series.
Thanks to Vagrant and provisioning scripts, we have automated the deployment of this environment, allowing us to easily reproduce our tests, save time, and avoid manual errors.
What we set up here is not limited to the context of Postgres and Barman: this type of lab can serve as a generic base for all kinds of DevOps practices, system experiments, or technical tests in isolated environments.
🧰 A simple, reproducible, and extensible foundation — ideal for learning, testing, breaking… and starting over!
See you soon in the next post on this blog.
Top comments (0)