Vagrant Development
Complete guide for managing virtual machine environments with Vagrant.
Overview
Vagrant is a tool for building and managing virtual machine environments in a single, consistent workflow.
Key Characteristics:
- Reproducible environments
- Multiple providers (VirtualBox, VMware, Docker)
- Provisioners (Shell, Ansible, Chef, Puppet)
- Multi-machine support
- Portable boxes
Installation
Install Vagrant
# macOS (Homebrew)
brew install hashicorp/tap/hashicorp-vagrant
# Ubuntu/Debian
curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add -
sudo apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main"
sudo apt-get update && sudo apt-get install vagrant
# Windows (Chocolatey)
choco install vagrant
# Or download from https://www.vagrantup.com/downloads
Install Provider
# VirtualBox (most common)
# macOS: brew install --cask virtualbox
# Ubuntu: sudo apt install virtualbox
# Windows: choco install virtualbox
# VMware (requires license)
vagrant plugin install vagrant-vmware-desktop
# Hyper-V (Windows only)
# Enable in Windows Features
# libvirt (Linux)
vagrant plugin install vagrant-libvirt
Basic Vagrantfile
Minimal Configuration
# Vagrantfile
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/focal64"
config.vm.hostname = "myvm"
config.vm.provider "virtualbox" do |vb|
vb.memory = "2048"
vb.cpus = 2
end
end
Complete Configuration
# Vagrantfile
Vagrant.configure("2") do |config|
# Base box
config.vm.box = "ubuntu/focal64"
config.vm.box_version = ">= 202310.0.0"
config.vm.hostname = "dev-environment"
# Network configuration
config.vm.network "private_network", ip: "192.168.33.10"
config.vm.network "forwarded_port", guest: 80, host: 8080
config.vm.network "forwarded_port", guest: 443, host: 8443
# Synced folders
config.vm.synced_folder "./app", "/var/www/html"
config.vm.synced_folder "./data", "/data", disabled: false
# VirtualBox provider
config.vm.provider "virtualbox" do |vb|
vb.memory = "4096"
vb.cpus = 2
vb.name = "my-dev-vm"
vb.gui = false
vb.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
vb.customize ["modifyvm", :id, "--ioapic", "on"]
end
# Shell provisioner
config.vm.provision "shell", inline: <<-SHELL
apt-get update
apt-get install -y apache2 mysql-server php
SHELL
# Ansible provisioner
config.vm.provision "ansible" do |ansible|
ansible.playbook = "provisioning/playbook.yml"
ansible.become = true
end
end
Common Commands
# Create Vagrantfile
vagrant init ubuntu/focal64
# Start VM
vagrant up
# SSH into VM
vagrant ssh
# Stop VM
vagrant halt
# Restart VM
vagrant reload
# Destroy VM
vagrant destroy
# Suspend VM
vagrant suspend
# Resume VM
vagrant resume
# Status
vagrant status
# Global status
vagrant global-status
# Box management
vagrant box list
vagrant box add ubuntu/focal64
vagrant box remove ubuntu/focal64
vagrant box update
vagrant box outdated
# Plugin management
vagrant plugin list
vagrant plugin install vagrant-vbguest
vagrant plugin uninstall vagrant-vbguest
# Validate Vagrantfile
vagrant validate
# SSH config
vagrant ssh-config
# Package box
vagrant package --output my-custom.box
Providers
VirtualBox Provider
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/focal64"
config.vm.provider "virtualbox" do |vb|
# Basic settings
vb.name = "my-vm"
vb.gui = false
vb.memory = 4096
vb.cpus = 2
# Advanced settings
vb.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
vb.customize ["modifyvm", :id, "--natdnsproxy1", "on"]
vb.customize ["modifyvm", :id, "--ioapic", "on"]
vb.customize ["modifyvm", :id, "--pae", "on"]
# Video settings
vb.customize ["modifyvm", :id, "--vram", "128"]
vb.customize ["modifyvm", :id, "--accelerate3d", "on"]
# Storage
vb.customize ["storagectl", :id, "--name", "SATA Controller", "--ahci", "on"]
# Network
vb.customize ["modifyvm", :id, "--nictype1", "virtio"]
# Clipboard
vb.customize ["modifyvm", :id, "--clipboard", "bidirectional"]
vb.customize ["modifyvm", :id, "--draganddrop", "bidirectional"]
end
end
VMware Provider
Vagrant.configure("2") do |config|
config.vm.box = "bento/ubuntu-22.04"
config.vm.provider "vmware_fusion" do |vmw| # macOS
vmw.vmx["memsize"] = "4096"
vmw.vmx["numvcpus"] = "4"
vmw.vmx["vhv.enable"] = "TRUE"
vmw.linked_clone = true
vmw.gui = false
end
config.vm.provider "vmware_workstation" do |vmw| # Linux/Windows
vmw.vmx["memsize"] = "4096"
vmw.vmx["numvcpus"] = "4"
end
end
Hyper-V Provider
Vagrant.configure("2") do |config|
config.vm.box = "microsoft/windows-server-2022"
config.vm.provider "hyperv" do |hv|
hv.memory = 4096
hv.maxmemory = 8192
hv.cpus = 2
hv.enable_virtualization_extensions = true
hv.enable_checkpoints = true
hv.vmname = "dev-vm"
hv.vlan_id = 100
hv.ip_address_timeout = 180
end
end
libvirt Provider
Vagrant.configure("2") do |config|
config.vm.box = "generic/ubuntu2204"
config.vm.provider :libvirt do |libvirt|
libvirt.cpus = 4
libvirt.memory = 4096
libvirt.driver = "kvm"
libvirt.cpu_mode = "host-passthrough"
libvirt.disk_bus = "virtio"
libvirt.disk_driver :cache => "none", :io => "native"
libvirt.video_type = "qxl"
libvirt.video_vram = "128"
libvirt.interface_type = "bridge"
libvirt.interface_source = "br0"
end
end
AWS Provider
Vagrant.configure("2") do |config|
config.vm.box = "aws"
config.vm.provider :aws do |aws, override|
override.ssh.username = "ec2-user"
override.ssh.private_key_path = "~/.ssh/my-key.pem"
aws.access_key_id = ENV['AWS_ACCESS_KEY_ID']
aws.secret_access_key = ENV['AWS_SECRET_ACCESS_KEY']
aws.region = "us-east-1"
aws.ami = "ami-0c55b159cbfafe1f0"
aws.instance_type = "t3.medium"
aws.keypair_name = "my-key"
aws.security_groups = ["default"]
aws.tags = {
'Name' => 'vagrant-dev',
'Environment' => 'development'
}
aws.block_device_mappings = [{
device_name: '/dev/sda1',
ebs: {
volume_size: 50,
volume_type: 'gp3',
delete_on_termination: true
}
}]
end
end
DigitalOcean Provider
Vagrant.configure("2") do |config|
config.vm.box = "digitalocean"
config.vm.provider :digital_ocean do |provider|
provider.access_token = ENV['DIGITALOCEAN_ACCESS_TOKEN']
provider.ssh_key_id = "12345678"
provider.image = "ubuntu-22-04-x64"
provider.size = "s-2vcpu-4gb"
provider.region = "nyc3"
provider.ssh_username = "root"
provider.private_networking = true
provider.tags = ["vagrant", "development"]
end
end
Provisioning
Shell Provisioner
Vagrant.configure("2") do |config|
# Inline script
config.vm.provision "shell", inline: <<-SHELL
apt-get update
apt-get install -y nginx
SHELL
# External script
config.vm.provision "shell", path: "scripts/setup.sh"
# With arguments
config.vm.provision "shell", path: "scripts/setup.sh",
args: ["--verbose", "--env", "development"]
# With environment variables
config.vm.provision "shell", path: "scripts/setup.sh",
env: {
"APP_ENV" => "development",
"DB_HOST" => "localhost"
}
# Run as non-root
config.vm.provision "shell", path: "scripts/setup.sh",
privileged: false
# Run on specific machine
config.vm.define "web" do |web|
web.vm.provision "shell", inline: "echo 'Web server'"
end
end
Ansible Provisioner
Vagrant.configure("2") do |config|
config.vm.provision "ansible" do |ansible|
ansible.playbook = "provisioning/playbook.yml"
ansible.inventory_path = "provisioning/inventory"
ansible.verbose = "v"
ansible.become = true
# Extra variables
ansible.extra_vars = {
http_port: 80,
db_host: "localhost"
}
# Tags
ansible.tags = ["web", "nginx"]
ansible.skip_tags = ["db"]
# Vault
ansible.vault_password_file = "~/.vault_pass"
# Roles path
ansible.roles_path = "provisioning/roles"
ansible.galaxy_role_file = "provisioning/requirements.yml"
end
end
Ansible Local (runs on guest)
Vagrant.configure("2") do |config|
config.vm.provision "ansible_local" do |ansible|
ansible.playbook = "playbook.yml"
ansible.install_mode = "pip"
ansible.version = "2.14.0"
ansible.verbose = "v"
ansible.become = true
ansible.galaxy_file = "requirements.yml"
ansible.galaxy_roles_path = "roles"
end
end
Chef Provisioner
Vagrant.configure("2") do |config|
config.vm.provision "chef_solo" do |chef|
chef.cookbooks_path = "cookbooks"
chef.roles_path = "roles"
chef.add_recipe "webserver::default"
chef.add_recipe "database::default"
chef.json = {
webserver: {
port: 80,
docroot: "/var/www/html"
}
}
end
end
Puppet Provisioner
Vagrant.configure("2") do |config|
config.vm.provision "puppet_apply" do |puppet|
puppet.manifests_path = "manifests"
puppet.manifest_file = "site.pp"
puppet.module_path = "modules"
puppet.facter = {
"environment" => "development",
"server_role" => "web"
}
puppet.options = ["--verbose", "--debug"]
end
end
Docker Provisioner
Vagrant.configure("2") do |config|
config.vm.provision "docker" do |d|
# Pull images
d.pull_images "nginx:latest"
d.pull_images "postgres:14"
# Run containers
d.run "nginx",
image: "nginx:latest",
args: "-p 80:80 -v /vagrant/nginx.conf:/etc/nginx/nginx.conf"
d.run "postgres",
image: "postgres:14",
args: "-e POSTGRES_PASSWORD=secret -p 5432:5432"
end
end
Multi-Machine Setup
Basic Multi-Machine
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/focal64"
config.vm.define "web" do |web|
web.vm.hostname = "web"
web.vm.network "private_network", ip: "192.168.33.10"
web.vm.network "forwarded_port", guest: 80, host: 8080
web.vm.provider "virtualbox" do |vb|
vb.memory = "2048"
end
web.vm.provision "shell", inline: "apt-get install -y nginx"
end
config.vm.define "db" do |db|
db.vm.hostname = "db"
db.vm.network "private_network", ip: "192.168.33.20"
db.vm.provider "virtualbox" do |vb|
vb.memory = "4096"
end
db.vm.provision "shell", inline: "apt-get install -y postgresql"
end
end
Complex Multi-Machine with Dependencies
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/focal64"
# Primary machine (default)
config.vm.define "loadbalancer", primary: true do |lb|
lb.vm.hostname = "lb"
lb.vm.network "private_network", ip: "192.168.33.10"
lb.vm.network "forwarded_port", guest: 80, host: 8080
lb.vm.provider "virtualbox" do |vb|
vb.memory = "1024"
end
lb.vm.provision "shell", inline: <<-SHELL
apt-get update
apt-get install -y haproxy
SHELL
end
# Web servers
(1..3).each do |i|
config.vm.define "web#{i}" do |web|
web.vm.hostname = "web#{i}"
web.vm.network "private_network", ip: "192.168.33.#{20 + i}"
web.vm.provider "virtualbox" do |vb|
vb.memory = "2048"
end
web.vm.provision "shell", inline: <<-SHELL
apt-get update
apt-get install -y nginx
echo "Web Server #{i}" > /var/www/html/index.html
SHELL
# Only start after loadbalancer
web.vm.provision "shell", run: "never" do |s|
s.inline = "echo 'Web#{i} ready'"
end
end
end
# Database
config.vm.define "database" do |db|
db.vm.hostname = "db"
db.vm.network "private_network", ip: "192.168.33.50"
db.vm.provider "virtualbox" do |vb|
vb.memory = "4096"
end
db.vm.provision "shell", inline: <<-SHELL
apt-get update
apt-get install -y postgresql
systemctl start postgresql
SHELL
end
end
Control Commands
# Start all machines
vagrant up
# Start specific machine
vagrant up web
# Start multiple machines
vagrant up web db
# SSH to specific machine
vagrant ssh web
# Provision specific machine
vagrant provision web
# Reload specific machine
vagrant reload web
# Destroy specific machine
vagrant destroy web
Networking
Private Networks
Vagrant.configure("2") do |config|
# Static IP
config.vm.network "private_network", ip: "192.168.33.10"
# With netmask
config.vm.network "private_network",
ip: "192.168.33.10",
netmask: "255.255.255.0"
# DHCP
config.vm.network "private_network", type: "dhcp"
# Internal network (isolated)
config.vm.network "private_network",
ip: "10.0.0.10",
virtualbox__intnet: "internal_network"
end
Public Networks (Bridged)
Vagrant.configure("2") do |config|
# Bridge to specific interface
config.vm.network "public_network",
bridge: "en0: Wi-Fi (AirPort)"
# With static IP
config.vm.network "public_network",
bridge: "en0",
ip: "192.168.1.100"
# Auto-bridge (prompts to select)
config.vm.network "public_network"
end
Port Forwarding
Vagrant.configure("2") do |config|
# Basic port forwarding
config.vm.network "forwarded_port", guest: 80, host: 8080
# With auto-correction
config.vm.network "forwarded_port",
guest: 80,
host: 8080,
auto_correct: true
# UDP
config.vm.network "forwarded_port",
guest: 53,
host: 1053,
protocol: "udp"
# Multiple ports
config.vm.network "forwarded_port", guest: 80, host: 8080
config.vm.network "forwarded_port", guest: 443, host: 8443
config.vm.network "forwarded_port", guest: 3306, host: 3306
end
Synced Folders
VirtualBox Shared Folders (Default)
Vagrant.configure("2") do |config|
config.vm.synced_folder "./app", "/var/www/html"
# Disable default
config.vm.synced_folder ".", "/vagrant", disabled: true
# With options
config.vm.synced_folder "./app", "/var/www/html",
owner: "www-data",
group: "www-data",
mount_options: ["dmode=775", "fmode=664"]
end
NFS (Better Performance)
Vagrant.configure("2") do |config|
config.vm.synced_folder "./app", "/var/www/html",
type: "nfs",
nfs_udp: false,
mount_options: [
"nfsvers=3",
"tcp",
"rsize=32768",
"wsize=32768"
]
end
RSync
Vagrant.configure("2") do |config|
config.vm.synced_folder "./app", "/var/www/html",
type: "rsync",
rsync__auto: true,
rsync__exclude: [".git/", "node_modules/", "*.log"],
rsync__args: ["--verbose", "--archive", "--delete", "-z"]
end
SMB (Windows)
Vagrant.configure("2") do |config|
config.vm.synced_folder "./app", "/var/www/html",
type: "smb",
smb_host: "127.0.0.1",
smb_username: ENV['USER'],
smb_password: ENV['SMB_PASSWORD']
end
Custom Boxes
Packaging a Box
# From running VM
vagrant package --base my-configured-vm --output my-custom.box
# With Vagrantfile template
vagrant package --base my-vm --output my.box --vagrantfile Vagrantfile.template
# With include files
vagrant package --base my-vm --output my.box --include README.md,metadata.json
metadata.json
{
"name": "myorg/ubuntu-custom",
"description": "Custom Ubuntu 20.04 with pre-installed tools",
"versions": [
{
"version": "1.0.0",
"providers": [
{
"name": "virtualbox",
"url": "https://example.com/boxes/ubuntu-custom-1.0.0.box",
"checksum": "sha256:abc123...",
"checksum_type": "sha256"
}
]
}
]
}
Adding and Using Custom Boxes
# Add local box
vagrant box add myorg/ubuntu-custom ./my-custom.box
# Add from URL
vagrant box add myorg/ubuntu-custom https://example.com/my-custom.box
# Use in Vagrantfile
# config.vm.box = "myorg/ubuntu-custom"
# List boxes
vagrant box list
# Update box
vagrant box update --box myorg/ubuntu-custom
# Remove box
vagrant box remove myorg/ubuntu-custom
Publishing to Vagrant Cloud
# Login
vagrant cloud auth login
# Publish
vagrant cloud publish myorg/ubuntu-custom 1.0.0 \
--description "Custom Ubuntu with development tools" \
--provider virtualbox \
--file ./my-custom.box
Plugins
Installing Plugins
# Install plugin
vagrant plugin install vagrant-vbguest
# Install specific version
vagrant plugin install vagrant-vbguest --plugin-version 0.21.0
# List installed plugins
vagrant plugin list
# Update plugin
vagrant plugin update vagrant-vbguest
# Uninstall plugin
vagrant plugin uninstall vagrant-vbguest
Useful Plugins
# VirtualBox Guest Additions
vagrant plugin install vagrant-vbguest
# Host Manager (manages /etc/hosts)
vagrant plugin install vagrant-hostmanager
# Disk size management
vagrant plugin install vagrant-disksize
# Snapshot management
vagrant plugin install vagrant-snapshot
# Cachier (package cache)
vagrant plugin install vagrant-cachier
Plugin Configuration
# vagrant-vbguest
config.vbguest.auto_update = true
config.vbguest.no_remote = true
# vagrant-hostmanager
config.hostmanager.enabled = true
config.hostmanager.manage_host = true
config.hostmanager.manage_guest = true
# vagrant-disksize
config.disksize.size = '50GB'
Triggers
Basic Triggers
Vagrant.configure("2") do |config|
config.trigger.before :up do |trigger|
trigger.name = "Before Up"
trigger.info = "Starting VM..."
end
config.trigger.after :up do |trigger|
trigger.name = "After Up"
trigger.run = {inline: "echo 'VM is ready!'"}
end
config.trigger.before :destroy do |trigger|
trigger.name = "Confirm Destroy"
trigger.ask = "Are you sure you want to destroy?"
end
end
Advanced Triggers
Vagrant.configure("2") do |config|
# Run commands on host
config.trigger.after :up do |trigger|
trigger.run = {
inline: "echo 'VM IP:' && vagrant ssh -c 'ip addr show eth1'"
}
end
# Run commands on guest
config.trigger.after :provision do |trigger|
trigger.run_remote = {
inline: "systemctl status nginx"
}
end
# Execute only on specific machine
config.vm.define "web" do |web|
web.trigger.after :up do |trigger|
trigger.info = "Web server is up!"
end
end
end
Troubleshooting
Common Errors
SSH Timeout
Vagrant.configure("2") do |config|
# Increase SSH timeout
config.ssh.timeout = 120
config.ssh.insert_key = false
# For slow networks
config.vm.provider "virtualbox" do |vb|
vb.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
end
end
Network Issues
# Debug network
vagrant ssh -c "ip addr show"
vagrant ssh -c "cat /etc/network/interfaces"
# Reload network
vagrant reload
# Check VirtualBox networks
VBoxManage list natnets
VBoxManage list hostonlyifs
Synced Folder Permission Denied
config.vm.synced_folder "./app", "/var/www/html",
owner: "vagrant",
group: "vagrant",
mount_options: ["dmode=775", "fmode=664"]
Box Download Issues
# Clean download cache
rm -rf ~/.vagrant.d/tmp/*
# Download with specific version
vagrant box add ubuntu/focal64 --box-version 202310.0.0
# Use alternative download location
export VAGRANT_SERVER_URL="https://vagrantcloud.com"
Debug Mode
# Enable debug logging
VAGRANT_LOG=debug vagrant up
# Enable info logging
VAGRANT_LOG=info vagrant up
# Debug specific plugin
VAGRANT_LOG=debug VAGRANT_DEFAULT_PROVIDER=virtualbox vagrant up
Reset Environment
# Destroy all VMs
vagrant destroy -f
# Remove cached box
vagrant box remove ubuntu/focal64
# Clean up
rm -rf .vagrant
rm -rf ~/.vagrant.d/data
# Fresh start
vagrant up
Performance Optimization
VirtualBox Optimization
Vagrant.configure("2") do |config|
config.vm.provider "virtualbox" do |vb|
# Memory and CPU
vb.memory = 4096
vb.cpus = 2
# Enable PAE/NX
vb.pae = true
# I/O optimization
vb.customize ["storagectl", :id, "--name", "SATA Controller", "--ahci", "on"]
vb.customize ["storageattach", :id, "--storagectl", "SATA Controller",
"--type", "hdd", "--nonrotational", "on"]
# Network optimization
vb.customize ["modifyvm", :id, "--nictype1", "virtio"]
# Disable audio (saves resources)
vb.customize ["modifyvm", :id, "--audio", "none"]
end
# Use NFS for better disk I/O
config.vm.synced_folder "./app", "/app", type: "nfs"
end
Parallel Operations
# Enable parallel execution
Vagrant.configure("2") do |config|
# Machines can be started in parallel
config.vm.define "web"
config.vm.define "db"
# Use provisioner ordering
config.vm.provision "shell", inline: "apt-get update", run: "always"
end
Best Practices
- Version control your Vagrantfile
- Use specific box versions for reproducibility
- Use provisioners for repeatable setup
- Enable auto_correct for port forwarding
- Use triggers for automation hooks
- Document custom boxes with README
- Test multi-machine setups thoroughly
- Use NFS for better synced folder performance
- Keep Vagrant updated
- Clean up unused boxes regularly
Resources
- Documentation: https://www.vagrantup.com/docs
- Box Catalog: https://app.vagrantup.com/boxes/search
- GitHub: https://github.com/hashicorp/vagrant
- Community: https://discuss.hashicorp.com/c/vagrant
Quick Reference
Common Vagrantfile Patterns
# Basic
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/focal64"
config.vm.network "private_network", ip: "192.168.33.10"
config.vm.synced_folder ".", "/vagrant"
end
# With provisioning
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/focal64"
config.vm.provision "shell", path: "setup.sh"
end
# Multi-machine
Vagrant.configure("2") do |config|
config.vm.define "web"
config.vm.define "db"
end
Common Commands
vagrant up # Start VM
vagrant ssh # SSH into VM
vagrant halt # Stop VM
vagrant destroy # Delete VM
vagrant reload # Restart with new config
vagrant provision # Run provisioners
vagrant status # Show status
vagrant global-status # All VMs
vagrant box list # List boxes
vagrant plugin list # List plugins