Vagrantfile Configuration Tips & Tricks

2/7/2021

When I first started working with vagrantVagrantfiles were a black box to me. Someone gave me one that worked and I just copied it whenever I started a new project. Over time, I have come to tweak almost every aspect of my Vagrantfiles and want to share some of my favorites.

If you're new to Vagrant, take a look at HashiCorp's very simple introduction. Vagrantfiles, in turn, are simply scripts that uses Ruby syntax to "describe the type of machine required for a [vagrant] project". Here's a basic example: 

Vagrant.configure("2") do |config|
  config.vm.box = "aracpac/ubuntu20"
end

Issuing vagrant up from a folder that contains this Vagrantfile will initialize a new project using the aracpac/ubuntu20 image from Vagrant Cloud.

Because Vagrantfiles are written in Ruby, they can be used to do anything that Ruby can do, making them incredibly powerful bootstrapping tools. Let's look at a more complex example. Recently, I wrote about AracPac, a series of vagrant boxes I built for web developers; here's a Vagrantfile from that project:

# -*- mode: ruby -*-
# vi: set ft=ruby :

# https://github.com/aracpac

########################################################################################################################
# EDIT THESE VARIABLES TO SUIT YOUR NEEDS ##############################################################################
########################################################################################################################
########################################################################################################################
vagrantConfig = Hash.new
vagrantConfig[ "ip" ] = "192.168.10.10" # local ip for the box (used when 'private_network' is set to 'ip')
vagrantConfig[ "hostname" ] = "dev.local" # primary hostname for the box
vagrantConfig[ "aliases" ] = [ "admin.dev.local" ]; # additional hostnames for the box
vagrantConfig[ "remote_share_point" ] = "/var/www" # the remote share point mapped in the guest's /etc/exports
vagrantConfig[ "remote_share_point_windows" ] = "\\var\\www\\html" # on windows, the path must be escaped
vagrantConfig[ "local_share_point" ] = "./www" # the local mount point
vagrantConfig[ "local_share_point_windows" ] = "X:" # must correspond to an unmapped drive
########################################################################################################################
########################################################################################################################
# DO NOT EDIT PAST THIS POINT ##########################################################################################
########################################################################################################################

if Vagrant::Util::Platform.windows?
    $nfs_fix = <<-NFSFIX
    @ECHO OFF
    :: This sets the default NFS user to 1000, which maps to the vagrant user in the AracPac development box :::::::::::::::
    :: use a temporary vb script to rerun this script as an administrator
    set "params=%*"
    cd /d "%~dp0" && ( if exist "%temp%\\getadmin.vbs" del "%temp%\\getadmin.vbs" ) && fsutil dirty query %systemdrive% 1>nul 2>nul || (  echo Set UAC = CreateObject^("Shell.Application"^) : UAC.ShellExecute "cmd.exe", "/k cd ""%~sdp0"" && %~s0 %params%", "", "runas", 1 >> "%temp%\\getadmin.vbs" && "%temp%\\getadmin.vbs" && exit /B )
    ECHO Enabling necessary windows features
    powershell Enable-WindowsOptionalFeature -Online -FeatureName ServicesForNFS-ClientOnly -All
    powershell Enable-WindowsOptionalFeature -Online -FeatureName ClientForNFS-Infrastructure -All
    powershell Enable-WindowsOptionalFeature -Online -FeatureName NFS-Administration -All
    ECHO Setting anonymous uid to 1000
    REG ADD HKLM\\Software\\Microsoft\\ClientForNFS\\CurrentVersion\\Default /f /v AnonymousUid /t REG_DWORD /d 1000
    ECHO Setting anonymous guid to 1000
    REG ADD HKLM\\Software\\Microsoft\\ClientForNFS\\CurrentVersion\\Default /f /v AnonymousGid /t REG_DWORD /d 1000
    ECHO Setting default windows filemode to 775
    nfsadmin client localhost config fileaccess=775
    ECHO Restarting the NFS Client
    net stop nfsclnt /y
    net stop nfsrdr /y
    net start nfsrdr /y
    net start nfsclnt /y
    ECHO Done!
    NFSFIX

    $nfsmount = <<-NFSMOUNT
    net use #{ vagrantConfig[ "local_share_point_windows" ] } \\\\#{ vagrantConfig[ "hostname" ] }#{ vagrantConfig[ "remote_share_point" ] }
    NFSMOUNT

    $nfsumount = <<-NFSUMOUNT
    net use #{ vagrantConfig[ "local_share_point_windows" ] } /delete /y
    NFSUMOUNT

    # create a batch file to fix nfs read-only issues on windows ()only needs to be run once per host machine)
    unless File.exist?( './nfs_fix.bat' )
        File.write( './nfs_fix.bat', $nfs_fix )
    end
else
    $nfsmount = <<-NFSMOUNT
    sudo mount -t nfs -o rw,rsize=8192,wsize=8192 #{ vagrantConfig[ "ip" ] }:#{ vagrantConfig[ "remote_share_point" ] } #{ vagrantConfig[ "local_share_point" ] }
    NFSMOUNT

    $nfsumount = <<-NFSUMOUNT
    sudo umount -f #{ vagrantConfig[ "local_share_point" ] }
    NFSUMOUNT

    # create the local mount point for the remote NFS folder if it doesn't exist
    unless File.exist?( vagrantConfig[ "local_share_point" ] )
        FileUtils.mkdir_p vagrantConfig[ "local_share_point" ]
    end
end

Vagrant.configure( "2" ) do |config|
    # configure vagrant hostmanager if it's installed
    if Vagrant.has_plugin?( "vagrant-hostmanager" )
        config.hostmanager.enabled = true
        config.hostmanager.manage_host = true
        config.hostmanager.manage_guest = true
        config.hostmanager.ignore_private_ip = false
        config.hostmanager.include_offline = true
    end
    # configure vagrant
    config.vm.box = "aracpac/ubuntu20"
    config.vm.box_version = "1.0"
    config.vm.define vagrantConfig[ "hostname" ] do |node|
        node.vm.hostname = vagrantConfig[ "hostname" ]
        node.vm.network "private_network", ip: vagrantConfig[ "ip" ]
        if Vagrant.has_plugin?( "vagrant-hostmanager" )
            if  !vagrantConfig[ "aliases" ].empty?
                node.hostmanager.aliases = vagrantConfig[ "aliases" ]
            end
        end
        # configure triggers to mount and unmount nfs share
        if Vagrant::Util::Platform.windows?
            node.trigger.after [ :up, :provision ] do |trigger|
                if `net use #{ vagrantConfig[ "local_share_point_windows" ] } 2> nul` == ""
                    trigger.info = "Mounting NFS to #{ vagrantConfig[ "local_share_point_windows" ] }"
                    trigger.run = { inline: $nfsmount }
                else
                    trigger.info = "#{ vagrantConfig[ "local_share_point_windows" ] } is already mapped, skipping"
                end
            end
            node.trigger.after [ :destroy, :halt ] do |trigger|
                if `net use #{ vagrantConfig[ "local_share_point_windows" ] } 2> nul` == ""
                    trigger.info = "#{ vagrantConfig[ "local_share_point_windows" ] } is not mapped, skipping"
                else
                    trigger.info = "Unmounting NFS from #{ vagrantConfig[ "local_share_point_windows" ] }"
                    trigger.run = { inline: $nfsumount }
                end
            end
        else
            node.trigger.after [ :up, :provision ] do |trigger|
                if `mount | grep #{ File.expand_path vagrantConfig[ "local_share_point" ] }` == ""
                    trigger.info = "Mounting NFS to #{ vagrantConfig[ "local_share_point" ] }"
                    trigger.run = { inline: $nfsmount }
                else
                    trigger.info = "#{ vagrantConfig[ "local_share_point" ] } is already mounted, skipping"
                end
            end
            node.trigger.after [ :destroy, :halt ] do |trigger|
                if `mount | grep #{ File.expand_path vagrantConfig[ "local_share_point" ] }` == ""
                    trigger.info = "#{ vagrantConfig[ "local_share_point" ] } is not mounted, skipping"
                else
                    trigger.info = "Unmounting NFS from #{ vagrantConfig[ "local_share_point" ] }"
                    trigger.run = { inline: $nfsumount }
                end
            end
        end
    end

    # uncomment to expose ports in the guest (vagrant) machine to your local network
    # config.vm.network "forwarded_port", guest: 80, host: 8080, id: "http", protocol: "tcp", auto_correct: true
    # config.vm.network "forwarded_port", guest: 443, host: 8443, id: "https", protocol: "tcp", auto_correct: true
    # config.vm.network "forwarded_port", guest: 3306, host: 13306, id: "mysql", protocol: "tcp", auto_correct: true

    # configure virtualbox
    config.vm.provider :virtualbox do |vb|
        vb.name = config.vm.hostname
        vb.gui = false
        # enable host i/o cache on the sata controller (see https://www.virtualbox.org/manual/ch05.html#iocaching)
        vb.customize ["storagectl", :id, "--name", "SATA Controller",  "--hostiocache", "on"]
        # use nameservers based on host machine. fixes broken /etc/resolv.conf (see https://www.virtualbox.org/manual/ch09.html#nat_host_resolver_proxy)
        vb.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
        vb.customize ["modifyvm", :id, "--natdnsproxy1", "on"]
        # change network card type for better performance (see https://www.virtualbox.org/manual/ch06.html#nichardware)
        vb.customize ["modifyvm", :id, "--nictype1", "virtio"]
        vb.customize ["modifyvm", :id, "--nictype2", "virtio" ]
        # enable pae/nx (see https://www.virtualbox.org/manual/ch03.html#settings-processor)
        vb.customize ["modifyvm", :id, "--pae", "on"]
        # enable kvm paravirtualization (see https://www.virtualbox.org/manual/ch10.html#gimproviders)
        vb.customize ["modifyvm", :id, "--paravirtprovider", "kvm"]
        # lower time sync threshold (see https://www.virtualbox.org/manual/ch09.html#idm8477)
        vb.customize [ "guestproperty", "set", :id, "/VirtualBox/GuestAdd/VBoxService/--timesync-set-threshold", 1000 ]
        # 2 GB RAM, 2 CPUs, capped at 75% (see https://unix.stackexchange.com/a/325959/138364)
        vb.customize [ "modifyvm", :id, "--cpuexecutioncap", "75" ]
        vb.memory = 2048
        vb.cpus = 2
    end
end

Let's walk through it, from top to bottom.

Lines 10-17 define a Ruby hash, vagrantConfig, and set various hash keys to configuration values. Later in the script, the vagrantConfig hash keys are referenced instead of literal values, allowing users to modify the Vagrantfile's behaviour from a single point at the top of the script. Specifically, users can modify the IP address that will be associated with the vagrant box, the hostname and aliases that will be added to the local hosts file (if the vagrant-hostmanager plugin is present) and the local and remote mount points that will be mounted via NFS.

Line 23 detects if we're running on Windows, and if we are:

  1. creates an nfs_fix.bat file that the user can run to enable Windows NFS support
  2. defines triggers for mounting and dismounting an NFS share from the guest machine to a mapped drive on the host

The nfs_fix.bat file does several things, all of which are necessary to mount NFS shares on a Windows machine. It:

  • enables the Windows ServicesForNFS-ClientOnly, ClientForNFS-Infrastructure, and NFS-Administration features
  • changes the Windows registry ClientForNFS settings AnonymousUid and AnonymousGid to 1000, which matches the vagrant user in the guest machine
  • sets the default Windows filemode to 755 (this allows files created through the Windows NFS mount to be group accessible in the guest machine)

Cool! So we can use a Ruby heredoc string literal to create an arbitrary script on the user's machine.

Similarly, lines 48-54 and 61-67 define in-line scripts that later hook into vagrant events on lines 96-130. In our case, we NFS-mount on vagrant up and vagrant provision, and dismount on vagrant destroy and vagrant halt. Don't like the NFS mount options I've used? No problem, just modify those lines.

We can also detect if the user has a specific vagrant plugin installed, and if they do, add extra configuration options for it. For instance, lines 90-94 test for the vagrant-hostmanager plugin, and if it's present, configure it accordingly. We could even raise an exception if the plugin was missing:

unless Vagrant.has_plugin?( "vagrant-hostmanager" )
  raise "!*! Plugin required !*!\n\n\tvagrant plugin install vagrant-hostmanager\n"
end

Lines 134-136 can be uncommented to add port forwarding rules that allow other machines to access the vagrant box using our host machine's IP address. This is really handy when testing a layout on multiple local screens.

Vagrantfiles also allow us to configure almost every aspect of the corresponding VirtualBox machine. For example, line 143 enables host input/output caching. I've curated a set of VirtualBox tweaks that I find useful, each of which has an in-line comment linking either to the manual or an external source that describes the option.

Finally, if you want your Vagrantfile to load a private box that hasn't been uploaded to Vagrant Cloud, you can simply replace line 140 with the name of private box and link to a JSON file that describes the box. For instance:

config.vm.box = "your-box-name"
config.vm.box_url = "https://path/to/your/vagrant.json"

Where https://path/to/your/vagrant.json contains a manifest like:

{
  "name": "your-box-name",
  "description": "Your box description",
  "versions": [{
    "version": "1.0",
    "providers": [{
      "name": "virtualbox",
      "url": "https://path/to/your/virtualbox.box",
      "checksum_type": "sha1",
      "checksum": "checksum"
    }]
  }]
}

Just be sure to replace the checksum value to the actual sha1 checksum of https://path/to/your/virtualbox.box.

So to summarize, Vagrantfiles can:

  • detect the user's operating system and execute OS-specific code (checkout the Vagrant::Util class for extra goodies)
  • trigger actions at certain points of the vagrant lifecycle (check out the docs for a list of all the triggers)
  • create arbitrary files on the user's machine (and really, the sky's the limit here since you can output shell scripts)
  • interact with the VirtualBox API
  • configure port forwarding
  • load private boxes

Not too shabby for a configuration file!