Vagrantfile Configuration Tips & Tricks
When I first started working with vagrant, Vagrantfiles 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:
- creates an
nfs_fix.bat
file that the user can run to enable Windows NFS support - 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
, andNFS-Administration
features - changes the Windows registry
ClientForNFS
settingsAnonymousUid
andAnonymousGid
to1000
, which matches thevagrant
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!