Full Vagrant Ubuntu 22 Dev (VirtualBox) setup
A full setup for a Vagrant Ubuntu 22.04 Dev environment, including a script to install software and configure the machine for an ssh user
Updated April, 2024
Introduction
There are times when I just don't want to mess with virtual environments or Conda etc. My main Linux machine, right now, is far from being a beast, so I need to keep it lean. Sure docker's fine, and I've used that plenty, but sometimes it's useful to replicate something like a server you're going to start with when you spin one up in the cloud or with a VM provider, which is where Vagrant comes in. Highly repeatable, enjoyably destructible, and you can use it to test out anything you would like to do with a server, without having to worry about messing up your main machine.
Prerequisites
VirtualBox & Vagrant installation
Pretty horrible looking website but it's a decent product: VirtualBox, follow the link to your OS; For Windows & MacOS it'll download an installer. As I write this (April '24), the Linux instructions still point to version 6.1, but 7.0 is available either via your package manager or if you go to download.virtualbox.org/virtualbox/.
If you're on Linux Mint, remember to point at the jammy
repo, not the lsb_release
one, which is virginia
in my case. these instructions should work on Mint or Ubuntu:
VirtualBox installation
# Get the correct codename
UBUNTU_CODENAME=$(grep 'UBUNTU_CODENAME' /etc/os-release | cut -d= -f2)
# Add the repo
echo "deb [signed-by=/usr/share/keyrings/virtualbox-archive-keyring.gpg] https://download.virtualbox.org/virtualbox/debian $UBUNTU_CODENAME contrib" | sudo tee /etc/apt/sources.list.d/virtualbox.list
# Get the key
wget -q https://www.virtualbox.org/download/oracle_vbox_2016.asc -O- | sudo gpg --dearmor -o /usr/share/keyrings/virtualbox-archive-keyring.gpg
# Update the package list
sudo apt update
# See available versions
apt-cache search virtualbox | grep "Oracle VM VirtualBox"
# Install the latest version
sudo apt install virtualbox
Vagrant installation
# Get the correct codename
UBUNTU_CODENAME=$(grep 'UBUNTU_CODENAME' /etc/os-release | cut -d= -f2)
# Add the repo
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $UBUNTU_CODENAME main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
# Update the package list and install
sudo apt update && sudo apt install vagrant
Guest Additions & Vagrant plugins
I actually found that the plugin that is supposed to install the guest additions made the machine build hang, I think I know why but it seemed easier to install it by other means, so I removed the plugin and will install them as part of the scripted build below. To enable this I downloaded guest additions into the same directory as I'm running Vagrant from. You can download it Guest Additions as follows:
# Fetch the latest VirtualBox version & build the URL
LATEST_VB_VERSION=$(curl -s https://download.virtualbox.org/virtualbox/LATEST.TXT)
GUEST_ADDITIONS_URL="https://download.virtualbox.org/virtualbox/${LATEST_VB_VERSION}/VBoxGuestAdditions_${LATEST_VB_VERSION}.iso"
# Download the Guest Additions ISO
curl -O $GUEST_ADDITIONS_URL -o VBoxGuestAdditions.iso
I'm only using the disksize plugin in this example, which is useful for when you need to increase the size of the disk that comes with the image. You can install it like this:
vagrant plugin install vagrant-disksize
Setting up your environment
To run this as I am, you'll need to have a directory with the following files in it:
- Vagrantfile
- config.yml
- bootstrap-ubuntu.sh
- VBoxGuestAdditions.iso
Here's a quick command to make the files:
touch Vagrantfile config.yml bootstrap-ubuntu.sh
Now that you're all set up, let's take a look at the files you've just created.
Vagrantfile
There's a few things to point out about this file:
- Don't worry too much about getting your networking right at first, if it can't find the network you specify, it'll prompt you to choose one during build, it usually makes sense which your active connection is, then you can replace
"en7: USB 10/100/1000 LAN"
with whatever yours is. - There is a
storageattach
line in thevb.customize
block that presents the Guest Additions ISO to the VM. We then use an inline script to run the guest additions. I think the reason this wasn't working when it tried to automate it was that the Ubuntu image needed these packages installing:bzip2 gcc make perl
- Also in the
storageattach
line, you may find that your--storagectl
is something other thanIDE
, you can find out by going into the VirtualBox GUI and looking at the storage settings for the VM. - The
ASK_CREDENTIALS
variable is used to decide whether to prompt you for a username and password, when we build the machine it will look like this:ASK_CREDENTIALS=true vagrant up
. if you don't want a user creating, just leave theASK_CREDENTIALS=true
part out and runvagrant up
. - If you do create a user it will be added to the sudo group with no password required, you should adjust this is that's not how you like things.
require 'yaml'
CONFIG = YAML.load_file('config.yml')
Vagrant.configure("2") do |config|
config.disksize.size = "50GB"
config.ssh.insert_key = false
config.vm.define "ubuntu-dev" do |dev|
dev.vm.box = "ubuntu/jammy64"
dev.vm.hostname = "ubuntu-dev"
dev.vm.network "public_network", ip: "192.168.76.220", bridge: "en7: USB 10/100/1000 LAN"
dev.vm.provider "virtualbox" do |vb|
vb.memory = 8192
vb.cpus = 4
vb.name = "ubuntu-dev"
vb.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
vb.customize ["modifyvm", :id, "--natdnsproxy1", "on"]
vb.customize ["modifyvm", :id, "--graphicscontroller", "vmsvga"]
vb.customize ["modifyvm", :id, "--vram", "180"]
vb.customize ['storageattach', :id, '--storagectl', 'IDE', '--port', 1, '--device', 0, '--type', 'dvddrive', '--medium', File.join(File.dirname(__FILE__), "VBoxGuestAdditions.iso")]
end
# Provisioning to install Guest Additions
dev.vm.provision "shell", inline: <<-SHELL
if [ -z "$(find /opt -name VBoxGuestAdditions.run -print -quit)" ]; then
sudo apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt install -y bzip2 gcc make perl
sudo mkdir /media/VBoxGuestAdditions
sudo mount -o loop /dev/cdrom /media/VBoxGuestAdditions
sudo /media/VBoxGuestAdditions/VBoxLinuxAdditions.run ` true
sudo umount /media/VBoxGuestAdditions
sudo rmdir /media/VBoxGuestAdditions
else
echo "Guest Additions already installed."
fi
SHELL
if ENV['ASK_CREDENTIALS'] == 'true'
# Prompt the user for a username
puts "Please enter a username for the Ubuntu user:"
user_name = STDIN.gets.chomp
# Prompt the user for a password
puts "Please enter a password for the Ubuntu user:"
user_password = STDIN.noecho(&:gets).chomp
puts "\n"
# Provision to create the user and configure SSH
dev.vm.provision "shell", inline: <<-SHELL
echo "Creating a user with the provided username and password..."
sudo useradd -m -s /bin/bash #{user_name}
echo "#{user_name}:#{user_password}" | sudo chpasswd
# Configure SSH to allow password authentication for the new user
sudo mkdir -p /home/#{user_name}/.ssh
sudo bash -c 'echo "Match User #{user_name}" >> /etc/ssh/sshd_config'
sudo bash -c 'echo " PasswordAuthentication yes" >> /etc/ssh/sshd_config'
# Add user to sudo group with nopasswd
sudo usermod -aG sudo #{user_name}
sudo bash -c 'echo "#{user_name} ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/#{user_name}'
sudo systemctl restart sshd
SHELL
end
# Provision to copy the configuration file
dev.vm.provision "file", source: "./config.yml", destination: "/home/vagrant/provision.conf"
# Provision to run the bootstrap script
dev.vm.provision "shell", path: "bootstrap-ubuntu.sh", run: "always", env: {"CONFIG_FILE" => "/home/vagrant/provision.conf"}
# Adding direct reboot at the end of provisioning
dev.vm.provision "shell", inline: "echo 'Rebooting now...'; sudo reboot", run: "always"
end
end
bootstrap-ubuntu.sh
This script will take values from the config.yml
file you have locally, on the server it will be written to /home/vagrant/provision.conf
and then read in by the script.
#!/bin/bash
# Default configuration file
CONFIG_FILE="provision.conf"
# Function to log messages
log() {
echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')] $@"
}
# Function for installing software on Ubuntu
install_ubuntu() {
package_name=$1
case $package_name in
set_vim_editor)
log "Setting default editor to Vim on Ubuntu..."
sudo update-alternatives --set editor /usr/bin/vim.basic
;;
pip)
log "Installing pip on Ubuntu..."
sudo DEBIAN_FRONTEND=noninteractive add-apt-repository -y universe
sudo apt update
sudo DEBIAN_FRONTEND=noninteractive apt install -y python3-pip
;;
git)
log "Installing Git on Ubuntu..."
sudo DEBIAN_FRONTEND=noninteractive apt install -y git
;;
nodejs)
log "Installing Node.js $nodejs_version on Ubuntu..."
curl -sL https://deb.nodesource.com/setup_$nodejs_version.x | sudo DEBIAN_FRONTEND=noninteractive -E bash -
sudo DEBIAN_FRONTEND=noninteractive apt install -y nodejs
;;
mysql)
log "Installing MySQL on Ubuntu..."
sudo DEBIAN_FRONTEND=noninteractive apt install -y mysql-server
;;
docker)
log "Installing Docker on Ubuntu..."
sudo DEBIAN_FRONTEND=noninteractive apt install apt-transport-https ca-certificates curl software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
apt-cache policy docker-ce
sudo DEBIAN_FRONTEND=noninteractive apt install -y docker-ce
;;
docker-compose)
log "Installing Docker Compose on Ubuntu..."
sudo DEBIAN_FRONTEND=noninteractive apt install -y docker-compose
;;
ubuntu-desktop)
log "Installing graphical environment on Ubuntu..."
sudo snap install firefox
sudo DEBIAN_FRONTEND=noninteractive apt install -y ubuntu-desktop
;;
budgie-desktop)
log "Installing Budgie Desktop on Ubuntu..."
sudo add-apt-repository -y ppa:ubuntubudgie/backports-budgie
sudo apt update
sudo DEBIAN_FRONTEND=noninteractive apt install -y budgie-desktop-environment
;;
jetbrains-toolbox)
log "Installing JetBrains Toolbox on Ubuntu..."
sudo DEBIAN_FRONTEND=noninteractive apt install -y wget
wget -O jetbrains-toolbox.tar.gz "https://download.jetbrains.com/toolbox/jetbrains-toolbox-1.21.9712.tar.gz"
tar -xvf jetbrains-toolbox.tar.gz
sudo mv jetbrains-toolbox-1.21.9712/jetbrains-toolbox /usr/local/bin
;;
*)
log "Unsupported package: $package_name"
;;
esac
}
# Parse arguments
for i in "$@"
do
case $i in
--config=*)
CONFIG_FILE="${i#*=}"
shift
;;
*)
# unknown option
;;
esac
done
# Source the configuration file
if [ -f "$CONFIG_FILE" ]; then
source "$CONFIG_FILE"
log "Using configuration file: $CONFIG_FILE"
else
log "Configuration file not found: $CONFIG_FILE"
exit 1
fi
# Detect the distribution
if [ -f /etc/os-release ]; then
source /etc/os-release
if [[ "$ID" == "ubuntu" ]]; then
# Run a full update and upgrade
sudo apt update
sudo DEBIAN_FRONTEND=noninteractive apt full-upgrade -y
# Unattended user creation
sudo DEBIAN_FRONTEND=noninteractive apt install -y whois
# Alias python to python3
sudo DEBIAN_FRONTEND=noninteractive apt install -y python-is-python3
# Conditional installations based on configuration
[[ "$set_vim_editor" == "true" ]] && install_ubuntu "set_vim_editor"
[[ "$install_pip" == "true" ]] && install_ubuntu "pip"
[[ "$install_git" == "true" ]] && install_ubuntu "git"
[[ "$install_nodejs" == "true" ]] && install_ubuntu "nodejs"
[[ "$install_mysql" == "true" ]] && install_ubuntu "mysql"
[[ "$install_docker" == "true" ]] && install_ubuntu "docker"
[[ "$install_docker_compose" == "true" ]] && install_ubuntu "docker-compose"
[[ "$install_ubuntu_desktop" == "true" ]] && install_ubuntu "ubuntu-desktop"
[[ "$install_budgie_desktop" == "true" ]] && install_ubuntu "budgie-desktop"
# Only install JetBrains Toolbox if if either Ubuntu Desktop or Budgie Desktop is installed
[[ "$install_jetbrains_toolbox" == "true" && ("$install_ununtu_desktop" == "true" ` "$install_budgie_desktop" == "true") ]] && install_ubuntu "jetbrains-toolbox"
else
log "Unsupported distribution: $ID. Exiting."
exit 1
fi
else
log "Unable to detect distribution. Exiting."
exit 1
fi
log "Installation complete."
As you can see, there are a few options and it would be easy to add more by adding a block for the installation commands, a line in the conditional block, and a line in the config.yml
file.
With the ubuntu-desktop
installation, I found that it was hanging on a snap installation of Firefox, so I added that as a separate command prior to the desktop installation.
config.yml
This file is where you configure the software you want to install. Here's an example:
set_vim_editor=true
install_pip=true
install_git=true
install_nodejs=false
nodejs_version=20
install_mysql=false
install_docker=false
install_docker_compose=false
install_ubuntu_desktop=false
install_budgie_desktop=true
install_jetbrains_toolbox=false
Running the build
So now you should have a directory that looks something like this:
$ tree
.
├── bootstrap-ubuntu.sh
├── config.yml
├── Vagrantfile
└── VBoxGuestAdditions_7.0.14.iso
When you've made any tweaks you need to make, you can run the build like this:
# To install a user you can ssh into, run it like this and you will be prompted for a username and password
ASK_CREDENTIALS=true vagrant up
# To just build the machine (you can still use the vagrant ssh command)
vagrant up
Conclusion
This setup should give you a good starting point for a development server with a working method for an additional user to be able to connect remotely. It should pick up an IP from your home router with a bridge configuration, so if you want to do something that's going to be exposed to your home network or the internet, then you can do that, and it has an option to install extra software and a desktop environment if you want to use it like that.