Vagrant is an incredible tool to help get developers setup quickly, and provide repeatable environment similar to production. However everybody has different tools and tricks to make themselves more productive, and not having these within the vagrant environment can slow them down. These things include installing programs (vim, emacs, htop), setting up your custom cofiguration (.vimrc, .emacs), or tweaking vagrant’s memory or CPU settings. To allow people to make these customizations I’ve successfully used the below techniques. I’ve found enabling these items allow people to be more comfortable with doing a vagrant destroy and not feeling like they will lose any custom setup, and can also allow new features to be tested before pushing them to the larger team.

None of the techniques here are real advanced, but they were non-obvious to me when setting up my first vagrant file so I figured I would share.

Customization via Environment Variables

If you want to allow users to customize parts of a virtual machine, or have different flags that can be passed into your provisioning scripts using environment variables is a great way to do it. With environment variables you can allow your users to customize the number of CPUs, the amount of memory to allocate, and even provide feature flags to the provisioning script to enable experimental behavior. You can do this as shown below:

 1 Vagrant.configure("2") do |config|
 2 
 3     # Environment variable customizations can be setup this way. Vagrantfiles
 4     # are just ruby files so you can do powerful things.
 5     num_cpus = ENV['VAGRANT_CPU']
 6     mem_size = ENV['VAGRANT_MEM']
 7     beta_tester = ENV['VAGRANT_BETA'] == 'true'
 8 
 9     if beta_tester
10         # Setup beta features here
11     else
12         ENV['VAGRANTMANAGER_BETA'] = 'false'
13     end
14 
15     config.vm.box = 'ubuntu/trusty64'
16 
17     config.vm.provider :virtualbox do |vb|
18     	# The vm setup can be customized based on the environment variables
19     	# as could mount points, ip addresses, or anything else.
20         vb.customize ["modifyvm", :id, "--memory", mem_size ? mem_size : 2048]
21         vb.customize ["modifyvm", :id, "--cpus",  num_cpus ? num_cpus : 2]
22     end
23 
24     config.vm.provision :shell, :inline => <<-SH
25       # By using inline scripts you can also pass values into the provisioning
26       # step where they can be leveraged by your provisioning scripts. Below
27       # you see an example of how I've leveraged this to allow features to be
28       # rolled out slowly using an opt in beta tester environment variable.
29       set -eux
30       export VAGRANTMANAGER_BETA=#{ENV['VAGRANT_BETA']}
31 
32       # This will call the startup.sh file to run the provisioning scripts.
33       # I prefer doing provisioning using scripts since production will almost
34       # always do this as well. Then these scripts can call into chef or some
35       # other config management tool if advanced things are necessary.
36       source /vagrant/startup.sh
37     SH
38 end

As you can see above you can enable powerful customizations using environment variables. You can also use this technique to pass in user credentials that you might not want to hard code into your vagrant box.

Customization via Mount Points

Environment variables are great but they don’t allow you do things like installing custom packages or configuration. To allow for this one trick is to check for a custom local path, and if that path is there you can leverage it as an extension point. To do this you might do something like:

 1 Vagrant.configure("2") do |config|
 2 
 3   # ... Snip ...
 4 
 5   # mount a custom script if the specific folder is found on the host disk.
 6   # Note that you could make this more flexible using environment variables
 7   # if desired.
 8   bootstrap_dir = File.expand_path("~/vmbootstrap")
 9   if File.directory?(bootstrap_dir)
10     # This will mount the users ~/vmbootstrap directory into the /opt/vmbootstrap
11     config.vm.synced_folder bootstrap_dir, "/opt/vmbootstrap"
12   end
13 
14   config.vm.provision :shell, :inline => <<-SH
15     set -eux
16     source /vagrant/startup.sh
17   SH
18 end

Then in your startup.sh file you can provide a custom execution hook into the provisioning script by doing something like:

 1 #!/bin/bash -eux
 2 
 3 # Do what ever provisioning you want here
 4 # ...
 5 
 6 # Call into your users custom setup script if it exists. This allows your
 7 # users to have scripts that run at provisioning time, which allows them
 8 # them install editors, user config files, debugging tools, or whatever
 9 # makes them happy. Since the whole vmbootstrap folder is mounted multiple
10 # files can be put into that file and copied around as well.
11 bootstrap_script='/opt/vmbootstrap/setup'
12 if [ -x $bootstrap_script ] ; then
13   echo 'running custom setup script'
14   sudo $bootstrap_script
15 fi

Naturally this technique can be done an infinite number of ways, but the example above is a simple approach that has served me well.

Conclusion

The two techniques above can be composed and extended to provide very complex customizations, but they can also be abused to make Vagrantfiles that are hard to follow and environments that don’t work. At first I hesitated building these hooks into the team’s vagrant images, but I’ve found that trusting people with these hooks has worked out well and made everybody happier. Most people probably don’t know the hooks exist which is just fine, but these customizations allow the power users and tinkerers to be more productive. I find these customization points allow Vagrant to work better for me.