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
sudoprivileges. - 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_portwithhost_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/21by default, which is why the private IP uses that range. - Vagrant always shares the project directory at
/vagranttoo. The extrasynced_folderline maps./siteto 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:
| Command | What it does |
|---|---|
vagrant halt | Shuts the VM down cleanly, keeping its disk. |
vagrant up | Boots an existing VM. Provisioning runs only on the first boot. |
vagrant reload --provision | Restarts the VM, applies Vagrantfile changes and re-runs provisioning. |
vagrant provision | Re-runs provisioning on a running VM. |
vagrant snapshot save clean | Saves a named snapshot of the current state. |
vagrant snapshot restore clean | Rolls the VM back to that snapshot. |
vagrant destroy -f | Deletes 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 hypervisororVERR_VMX_IN_VMX_ROOT_MODE: the KVM kernel modules are loaded and hold the CPU virtualization extensions. Unload them withsudo modprobe -r kvm_intel kvm(orkvm_amd kvmon AMD), then runvagrant upagain. 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. Runsudo 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 in192.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 theVagrantfileor addauto_correct: trueto theforwarded_portline.
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.
