Vagrant is a HashiCorp tool that builds and manages local virtual machines from a single text file, the Vagrantfile. Because the file lives in your project repository, every developer gets the same operating system, packages and network layout with one vagrant up. In this tutorial you will install Vagrant and VirtualBox on an Ubuntu 24.04 workstation, create a VM that serves a web page with Nginx, provision it with a shell script, and extend the setup to two machines that talk to each other.

Prerequisites

To follow this tutorial, you will need:

  • A desktop or laptop running Ubuntu 24.04 LTS on x86_64, with hardware virtualization (Intel VT-x or AMD-V) enabled in the firmware. Vagrant with VirtualBox is meant for local workstations; most cloud VPS instances do not expose nested virtualization.
  • A user with sudo privileges.
  • At least 8 GB of RAM and 20 GB of free disk space, since each VM reserves its own memory and disk.

You can confirm that the CPU exposes virtualization extensions with:

grep -cE 'vmx|svm' /proc/cpuinfo

Any number greater than 0 means the extensions are available.

Step 1 - Installing VirtualBox

Vagrant needs a provider to run the VMs. VirtualBox is the default provider and is packaged in the Ubuntu repositories, including the kernel modules built through DKMS:

sudo apt update
sudo apt install virtualbox

Check that the kernel driver is loaded and VirtualBox responds:

VBoxManage --version
7.0.16_Ubuntur162802

Your exact version may differ. If the command prints a warning about the vboxdrv kernel module, see the Troubleshooting section before continuing.

Step 2 - Installing Vagrant from the HashiCorp repository

Ubuntu's own vagrant package is often outdated, so install it from HashiCorp's official APT repository. First, download the signing key into a dedicated keyring:

sudo mkdir -p /etc/apt/keyrings
wget -qO- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/hashicorp-archive-keyring.gpg

Add the repository, restricted to that key:

echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list

Install Vagrant:

sudo apt update
sudo apt install vagrant

Verify the installation:

vagrant --version
Vagrant 2.4.x

Step 3 - Creating your first Vagrantfile

Vagrant VMs start from a box, a prebuilt base image. The bento/ubuntu-24.04 box is maintained by the Chef Bento project and includes the VirtualBox Guest Additions needed for synced folders.

Create a project directory and a folder that will hold the website files:

mkdir -p ~/vagrant-demo/site
cd ~/vagrant-demo
echo '<h1>Hello from Vagrant</h1>' > site/index.html

Create the Vagrantfile:

nano Vagrantfile

Add the following configuration:

Vagrant.configure("2") do |config|
  config.vm.box = "bento/ubuntu-24.04"
  config.vm.hostname = "web"

  # Reach Nginx in the VM from http://localhost:8080 on the host
  config.vm.network "forwarded_port", guest: 80, host: 8080, host_ip: "127.0.0.1"

  # Host-only network, reachable only from this workstation
  config.vm.network "private_network", ip: "192.168.56.10"

  # Share ./site with the VM
  config.vm.synced_folder "./site", "/srv/site"

  config.vm.provider "virtualbox" do |vb|
    vb.name = "vagrant-demo-web"
    vb.memory = 2048
    vb.cpus = 2
  end

  config.vm.provision "shell", path: "provision.sh"
end

A few notes on the options:

  • forwarded_port with host_ip: "127.0.0.1" keeps the port reachable only from your own machine instead of your whole LAN.
  • VirtualBox only allows host-only addresses in 192.168.56.0/21 by default, which is why the private IP uses that range.
  • Vagrant always shares the project directory at /vagrant too. The extra synced_folder line maps ./site to a cleaner path.

Step 4 - Writing the provisioning script

Provisioning installs and configures software the first time the VM boots, so nobody has to repeat manual steps. The shell provisioner runs the script as root inside the VM.

Create provision.sh in the same directory:

nano provision.sh
#!/usr/bin/env bash
set -euo pipefail

export DEBIAN_FRONTEND=noninteractive

apt-get update
apt-get install -y nginx

cat > /etc/nginx/sites-available/site <<'EOF'
server {
    listen 80 default_server;
    listen [::]:80 default_server;
    root /srv/site;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}
EOF

ln -sf /etc/nginx/sites-available/site /etc/nginx/sites-enabled/site
rm -f /etc/nginx/sites-enabled/default

nginx -t
systemctl reload nginx

The script is safe to run more than once: it overwrites the same files and reloads Nginx, which matters because you can re-run provisioning at any time.

Step 5 - Starting and accessing the VM

Bring the VM up. The first run downloads the box (roughly 1 GB), so it takes a few minutes:

vagrant up

The end of the output should include the provisioner running Nginx's configuration test:

==> default: Machine booted and ready!
==> default: Setting hostname...
==> default: Configuring and enabling network interfaces...
==> default: Mounting shared folders...
    default: /srv/site => /home/your_user/vagrant-demo/site
==> default: Running provisioner: shell...
    default: nginx: configuration file /etc/nginx/nginx.conf test is successful

Check the machine state:

vagrant status
Current machine states:

default                   running (virtualbox)

Test the forwarded port and the private network address from the host:

curl http://localhost:8080
curl http://192.168.56.10
<h1>Hello from Vagrant</h1>
<h1>Hello from Vagrant</h1>

Because site/ is a synced folder, editing site/index.html on the host changes the page immediately, with no rebuild. To open a shell inside the VM, run:

vagrant ssh

Type exit to return to the host.

Step 6 - Managing the VM lifecycle

These are the commands you will use every day:

CommandWhat it does
vagrant haltShuts the VM down cleanly, keeping its disk.
vagrant upBoots an existing VM. Provisioning runs only on the first boot.
vagrant reload --provisionRestarts the VM, applies Vagrantfile changes and re-runs provisioning.
vagrant provisionRe-runs provisioning on a running VM.
vagrant snapshot save cleanSaves a named snapshot of the current state.
vagrant snapshot restore cleanRolls the VM back to that snapshot.
vagrant destroy -fDeletes the VM and its disk. The box stays cached.

Snapshots are useful before risky changes such as a major package upgrade. For example:

vagrant snapshot save clean
vagrant snapshot list
==> default:
clean

Boxes are cached in ~/.vagrant.d/boxes and shared between projects. Keep them up to date and remove old versions with:

vagrant box list
vagrant box update
vagrant box prune

Step 7 - Defining a multi-machine environment

Real applications usually have more than one component. Vagrant can define several VMs in one Vagrantfile, each with its own settings. Destroy the single VM first:

vagrant destroy -f

Replace the contents of Vagrantfile with a web server and a database server on the same private network:

nano Vagrantfile
Vagrant.configure("2") do |config|
  config.vm.box = "bento/ubuntu-24.04"

  config.vm.provider "virtualbox" do |vb|
    vb.memory = 1024
    vb.cpus = 1
  end

  config.vm.define "db" do |db|
    db.vm.hostname = "db"
    db.vm.network "private_network", ip: "192.168.56.11"
    db.vm.provision "shell", inline: <<-SHELL
      export DEBIAN_FRONTEND=noninteractive
      apt-get update
      apt-get install -y postgresql
    SHELL
  end

  config.vm.define "web", primary: true do |web|
    web.vm.hostname = "web"
    web.vm.network "private_network", ip: "192.168.56.10"
    web.vm.network "forwarded_port", guest: 80, host: 8080, host_ip: "127.0.0.1"
    web.vm.synced_folder "./site", "/srv/site"
    web.vm.provision "shell", path: "provision.sh"
    web.vm.provision "shell", inline: "apt-get install -y postgresql-client"
  end
end

Settings in the outer block apply to both machines, and each define block adds machine-specific settings. primary: true makes web the default target for commands such as vagrant ssh.

Start both machines:

vagrant up
vagrant status
Current machine states:

db                        running (virtualbox)
web                       running (virtualbox)

Confirm that web can reach db over the private network:

vagrant ssh web -c "ping -c 2 192.168.56.11"
64 bytes from 192.168.56.11: icmp_seq=1 ttl=64 time=0.612 ms
64 bytes from 192.168.56.11: icmp_seq=2 ttl=64 time=0.498 ms

Most commands accept a machine name, for example vagrant halt db or vagrant provision web. Without a name they act on all machines.

Step 8 - Sharing the environment with your team

Commit the Vagrantfile, provision.sh and site/ to version control. Exclude the .vagrant/ directory, which holds machine IDs and SSH keys that are specific to your workstation:

echo ".vagrant/" >> .gitignore

A teammate then only needs to clone the repository and run vagrant up to get an identical environment.

Troubleshooting

  • VT-x is being used by another hypervisor or VERR_VMX_IN_VMX_ROOT_MODE: the KVM kernel modules are loaded and hold the CPU virtualization extensions. Unload them with sudo modprobe -r kvm_intel kvm (or kvm_amd kvm on AMD), then run vagrant up again. Stop any running KVM or libvirt VMs first.
  • The vboxdrv kernel module is not loaded: with Secure Boot enabled, the unsigned DKMS module is rejected. Run sudo dpkg-reconfigure virtualbox-dkms, set a MOK password when prompted, reboot and enroll the key in the blue MOK Manager screen.
  • The IP address configured for the host-only network is not within the allowed ranges: use an address in 192.168.56.0/21, or list additional ranges in /etc/vbox/networks.conf.
  • Port collision on 8080: another process or VM already uses the host port. Change host: in the Vagrantfile or add auto_correct: true to the forwarded_port line.

Conclusion

You now have a reproducible development environment defined in code: a VirtualBox VM provisioned with Nginx, a synced folder for live editing, and a two-machine layout with a private network. From here you can replace the shell provisioner with the ansible_local provisioner to reuse your Ansible playbooks, pin a specific box version with config.vm.box_version, or build your own base box with Packer so development matches the image you run in production.