Vagrant
Vagrant is an open-source tool for building and managing virtual machines in a single workflow. It provides a simple and easy-to-use command-line interface to create and configure lightweight, reproducible, and portable development environments. Vagrant works with various virtualization providers, including VirtualBox, VMware, Hyper-V, and Docker. It is widely used by developers to create consistent development environments across different machines and teams.
Getting Started
Follow the official documentation for the most up-to-date instructions:
VirtualBox: https://www.virtualbox.org/wiki/Linux_Downloads
Verify the installation:
vboxmanage --version vagrant --version
Initialize a Vagrant Project
Create a new directory for your Vagrant project:
mkdir vagrant-demo cd vagrant-demo
Initialize a Vagrantfile with Ubuntu 24.04 (Noble Numbat):
vagrant init cloud-image/ubuntu-24.04
This creates a Vagrantfile in your directory.
Configure the Vagrantfile
Open the Vagrantfile and update it as follows:
1Vagrant.configure("2") do |config|
2 config.vm.box = "cloud-image/ubuntu-24.04"
3 config.vm.hostname = "demo-vm"
4
5 config.vm.provider "virtualbox" do |vb|
6 vb.name = "DemoVM"
7 vb.memory = "4096"
8 vb.cpus = 2
9 end
10end
Working with Vagrant
The basic Vagrant commands to manage your VM are:
Start the VM:
vagrant upSSH into the VM:
vagrant sshHalt the VM:
vagrant haltDestroy the VM:
vagrant destroy
Other Common Commands
vagrant status: Check VM statusvagrant reload: Restart VM with updated configurationvagrant box list: List installed boxes
Provisioning with Vagrant
Available Provisioners
Provisioners in Vagrant are tools that allow you to automatically configure your virtual machines after they are created. They enable you to install software, update packages, and set up services without manual intervention. Vagrant supports multiple types of provisioners, including:
Shell: Executes shell scripts or inline commands.
Docker: Uses Docker containers for provisioning.
Ansible: Uses Ansible playbooks for configuration management.
Chef: Integrates with Chef for advanced provisioning.
Puppet: Applies Puppet manifests for system configuration.
For a complete list of supported provisioners, refer to the official documentation https://developer.hashicorp.com/vagrant/docs/provisioning.
Provisioners are defined in the Vagrantfile using the
config.vm.provision directive. For example:
Inline Provisioning
You can use inline shell scripts to provision your VM. For example:
1config.vm.provision "shell", inline: <<-SHELL
2 apt update
3 apt install -y nginx
4SHELL
Script Provisioning
You can also use external shell scripts for provisioning. For example:
config.vm.provision "shell", path: "setup.sh"
Where setup.sh is a shell script in the same directory as your
Vagrantfile.
Provisioning Execution
Provisioning can be triggered during vagrant up or later using
vagrant provision. This feature is essential for creating reproducible
and automated development environments.
Example: Install Nginx automatically:
1Vagrant.configure("2") do |config|
2 config.vm.box = "cloud-image/ubuntu-24.04"
3
4 config.vm.provision "shell", inline: <<-SHELL
5 apt update
6 apt install -y nginx
7 SHELL
8end
Apply provisioning:
1# to create and provision the VM
2vagrant up
3
4# to force provisioning at startup for an already created VM
5vagrant up --provision
6
7# to provision an already running VM
8vagrant provision
Combining Provisioners
Vagrant allows you to use multiple provisioners in a single Vagrantfile.
This is useful when you want to mix simple shell commands with more advanced
configuration management tools. Provisioners run in the order they are defined.
Example:
1Vagrant.configure("2") do |config|
2 config.vm.box = "cloud-image/ubuntu-24.04"
3
4 # First, run a shell script to update packages
5 config.vm.provision "shell", inline: <<-SHELL
6 apt update
7 SHELL
8
9 # Then, use an Ansible playbook
10 config.vm.provision "ansible" do |ansible|
11 ansible.playbook = "playbook.yml"
12 end
13end
You can also specify when a provisioner should run using the run option:
1config.vm.provision "shell", inline: "echo 'Hello!'", run: "always"
2config.vm.provision "shell", inline: "echo 'This runs only once'", run: "once"
Multi-Machine Environments
This section provides examples for defining and controlling multi-machine Vagrant environments
and enabling machine-to-machine communication using private networks
(Host‑only networking with VirtualBox). All examples assume Ubuntu 24.04
hosts and guests with the virtualbox provider.
Overview
A multi-machine Vagrant environment is a single project (i.e., one Vagrantfile)
that defines multiple VMs, often representing several roles (like web, app, and
db). Each machine can have its own CPU/memory, network interfaces, synced
folders, and machine-specific provisioners. A private network (i.e., a host‑only
adapter in VirtualBox) lets VMs reach each other directly by IP without
exposing services publicly.
Multi‑Machine Vagrantfile
The following minimal example defines two machines (web and db) on a
shared private network. Replace the IPs with any free addresses in your
VirtualBox host‑only network (commonly 192.168.56.0/24).
1# Vagrantfile
2Vagrant.configure("2") do |config|
3 # Base box (Ubuntu 24.04: "noble")
4 config.vm.box = "cloud-image/ubuntu-24.04"
5
6 # --- DB machine ---
7 config.vm.define "db" do |db|
8 db.vm.hostname = "db.local"
9 db.vm.network "private_network", ip: "192.168.56.11"
10
11 db.vm.provider "virtualbox" do |vb|
12 vb.name = "demo-db"
13 vb.cpus = 1
14 vb.memory = 1024
15 end
16
17 # Machine-specific provisioning (optional)
18 db.vm.provision "shell", inline: <<-SHELL
19 sudo apt update
20 sudo apt install -y postgresql
21 SHELL
22 end
23
24 # --- WEB machine ---
25 config.vm.define "web" do |web|
26 web.vm.hostname = "web.local"
27 web.vm.network "private_network", ip: "192.168.56.10"
28
29 web.vm.provider "virtualbox" do |vb|
30 vb.name = "demo-web"
31 vb.cpus = 2
32 vb.memory = 2048
33 end
34
35 web.vm.provision "shell", inline: <<-SHELL
36 sudo apt update
37 sudo apt install -y nginx
38 SHELL
39 end
40end
Control the Machines
You can start and manage both machines with:
vagrant up # starts all machines in the Vagrantfile
vagrant status # shows each machine status
vagrant global-status # lists all Vagrant machines on the host
You can SSH into each machine individually with:
vagrant ssh web
vagrant ssh db
Bring up several in sequence:
vagrant up db web
Start sequentially (avoid parallel startup):
vagrant up --no-parallel
# or
vagrant up db && vagrant up web
Use Vagrant triggers to block until a dependency is reachable:
1config.trigger.after :up do |t|
2t.only_on = ["web"]
3t.run = {
4 inline: "until nc -z 192.168.56.11 5432; do echo 'Waiting for DB...'; sleep 2; done"
5}
6end
Patterns & Tips
Per machine config.vm.define blocks
Each machine gets a logical name and its own block for settings.
1config.vm.define "app" do |app|
2 app.vm.box = "ubuntu/noble64"
3 app.vm.hostname = "app.local"
4end
Separate role blocks keep configs clear
Group provider, network, provisioners, and synced folders under each machine definition to avoid cross‑contamination.
Globals and Per Machine Override
Keep global settings (like base box) at the top level, and override per machine as needed.
Global default:
config.vm.box = "cloud-image/ubuntu-24.04"
Per machine override (e.g., a different base image for db):
db.vm.box = "bento/ubuntu-24.04"
CPU/Memory Per Machine
web.vm.provider "virtualbox" do |vb|
vb.cpus = 2
vb.memory = 2048
end
db.vm.provider "virtualbox" do |vb|
vb.cpus = 1
vb.memory = 1024
end
Private Networks for Inter‑VM Communication
Private networks create a host-only LAN that is not reachable from outside your host. All VMs on the same private network can talk to each other using IP addresses.
Static IPs (most common)
Assign unique IPs in the same subnet:
web.vm.network "private_network", ip: "192.168.56.10" db.vm.network "private_network", ip: "192.168.56.11"
Ensure the subnet (e.g.,
192.168.56.0/24) exists in VirtualBox Host‑Only Networks. If unsure, bring machines up; Vagrant/VirtualBox will create a host‑only adapter as needed.
DHCP (alternative)
Let the host-only DHCP server assign addresses:
web.vm.network "private_network", type: "dhcp" db.vm.network "private_network", type: "dhcp"
Multiple Private Networks
You can attach multiple host‑only networks (e.g., a backend and a monitoring LAN):
app.vm.network "private_network", ip: "192.168.56.20" # backend app.vm.network "private_network", ip: "192.168.57.20" # monitoring
Machines can share one or more of these to control communication patterns.
Name Resolution Between Machines
By default, VMs do not resolve each other by hostname. Use one of:
IP Addresses
# From web -> db
ping -c1 192.168.56.11
psql -h 192.168.56.11 -U postgres
/etc/hosts Entries via Provisioning
Add hosts entries on each VM so names resolve locally:
1hosts = <<-HOSTS
2192.168.56.10 web.local web
3192.168.56.11 db.local db
4HOSTS
5
6["web", "db"].each do |m|
7 config.vm.define m do |node|
8 node.vm.provision "shell", inline: <<-SHELL
9 cat <<'EOF' | sudo tee -a /etc/hosts
10 #{hosts}
11 EOF
12 SHELL
13 end
14end
Hostmanager Plugin
The community vagrant-hostmanager plugin can manage host entries across the host and guests. If you choose this route:
vagrant plugin install vagrant-hostmanager
Then add:
1Vagrant.configure("2") do |config|
2 config.hostmanager.enabled = true
3 config.hostmanager.manage_host = true
4 config.hostmanager.manage_guest = true
5 config.hostmanager.ignore_private_ip = false
6 config.hostmanager.include_offline = true
7 config.vm.define 'example-box' do |node|
8 node.vm.hostname = 'example-box-hostname'
9 node.vm.network :private_network, ip: '192.168.42.42'
10 node.hostmanager.aliases = %w(example-box.localdomain example-box-alias)
11 end
12end
Syncing Code/Data across Machines
A shared project folder can be mounted on multiple VMs for consistent builds. For example, the folder “./src” on the host is mounted to “/srv/src” on both VMs:
1web.vm.synced_folder "./src", "/srv/src", create: true
2app.vm.synced_folder "./src", "/srv/src", create: true
More details on synced folder options, see the official docs.
Troubleshooting
IP conflicts: Ensure each VM gets a unique IP; verify the host‑only network range in VirtualBox. Adjust to another subnet (e.g.,
192.168.57.0/24) if your host uses192.168.56.0/24elsewhere.Service not reachable: Confirm the service binds to the private IP or to 0.0.0.0 (not only
127.0.0.1). Restart the service and check its port withss -tulpn | grep LISTEN.Provisioning order: Use
vagrant up --no-parallelorvagrant up db && vagrant up web. Add triggers to wait until dependent ports are open.UFW/Firewall: If enabled, allow the private subnet:
sudo ufw allow from 192.168.56.0/24. For ICMP, also allow on the interface:sudo ufw allow in on enp0s8.Name resolution: If hostnames don’t resolve, either use IPs or ensure
/etc/hostsentries are provisioned on every VM, or use the vagrant-hostmanager plugin.