Vagrant for Development Environments

Vagrant automates development environment setup by providing lightweight, reproducible virtual machines configured through code. Vagrant enables developers to work in environments identical to production, eliminating dependency and configuration inconsistencies. This guide covers installation, Vagrantfile configuration, box management, provisioning strategies, networking, multi-machine setups, and synced folders.

Table of Contents

  1. Vagrant Overview
  2. Installation and Setup
  3. Vagrantfile Configuration
  4. Box Management
  5. Provisioning Strategies
  6. Networking Configuration
  7. Synced Folders
  8. Multi-Machine Environments
  9. Advanced Features
  10. Conclusion

Vagrant Overview

Vagrant simplifies development environment creation by automating VM provisioning and configuration. Instead of manually installing software on development systems, Vagrant defines infrastructure as code, enabling teams to share identical environments.

Key benefits:

  • Reproducibility: Same environment for all developers
  • Consistency: Development matches production
  • Isolation: No system package pollution
  • Collaboration: Share Vagrant configurations via version control
  • Multiple Providers: VirtualBox, Docker, Hyper-V support
  • Simple Workflow: One command to start working

Vagrant architecture:

┌────────────────────────┐
│  Development Machine   │
├────────────────────────┤
│  Vagrant CLI           │
│  (vagrant up/ssh/etc)  │
└──────────┬─────────────┘
           │
           ▼
┌────────────────────────┐
│  Hypervisor/Provider   │
│  (VirtualBox/Docker)   │
└──────────┬─────────────┘
           │
           ▼
┌────────────────────────┐
│  Virtual Machine       │
│  (Development OS)      │
│  Provisioned with code |
└────────────────────────┘

Installation and Setup

Install Vagrant and required dependencies.

Install Vagrant:

# macOS
brew install vagrant

# Ubuntu/Debian
wget https://releases.hashicorp.com/vagrant/2.4.0/vagrant_2.4.0_linux_amd64.zip
unzip vagrant_2.4.0_linux_amd64.zip
sudo mv vagrant /usr/local/bin/

# Windows
choco install vagrant

# Verify installation
vagrant version

Install VirtualBox provider:

# macOS
brew install virtualbox

# Ubuntu/Debian
sudo apt-get install virtualbox

# Windows
choco install virtualbox

# Verify
vboxmanage --version

Install additional providers (optional):

# Docker provider
vagrant plugin install vagrant-docker-compose

# Hyper-V provider (Windows)
vagrant plugin install vagrant-hyperv

# List installed plugins
vagrant plugin list

Vagrantfile Configuration

The Vagrantfile defines VM configuration, provisioning, and networking.

Basic Vagrantfile:

# Vagrantfile
Vagrant.configure("2") do |config|
  # Base box
  config.vm.box = "ubuntu/focal64"
  config.vm.box_version = "20.04.5"

  # Hostname
  config.vm.hostname = "devenv"

  # Network
  config.vm.network "private_network", ip: "192.168.33.10"

  # Provider configuration
  config.vm.provider "virtualbox" do |vb|
    vb.gui = false
    vb.cpus = 2
    vb.memory = 2048
  end

  # Provisioning
  config.vm.provision "shell", inline: <<-SHELL
    apt-get update
    apt-get install -y nginx
  SHELL
end

Comprehensive Vagrantfile:

# Vagrantfile
Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/focal64"
  config.vm.box_version = "20.04.5"
  config.vm.box_check_update = false

  # Hostname and domain
  config.vm.hostname = "webapp.local"

  # Networks
  # Private network for host-only access
  config.vm.network "private_network", ip: "192.168.33.10"

  # Port forwarding
  config.vm.network "forwarded_port", guest: 80, host: 8080
  config.vm.network "forwarded_port", guest: 443, host: 8443
  config.vm.network "forwarded_port", guest: 3000, host: 3000

  # Public network (bridged)
  # config.vm.network "public_network"

  # Synced folder
  config.vm.synced_folder ".", "/vagrant", type: "virtualbox"
  config.vm.synced_folder "app", "/opt/app", owner: "www-data", group: "www-data"

  # VirtualBox configuration
  config.vm.provider "virtualbox" do |vb|
    vb.name = "webapp-dev"
    vb.gui = false
    vb.cpus = 2
    vb.memory = 2048
    vb.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
  end

  # Shell provisioning
  config.vm.provision "shell", path: "scripts/bootstrap.sh"

  # Shell inline provisioning
  config.vm.provision "shell", inline: <<-SHELL
    echo "Provisioning development environment"
    systemctl start nginx
    systemctl enable nginx
  SHELL

  # File provisioning
  config.vm.provision "file", source: "config/nginx.conf",
    destination: "/tmp/nginx.conf"

  # Ansible provisioning
  config.vm.provision "ansible" do |ansible|
    ansible.playbook = "provisioning/playbook.yml"
    ansible.become = true
  end

  # Docker provisioning
  config.vm.provision :docker do |d|
    d.images = ["ubuntu:20.04"]
  end

  # Disable default share
  # config.vm.synced_folder ".", "/vagrant", disabled: true
end

Configuration with variables:

# Vagrantfile
require 'yaml'

# Load configuration from file
config_file = File.expand_path('vagrant.yml', __dir__)
config_data = YAML.load_file(config_file) if File.exist?(config_file)
config_data ||= {}

Vagrant.configure("2") do |config|
  config.vm.box = config_data['box'] || "ubuntu/focal64"
  config.vm.hostname = config_data['hostname'] || "vagrant"

  config.vm.provider "virtualbox" do |vb|
    vb.cpus = config_data['cpus'] || 2
    vb.memory = config_data['memory'] || 2048
  end

  config.vm.network "private_network",
    ip: config_data['ip'] || "192.168.33.10"

  (config_data['ports'] || {}).each do |guest, host|
    config.vm.network "forwarded_port",
      guest: guest, host: host
  end
end

vagrant.yml:

# vagrant.yml
box: ubuntu/focal64
hostname: webapp
cpus: 4
memory: 4096
ip: 192.168.33.10

ports:
  80: 8080
  443: 8443
  3000: 3000
  5432: 5432

Box Management

Vagrant boxes are base images for virtual machines.

Find and use boxes:

# Search for boxes
vagrant box search ubuntu

# Add a box
vagrant box add ubuntu/focal64

# Specific version
vagrant box add ubuntu/focal64 --box-version 20.04.5

# Add from URL
vagrant box add mybox https://example.com/boxes/mybox.box

# List boxes
vagrant box list

# Remove box
vagrant box remove ubuntu/focal64

# Update boxes
vagrant box update

Create custom box:

# Package VM as box
vagrant package --output mybox.box

# Import box
vagrant box add mybox file://mybox.box

# Add to Vagrant Cloud
# Login
vagrant login

# Push to cloud
vagrant cloud publish myorg/mybox 1.0 virtualbox mybox.box

Provisioning Strategies

Provision VMs with required software and configuration.

Shell provisioning:

# Inline script
config.vm.provision "shell", inline: <<-SHELL
  apt-get update
  apt-get install -y nginx
  systemctl start nginx
SHELL

# External script
config.vm.provision "shell", path: "bootstrap.sh"

# With arguments
config.vm.provision "shell",
  path: "bootstrap.sh",
  args: ["nginx", "php-fpm"]

# As different user
config.vm.provision "shell",
  inline: "echo 'Hello from non-root'",
  privileged: false

bootstrap.sh:

#!/bin/bash
set -e

echo "Installing packages..."
apt-get update
apt-get install -y \
  nginx \
  curl \
  wget \
  vim \
  git

echo "Configuring services..."
systemctl start nginx
systemctl enable nginx

echo "Bootstrap complete"

File provisioning:

# Copy file
config.vm.provision "file",
  source: "config/nginx.conf",
  destination: "/tmp/nginx.conf"

# Copy directory
config.vm.provision "file",
  source: "app/",
  destination: "/opt/app"

# Run as root after copying
config.vm.provision "shell",
  inline: "mv /tmp/nginx.conf /etc/nginx/nginx.conf && systemctl reload nginx",
  privileged: true

Ansible provisioning:

config.vm.provision "ansible" do |ansible|
  ansible.playbook = "provisioning/playbook.yml"
  ansible.inventory_path = "provisioning/inventory"
  ansible.become = true
  ansible.extra_vars = {
    ansible_python_interpreter: "/usr/bin/python3"
  }
end

provisioning/playbook.yml:

---
- hosts: all
  become: yes
  
  vars:
    packages:
      - nginx
      - curl
      - git
  
  tasks:
    - name: Update package cache
      apt:
        update_cache: yes
    
    - name: Install packages
      apt:
        name: "{{ packages }}"
        state: present
    
    - name: Start nginx
      systemd:
        name: nginx
        state: started
        enabled: yes

Docker provisioning:

config.vm.provision :docker do |d|
  # Install Docker
  d.version = "latest"

  # Pull images
  d.images = [
    "ubuntu:20.04",
    "postgres:13",
    "redis:latest"
  ]

  # Build image
  d.build_image "/vagrant/Dockerfile",
    args: "-t myapp:latest"

  # Run container
  d.run "postgres:13",
    args: "-d --name db -e POSTGRES_PASSWORD=secret"

  # Install docker-compose
  d.install_docker_compose = true
end

Networking Configuration

Configure networking for VM access and communication.

Network types:

# Private network (host-only)
config.vm.network "private_network", ip: "192.168.33.10"

# Dynamic IP
config.vm.network "private_network", type: "dhcp"

# Port forwarding
config.vm.network "forwarded_port", guest: 80, host: 8080

# Public network (bridged)
config.vm.network "public_network",
  bridge: "en0: Wi-Fi (AirPort)"

# Multiple networks
config.vm.network "private_network", ip: "192.168.33.10"
config.vm.network "private_network", ip: "192.168.34.10"

Advanced networking:

# Configure hostname and DNS
config.vm.hostname = "webapp.local"
config.vm.network "private_network",
  ip: "192.168.33.10",
  hostname: true

# Port forwarding with autocorrect
config.vm.network "forwarded_port",
  guest: 80,
  host: 8080,
  auto_correct: true

# Multiple port forwards
[
  { guest: 80, host: 8080 },
  { guest: 443, host: 8443 },
  { guest: 3000, host: 3000 }
].each do |port|
  config.vm.network "forwarded_port",
    guest: port[:guest],
    host: port[:host]
end

# Configure /etc/hosts on host
config.hostsupdater.aliases = ["webapp.local", "api.local"]

Synced Folders

Share folders between host and guest for development.

Basic synced folders:

# Default sync (current directory to /vagrant)
config.vm.synced_folder ".", "/vagrant", type: "virtualbox"

# Sync app directory
config.vm.synced_folder "app", "/opt/app"

# Sync with ownership
config.vm.synced_folder "app", "/opt/app",
  owner: "www-data",
  group: "www-data"

# Sync with mount options
config.vm.synced_folder ".", "/vagrant",
  mount_options: ["dmode=755", "fmode=644"]

# Disable default share
config.vm.synced_folder ".", "/vagrant", disabled: true

NFS synced folders (faster for large directories):

# NFS requires nfs-utils on host
config.vm.synced_folder ".", "/vagrant", type: "nfs"

# With specific options
config.vm.synced_folder "app", "/opt/app",
  type: "nfs",
  nfs_version: 4,
  nfs_udp: false

rsync synced folders (one-way sync):

# Auto-sync on vagrant up
config.vm.synced_folder "app", "/opt/app",
  type: "rsync",
  rsync__auto: true,
  rsync__exclude: [".git", "node_modules", ".env"]

# Manual sync
# vagrant rsync
# vagrant rsync-auto

Multi-Machine Environments

Define complex environments with multiple VMs.

Multi-machine Vagrantfile:

# Vagrantfile
Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/focal64"

  # Web server
  config.vm.define "web" do |web|
    web.vm.hostname = "web.local"
    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.cpus = 2
      vb.memory = 2048
      vb.name = "webapp-web"
    end
    
    web.vm.provision "shell", path: "scripts/install-nginx.sh"
  end

  # Database server
  config.vm.define "db" do |db|
    db.vm.hostname = "db.local"
    db.vm.network "private_network", ip: "192.168.33.11"
    
    db.vm.provider "virtualbox" do |vb|
      vb.cpus = 2
      vb.memory = 4096
      vb.name = "webapp-db"
    end
    
    db.vm.provision "shell", path: "scripts/install-postgres.sh"
  end

  # Cache server
  config.vm.define "cache" do |cache|
    cache.vm.hostname = "cache.local"
    cache.vm.network "private_network", ip: "192.168.33.12"
    
    cache.vm.provider "virtualbox" do |vb|
      vb.cpus = 1
      vb.memory = 1024
      vb.name = "webapp-cache"
    end
    
    cache.vm.provision "shell", path: "scripts/install-redis.sh"
  end
end

Control multi-machine environments:

# Start all machines
vagrant up

# Start specific machine
vagrant up web

# SSH into specific machine
vagrant ssh db

# Provision specific machine
vagrant provision cache

# Status of all machines
vagrant status

# Destroy specific machine
vagrant destroy db

Advanced Features

Triggers and callbacks:

# Run script before/after actions
config.trigger.before :up do |trigger|
  trigger.info = "Preparing environment..."
  trigger.run = {inline: "echo 'Starting up'"}
end

config.trigger.after :up do |trigger|
  trigger.info = "Environment ready"
  trigger.run = {path: "scripts/post-up.sh"}
end

# Before provision
config.trigger.before :provision do |trigger|
  trigger.info = "Running pre-provision checks"
end

Plugins:

# Useful plugins
vagrant plugin install vagrant-vbguest      # VirtualBox guest additions
vagrant plugin install vagrant-cachier      # Shared cache directory
vagrant plugin install vagrant-env          # .env file support
vagrant plugin install vagrant-docker-login # Docker registry auth

# List plugins
vagrant plugin list

# Update plugins
vagrant plugin update

# Remove plugin
vagrant plugin uninstall vagrant-vbguest

Debugging:

# Verbose output
vagrant up --debug

# View logs
cat ~/.vagrant.d/logs/vagrant.log

# SSH with verbose
vagrant ssh -- -vvv

# Test connectivity
vagrant ssh -c "ping 8.8.8.8"

Conclusion

Vagrant eliminates development environment inconsistencies by providing reproducible, infrastructure-as-code virtual machines. By configuring VMs through Vagrantfiles, provisioning with shells or configuration management, and sharing configurations through version control, teams ensure all developers work in identical environments. Combined with Docker containers and orchestration tools, Vagrant provides a foundation for consistent development and testing workflows.