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:

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:

  1. Start the VM:

    vagrant up
    
  2. SSH into the VM:

    vagrant ssh
    
  3. Halt the VM:

    vagrant halt
    
  4. Destroy the VM:

    vagrant destroy
    

Other Common Commands

  • vagrant status: Check VM status

  • vagrant reload: Restart VM with updated configuration

  • vagrant 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 uses 192.168.56.0/24 elsewhere.

  • 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 with ss -tulpn | grep LISTEN.

  • Provisioning order: Use vagrant up --no-parallel or vagrant 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/hosts entries are provisioned on every VM, or use the vagrant-hostmanager plugin.