Red Green Repeat Adventures of a Spec Driven Junkie

Jekyll Vagrant Box

I’ve been working on a system where the Ruby install was a bit messed up on the host. Normally, I would fix this, but I wanted to get the my blog working to write an article.

With Ruby not working, I definitely saw a Rabbit Hole of stuff to fix: get rvm setup properly, install newer Ruby, make sure Gems work, etc.

I’ve done this process before and it can be frustrating because I don’t know how long it would take, even though I have done it before (sounds silly, but it’s true!) The main reason is I don’t know how the system got into such a state, so I don’t know what to undo.

Instead of fixing this problem directly on the system, I want to have a solution that can work for any system, Ruby setup or not, new or old, now and in the future:

Setup Vagrant to automatically configure a virtualbox to get everything needed for Jekyll to run.

Obvious, right? :-)

Requirements

If you don’t know what Vagrant or Virtualbox is, please read my introductory article on them.

If you don’t know what Jekyll is: Jekyll is a static site generator. It will take files formatted in Markdown and convert them into HTML and create a whole website.

Jekyll is a great tool to blog and focus on content, not markup.

More info on Jekyll here

Installing

I will be starting with a bare Vagrantfile and installing onto a Ubuntu 16.04 box (Xenial). So the Vagrantfile will be:

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/xenial64"
end

Installing Ruby

I usually use rvm to manage different version of Ruby in any system. Initially, I followed the standard installation method I would take to install, right from the rvm site

$ gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
$ curl -sSL https://get.rvm.io | bash -s
$ rvm install 2.4

Putting the above script into the Vagrantfile as:

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/xenial64"

  config.vm.provision "shell", inline: <<-SHELL
    gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
    curl -sSL https://get.rvm.io | bash -s
    rvm install 2.4
  SHELL
end

The above Vagrantfile results in a working version of Ruby, BUT, when I wanted to install items using bundler, these errors started occuring:

$ gem install bundler
Fetching: bundler-1.16.0.gem (100%)
ERROR:  While executing gem ... (Gem::FilePermissionError)
    You don't have write permissions for the /usr/local/rvm/gems/ruby-2.4.1 directory.

Trying with sudo prepended:

$ sudo gem install bundler
sudo: gem: command not found

This made me think: hmm… having rvm installed by root is not good. rvm is a user level utility and using sudo for everything just won’t work in all cases.

As much as I want to just run things as root all the time, being the root user is just too crazy for me, even in a virtual machine.

Remember, with great power comes great
responsibility

Better way to install Ruby

My desire is to use rvm to manage different ruby versions and to have vagrant install everything for me.

I tried:

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/xenial64"

  config.vm.provision "shell", inline: <<-SHELL
    su - vagrant
    gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
    curl -sSL https://get.rvm.io | bash -s
    rvm install 2.4
  SHELL
end

From my understanding, makes root the vagrant user and executes commands from that point forward.

Nope, rvm was still installed with root privileges.

I also tried:

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/xenial64"

  config.vm.provision "shell", privilieged: false, inline: <<-SHELL
    gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
    curl -sSL https://get.rvm.io | bash -s
    rvm install 2.4
  SHELL
end

But that did not work as well. Vagrant didn’t want to boot up for some reason.

The rvm site had the answer, use an external shell script.

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/xenial64"

  config.vm.provision :shell, path: "install_rvm.sh", args: "stable", privileged: false
  config.vm.provision :shell, path: "install_ruby.sh", args: "2.4", privileged: false
end

and the file contents of install_rvm.sh are:

#!/usr/bin/env bash

gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
curl -sSL https://get.rvm.io | bash -s $1

and install_ruby.sh are:

#!/usr/bin/env bash

source $HOME/.rvm/scripts/rvm || source /etc/profile.d/rvm.sh

rvm use --default --install $1

shift

if (( $# ))
then gem install $@
fi

rvm cleanup all

Place those two files in the same directory as the Vagrantfile and run vagrant up.

Now, gem can install bundler without root permissions:

$ gem install bundler
Successfully installed bundler-1.16.0
Parsing documentation for bundler-1.16.0
Done installing documentation for bundler after 5 seconds
1 gem installed

Installing Node

Of course, where there’s JavaScript, there’s an execjs requirement!

$ jekyll s
jekyll 3.3.1 | Error:  Could not find a JavaScript runtime. See https://github.com/rails/execjs for a list of available runtimes.

I defaulted to installing Node.JS for the ExecJS runtime:

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/xenial64"

  config.vm.provision "shell", inline: <<-SHELL
    curl -sL https://deb.nodesource.com/setup_6.x | bash -
    apt-get update
    apt-get install -y nodejs

    # update npm with newer version of node
    npm install npm --global
  SHELL
end

Keep it simple!

Networking?

After installing jekyll with:

$ gem install jekyll

and starting up:

$ jekyll s

I ran into a problem, how to access the site?? The IP address of the server is automatically resolved internally when I login through SSH.

Public Network

To setup an IP address that can be accessible from the host computer, add the public_network option to the configuration:

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/xenial64"

  config.vm.network "public_network"
end

On the next time the host system starts, there will be a question about which network adapter to use:

Available bridged network interfaces:
1) en1: Wi-Fi (AirPort)
2) en3: Thunderbolt 1
3) en0: Ethernet
4) bridge0
5) p2p0
==> default: When choosing an interface, it is usually the one that is
==> default: being used to connect to the internet.
    default: Which interface should the network bridge to? 1
==> default: Preparing network interfaces based on configuration...

To find the address, from the host system, use the ifconfig command:

$ ifconfig
...
enp0s8    Link encap:Ethernet  HWaddr 09:21:27:2d:5c:4b
          inet addr:192.168.1.100  Bcast:192.168.1.255  Mask:255.255.255.0
          inet6 addr: fe89::a21:27ff:fe2d:5c4b/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:118 errors:0 dropped:0 overruns:0 frame:0
          TX packets:10 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:14803 (14.8 KB)  TX bytes:1332 (1.3 KB)
...

In this case, the IP address is: 192.168.1.100. On the browser of the host system, use that address to access the local server with Jekyll’s port:

http://192.168.1.100:4000

Whole Script

Vagrantfile

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/xenial64"

  config.vm.provision :shell, path: "install_rvm.sh", args: "stable", privileged: false
  config.vm.provision :shell, path: "install_ruby.sh", args: "2.4", privileged: false

  config.vm.network "public_network"

  config.vm.provision "shell", inline: <<-SHELL
    curl -sL https://deb.nodesource.com/setup_6.x | bash -
    apt-get update
    apt-get install -y nodejs

    # update npm with newer version of node
    npm install npm --global
  SHELL
end

install_rvm.sh

#!/usr/bin/env bash

gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
curl -sSL https://get.rvm.io | bash -s $1

install_ruby.sh

#!/usr/bin/env bash

source $HOME/.rvm/scripts/rvm || source /etc/profile.d/rvm.sh

rvm use --default --install $1

shift

if (( $# ))
then gem install $@
fi

rvm cleanup all

Post Install

Some things to do post install:

  • run: gem install bundler
  • run: bundler at the folder with Gemfile listed
  • run: ifconfig to find out the IP address

Conclusion

Whew, what turned out to be a way to avoid fixing the installed version of Ruby on my system turned into a massive learning in Vagrant configuration. This article covered:

  • Using the “public_network” option
  • Using external scripts
  • Using “privileged” option to avoid root from installing software

This definitely took work, maybe just as much work as fixing the Ruby installation, but it has given me a tool to run a Jekyll server on almost any computer.