Recently I made some experiments to setup a Rails 6 dev environment using Vagrant.
I started from rails/rails-dev-box and applied some changes.

VM Setup

These are the features of the Vagrant file that I’m using:

  • Ubuntu 18.04 LTS box;
  • ability to install different ruby versions;
  • project files (placed in ./project) kept in sync using rsync.

Vagrantfile (which I keep in ./_vagrant directory):

Vagrant.configure('2') do |config|
  config.vm.box      = 'ubuntu/bionic64' # 18.04 LTS
  config.vm.hostname = 'my-project'
  config.vm.network :forwarded_port, guest: 3000, host: 3000
  config.vm.provider 'virtualbox' do |vb|
    # vb.check_guest_additions = false
    vb.gui    = true
    vb.memory = ENV.fetch('RAILS_DEV_BOX_RAM', 4096).to_i
    vb.name   = 'my-project'
    vb.cpus   = ENV.fetch('RAILS_DEV_BOX_CPUS', 2).to_i
  end
  config.vm.provision 'shell' do |sh|
    sh.env        = { 'RUBY_VERSION' => '2.7.2', 'SWAP_SIZE' => '2G' }
    sh.keep_color = true
    sh.path       = 'bootstrap.sh'
    sh.privileged = false
  end
  config.vm.synced_folder './project', '/project', type: 'rsync', rsync__exclude: ['.git/']
end

Dev Environment Setup:

Main features:

  • RVM setup + Ruby install;
  • chromedriver to run feature specs;
  • install NodeJS (14.x) using nodesource.com;

bootstrap.sh:

#!/usr/bin/env bash

# Suppress command output and exit on error
function check_result {
  echo "--- $1 ---"
  shift
  $@ > /dev/null
  result=$?
  test $result -eq 0 || \
    { echo "!!! error: $result"; exit $result; }
}

# Suppress installation steps output
function apt_install {
  echo "--- Installing $1 ---"
  shift
  sudo apt -y install "$@" >/dev/null 2>&1
}

echo '~~~ Bootstrap script ~~~'

echo '--- Adding swap file ---'
sudo fallocate -l $SWAP_SIZE /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap defaults 0 0' | sudo tee -a /etc/fstab

# Prevents "Warning: apt-key output should not be parsed (stdout is not a terminal)".
export APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=1
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo -E apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list

check_result 'Updating package information' sudo apt-get -y update

apt_install 'development tools' build-essential autoconf libtool

echo '--- Setup RVM ---'
echo silent >> ~/.curlrc
check_result 'Add RVM key' gpg --keyserver hkp://pool.sks-keyservers.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
\curl -sSL https://get.rvm.io | check_result 'Installing RVM' bash -s stable

source $HOME/.rvm/scripts/rvm
check_result "Installing Ruby $RUBY_VERSION" rvm install $RUBY_VERSION

check_result 'Update Ruby gems' gem update --system -N
check_result 'Installing Bundler' gem install bundler -N

apt_install Git git
apt_install SQLite sqlite3 libsqlite3-dev
apt_install Redis redis-server
apt_install 'Nokogiri dependencies' libxml2 libxml2-dev libxslt1-dev
apt_install 'chromedriver' chromium-driver

# PostgreSQL setup
apt_install PostgreSQL postgresql postgresql-contrib libpq-dev
sudo -u postgres createuser --superuser vagrant
cat > /tmp/pg.conf <<- EOF
local   all             postgres                                trust
local   all             all                                     trust
local   replication     all                                     trust
host    all             all             127.0.0.1/32            trust
host    all             all             ::1/128                 trust
host    replication     all             127.0.0.1/32            trust
host    replication     all             ::1/128                 trust
EOF
sudo cp /tmp/pg.conf /etc/postgresql/10/main/pg_hba.conf
sudo systemctl restart postgresql

# NodeJS setup
curl -sL https://deb.nodesource.com/setup_14.x | check_result 'Update NodeJS sources' sudo bash -
apt_install 'NodeJS' nodejs yarn

# Setup locales
sudo update-locale LANG=en_US.UTF-8 LANGUAGE=en_US.UTF-8 LC_ALL=en_US.UTF-8

# Shell utils
cat >> $HOME/.bashrc <<- EOF
alias be="bundle exec"
export DB_HOST="localhost"
export REDIS_URL="redis://localhost:6379/0"
export CHROMEDRIVER_PATH="`which chromedriver`"
cd /project
EOF

echo '--- Project setup ---'
source $HOME/.bashrc
cd /project
check_result 'bundle install' bundle install
check_result 'yarn install' yarn install --check-files
check_result 'rails db:reset' bin/rails db:reset

echo '~~~ Ready! ~~~'

Customizations

To setup MySQL in place of PostgreSQL:

debconf-set-selections <<< 'mysql-server mysql-server/root_password password root'
debconf-set-selections <<< 'mysql-server mysql-server/root_password_again password root'
install MySQL mysql-server libmysqlclient-dev libssl-dev
# Set the password in an environment variable to avoid the warning issued if set with `-p`.
MYSQL_PWD=root mysql -uroot <<SQL
CREATE USER 'rails'@'localhost';
SQL

To install ASDF in place of RVM:

git clone https://github.com/asdf-vm/asdf.git $HOME/.asdf --branch v0.8.0
. $HOME/.asdf/asdf.sh
asdf plugin-add ruby https://github.com/asdf-vm/asdf-ruby.git
asdf install ruby 2.7.2

Project Setup

The database configuration is pretty standard - project/config/database.yml:

default: &default
  adapter: postgresql
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  timeout: 5000

development:
  <<: *default
  database: db_development

test:
  <<: *default
  database: db_test

Selenium configuration for RSpec (for feature and system specs) - project/spec/support/selenium.rb:

Capybara.register_driver :selenium_chrome_headless do |app|
  Selenium::WebDriver::Chrome::Service.driver_path = ENV['CHROMEDRIVER_PATH'] if ENV['CHROMEDRIVER_PATH']
  capabilities = ::Selenium::WebDriver::Remote::Capabilities.chrome(
    'goog:chromeOptions' => {
      'args': %w[disable-dev-shm-usage disable-gpu headless no-sandbox window-size=1600,1000]
    }
  )
  options = {
    browser: :chrome,
    desired_capabilities: capabilities,
  }
  Capybara::Selenium::Driver.new(app, options)
end

RSpec.configure do |config|
  config.before(:each, type: :feature, js: true) do |_spec|
    Capybara.current_driver = :selenium_chrome_headless
    Capybara.javascript_driver = :selenium_chrome_headless
  end

  config.before(:each, type: :system) do
    driven_by(:selenium_chrome_headless)
  end
end

Usage

  • Start the machine (and create the machine the first time): vagrant up
  • Enter in the machine: vagrant ssh
  • Start the server (then point the browser to localhost:3000): bin/rails s -b 0.0.0.0
  • Watch for changes (in another shell): vagrant rsync-auto
  • Stop the machine: vagrant halt
  • Destroy the machine (and delete the VM): vagrant destroy

Conclusion

Preparing a good dev enviroment takes some time to tune the right options but when it’s ready it is pretty comfortable also when you need to share the machine configuration with other developers.

If you are curious, take a look also to this post about setting up a Docker Rails 6 dev environment.

Feel free to leave me a comment to improve this post.