DEV Community

Arunabh Gupta
Arunabh Gupta

Posted on

How I Stopped Manually Setting Up Virtual Machines

I could spin up a virtual machine using Oracle VM VirtualBox.

Install the OS. Set up packages. Configure everything just the way I wanted.

But then recreating them again and again and again... you get my point. It's tiring.

Creating virtual environments is easy, but doing the same thing again and again is just stupidity.

What are VMs?

Let’s first clear what the hell even are vms and why do we use it.

VM stands for "virtual machines." In simple terms these are “computer within a computer”. It’s an emulation of a physical computer system that runs an operating system and applications like an actual physical machine would.

Why do we use VMs ?

The primary goal of using a VM is isolation. By running a VM, you create a dedicated environment that is completely separate from your host machines’s OS. This is useful because:

  • We can run dangerous scripts and test them without risking our host OS.
  • We can run a Linux environment on macOS/Windows OS to match the production environment.
  • We can literally run any version of an OS (even the very old ones) to support specific software that won’t run on modern systems.

Oracle VirtualBox

To actually run a VM, you need a "hypervisor." In simple words, a hypervisor is a manager that sits between the computer’s hardware and virtual machines.

Oracle VirtualBox is also one such hypervisor. It’s an industry standard for local virtualization since it’s free and open source and can be run on any host operating system.

How it works (The Manual Way):

  1. The ISO Hunt: You have to go find a Linux .iso file (like Ubuntu or Debian) and download it.
  2. The Wizard: You open the VirtualBox GUI and click through a "New VM" wizard, assigning RAM, CPU cores, and virtual hard drive space.
  3. The Installation: You "insert" the virtual disk and walk through the entire OS installation process manually. Setting up the username, timezone, and partitions... every... single... time.

Click fatigue problem…

If you accidentally delete or misconfigure your VM, you have to delete it and do the entire process again, which is just too much pain.

Vagrant!!!

Here comes our savior. Vagrant is an open-source command-line tool by HashiCorp designed for creating, configuring, and managing portable VMs.

In simple words, Vagrant helps us automate the entire process of setting up a VM through code. Instead of clicking through a GUI to create a VM, you write a script called Vagrantfile. In DevOps terms, this is our first real encounter with Infrastructure as Code (IaC).

Vagrant is a huge improvement from the gui setup process due to the following reasons:

  • You no longer have to use the virtualbox window. Vagrant runs VMs in the background, and you can interact with them solely through the terminal.
  • If the configuration is messed up, just fix the script and restart or first destroy the old VM and then start the VM again using Vagrant CLI commands.

Instead of hunting for ISO files like a digital scavenger, Vagrant uses 'boxes'—pre-packaged images you can pull from Vagrant Cloud. Want Ubuntu 22.04? It’s one line of code away.

The command vagrant init hashicorp/bionic64 creates a base vagrant file with ubuntu/jammy64 box. Ex:

# -*- mode: ruby -*-
# vi: set ft=ruby :


# All Vagrant configuration is done below. The "2" in Vagrant.configure
# configures the configuration version (we support older styles for
# backwards compatibility). Please don't change it unless you know what
# you're doing.
Vagrant.configure("2") do |config|
 # The most common configuration options are documented and commented below.
 # For a complete reference, please see the online documentation at
 # https://docs.vagrantup.com.


 # Every Vagrant development environment requires a box. You can search for
 # boxes at https://vagrantcloud.com/search.
 config.vm.box = "ubuntu/jammy64"


 # Disable automatic box update checking. If you disable this, then
 # boxes will only be checked for updates when the user runs
 # `vagrant box outdated`. This is not recommended.
 # config.vm.box_check_update = false


 # Create a forwarded port mapping which allows access to a specific port
 # within the machine from a port on the host machine. In the example below,
 # accessing "localhost:8080" will access port 80 on the guest machine.
 # NOTE: This will enable public access to the opened port
 # config.vm.network "forwarded_port", guest: 80, host: 8080


 # Create a forwarded port mapping which allows access to a specific port
 # within the machine from a port on the host machine and only allow access
 # via 127.0.0.1 to disable public access
 # config.vm.network "forwarded_port", guest: 80, host: 8080, host_ip: "127.0.0.1"


 # Create a private network, which allows host-only access to the machine
 # using a specific IP.
 # config.vm.network "private_network", ip: "192.168.33.10"


 # Create a public network, which generally matched to bridged network.
 # Bridged networks make the machine appear as another physical device on
 # your network.
 # config.vm.network "public_network"


 # Share an additional folder to the guest VM. The first argument is
 # the path on the host to the actual folder. The second argument is
 # the path on the guest to mount the folder. And the optional third
 # argument is a set of non-required options.
 # config.vm.synced_folder "../data", "/vagrant_data"


 # Disable the default share of the current code directory. Doing this
 # provides improved isolation between the vagrant box and your host
 # by making sure your Vagrantfile isn't accessible to the vagrant box.
 # If you use this you may want to enable additional shared subfolders as
 # shown above.
 # config.vm.synced_folder ".", "/vagrant", disabled: true


 # Provider-specific configuration so you can fine-tune various
 # backing providers for Vagrant. These expose provider-specific options.
 # Example for VirtualBox:
 #
 # config.vm.provider "virtualbox" do |vb|
 #   # Display the VirtualBox GUI when booting the machine
 #   vb.gui = true
 #
 #   # Customize the amount of memory on the VM:
 #   vb.memory = "1024"
 # end
 #
 # View the documentation for the provider you are using for more
 # information on available options.


 # Enable provisioning with a shell script. Additional provisioners such as
 # Ansible, Chef, Docker, Puppet and Salt are also available. Please see the
 # documentation for more information about their specific syntax and use.
 # config.vm.provision "shell", inline: <<-SHELL
 #   apt-get update
 #   apt-get install -y apache2
 # SHELL
end

Enter fullscreen mode Exit fullscreen mode

A little bit of networking mumbo jumbo

In the following Vagrantfile (created from the command in the previous section), you can see some terms like "forwarded port," "private network," etc. Let’s discuss them 1 by 1.

  • Forwarded Port: Maps a port on your laptop to a port in the VM (e.g., see a website running on the VM via localhost:8080).
  • Private Network: Gives the VM a specific internal IP address (like 192.168.33.10) that only your laptop can see. This prevents the other person from accessing your VM from their laptop.
  • Public Network: Bridges the VM to your Wi-Fi, making it look like a real, separate computer on your home network. Your router sees the VM as a completely new physical device (like a new phone or a second laptop). It assigns the VM its own unique IP address from your home network's pool.

One 'complex' bit I found interesting is the host_ip parameter. By default, forwarding a port opens it up to your entire local network. If you're working on a public Wi-Fi, that's a security nightmare. By specifying host_ip: "127.0.0.1", you're essentially telling the VM, 'Only talk to me, and nobody else.' It’s a simple line of code that moves your setup from 'it works' to 'it’s secure.

Provisioning?

I manual setup of a VM, you often endup with a “Snowflake Server” - a machine that has unique, hand crafted configuration that no one can replicate because the steps weren’t documented.

Provisioning solves this by treating your server setup as a script. In Vagrant, you have three main ways to handle this ( btw I have only explored the first one in detail ):

1. Shell Provisioning (The Scripted Approach)

This is the most straightforward method. You write a standard Bash script that runs the moment the VM boots up.

  • The Process: Vagrant uploads the script to the guest machine and executes it with sudo privileges.
  • Use Case: Perfect for installing basic dependencies like git, curl, or setting up your Java or Go runtime environments.

Ex: In the vagrant file provided above, there is a section at the bottom of the file setting up basic provisioning. A better provisioning script would be something like below:

config.vm.provision "shell", inline: \<\<-SHELL  
  apt-get update  
  apt-get install \-y nginx  
  systemctl enable nginx  
  systemctl start nginx  
SHELL
Enter fullscreen mode Exit fullscreen mode

This sets up nginx in an ubuntu VM.

2. File Provisioning (The Configuration Transfer)

Sometimes you don't need to run a command; you just need to move a file.

  • The Mechanism: It uses SCP/SFTP to move files from your host machine into the VM.
  • Use Case: Moving your custom nginx.conf or a .env file into the VM before the application starts.

3. Configuration Management (The Scalable Way)

Vagrant also supports "Heavyweight" provisioners like Ansible, Chef, or Puppet.

  • The Theory: Instead of just running a list of commands, these tools ensure the machine is in a specific "State." If a package is already installed, they skip it. This is known as Idempotency.

The beauty of provisioning is that it happens at the initial boot. If I need to reset my environment, I run vagrant destroy and vagrant up --provision. This ensures that my development environment is disposable and reproducible. I am no longer afraid to break things because I know I can recreate my entire infrastructure in seconds.

MultiVM Setup: Real Advantage of Vagrant !!!

MultiVM setup is where we define an entire infrastructure-like a load balancer, two web servers, a database - everything inside a singel Vagrant file.

Instead of having one block of configuration, you use config.vm.define to create separate "identities" within your Vagrantfile. This allows you to manage the entire lifecycle of a distributed system with a single command.

When you define multiple machines, Vagrant treats the Vagrantfile as a loop. Each machine can have its own OS, its own IP address, and its own provisioning scripts.

Here is an example script

Vagrant.configure("2") do |config|
 config.hostmanager.enabled = true
 config.hostmanager.manage_host = true
 ### DB vm  ####
 config.vm.define "db01" do |db01|
   db01.vm.box = "generic/centos9s"
   db01.vm.hostname = "db01"
   db01.vm.network "private_network", ip: "192.168.56.15"
   db01.vm.provider "virtualbox" do |vb|
    vb.memory = "600"
  end


 end
 ### Memcache vm  ####
 config.vm.define "mc01" do |mc01|
   mc01.vm.box = "generic/centos9s"
   mc01.vm.hostname = "mc01"
   mc01.vm.network "private_network", ip: "192.168.56.14"
   mc01.vm.provider "virtualbox" do |vb|
    vb.memory = "600"
  end
 end
 ### RabbitMQ vm  ####
 config.vm.define "rmq01" do |rmq01|
   rmq01.vm.box = "generic/centos9s"
   rmq01.vm.hostname = "rmq01"
   rmq01.vm.network "private_network", ip: "192.168.56.13"
   rmq01.vm.provider "virtualbox" do |vb|
    vb.memory = "600"
  end
 end
 ### tomcat vm ###
  config.vm.define "app01" do |app01|
   app01.vm.box = "generic/centos9s"
   app01.vm.hostname = "app01"
   app01.vm.network "private_network", ip: "192.168.56.12"
   app01.vm.provider "virtualbox" do |vb|
    vb.memory = "800"
  end
  end

 ### Nginx VM ###
 config.vm.define "web01" do |web01|
   web01.vm.box = "ubuntu/jammy64"
   web01.vm.hostname = "web01"
 web01.vm.network "private_network", ip: "192.168.56.11"
 web01.vm.provider "virtualbox" do |vb|
   #  vb.gui = true
    vb.memory = "800"
  end
end
 end
Enter fullscreen mode Exit fullscreen mode

In this example we can use the following commands to manage our VMs using command line.

When you have multiple machines, your CLI commands change slightly:

  • vagrant up web01: Starts only the nginx server.
  • vagrant up: Starts the entire cluster.
  • vagrant ssh web01: Connects you specifically to the nginx server.

Conclusion: Why Vagrant Still Matters in a Containerized World

With the rise of Docker and Kubernetes, many assume that Virtual Machines are a relic of the past. However, Vagrant occupies a unique and necessary space in a developer's toolkit.

While containers are great for isolating applications, Vagrant is designed to isolate the entire environment. When you need to test kernel modules, experiment with different network topologies, or simulate a full-blown multi-node Linux cluster on your local machine, Vagrant is the industry standard for a reason.

The "Infrastructure as Code" Shift

The real power of Vagrant isn't just about avoiding the VirtualBox GUI; it’s about immutability. By defining your infrastructure in a Vagrantfile, you’ve moved away from "manual setup" and into Engineering.

  • Your environment is now Version Controlled: You can commit your Vagrantfile to Git.
  • Your environment is Sharable: A teammate can run vagrant up and have the exact same setup in minutes.
  • Your environment is Disposable: If a configuration goes sideways, you don't debug for hours; you destroy and up.

Top comments (0)