Using Vagrant to Create a Dev Environment: Put an End to ‘Works on My Machine’

Virtualization tutorial - IT technology blog
Virtualization tutorial - IT technology blog

The “works on my machine” trap — a real story

How many times have you heard “it works fine on my machine” right after a staging deploy blows up? I’ve lost count. One developer runs Ubuntu 20.04, another is on macOS Ventura, the team lead uses Windows 11 — naturally everyone has a different Node.js version, different MySQL configs, and don’t even get me started on Python paths.

The cost is painfully obvious: a new team member spends 2–3 days just setting up their environment before writing a single line of code. A bug appears, and nobody can tell whether it’s the code or the environment. Hours of debugging only to discover a one-version difference in a package. Developers aren’t lazy — this is a structural problem nobody proactively solves.

Why dev environments keep drifting

Break it down and there are a few core causes:

  • Everyone uses a different OS — macOS, Windows, Ubuntu — the same command can behave completely differently, especially around file permissions or line endings
  • No system-level version lockingpackage.json locks Node packages, but who locks the OS version, OpenSSL version, or glibc?
  • Manual setup from a README — the README is outdated, missing steps, or the author forgot to document a dependency they installed six months ago
  • Accumulated drift — every developer updates their machine on their own schedule; environments gradually diverge, and nobody notices until a mysterious bug appears

I run a homelab with Proxmox VE managing 12 VMs and containers — it’s my playground for testing everything before it hits production. Managing that many VMs is exactly what made me realize: creating VMs manually doesn’t scale. The entire process has to be automated.

Solutions: from manual to fully automated

Option 1: Manual VM + snapshot

Create one “golden” VM, snapshot it, and clone it for each developer. Sounds reasonable, but in practice those snapshot files are multiple gigabytes — you can’t commit them to git. When you need to add a new dependency, you have to tell everyone to update manually. More importantly, nobody knows what “golden” actually means — what was installed, in what order — so it’s not reproducible.

Option 2: Docker

Docker is excellent at packaging apps — that’s exactly what it was built for. But for dev environments that need kernel-level access, systemd, or multi-server topology simulation, containers fall short. On top of that, on Windows and macOS, Docker Desktop runs inside a hidden VM; volume mount performance with projects containing many small files (like Laravel or WordPress) is genuinely painful.

Option 3: Vagrant — define your VM as code

HashiCorp’s Vagrant (same company behind Terraform and Vault) lets you define a VM using a plain text file called a Vagrantfile. Everything — the OS, RAM, CPU, networking, pre-installed software — lives in that file and gets committed to git alongside your project code.

Why Vagrantfile works for this problem

Four key strengths, each addressing one of the issues above:

  • Reproducible: Run vagrant up on any machine and get exactly the same environment every time
  • Version controlled: It’s a plain text file — commit it to git, track every change in history
  • Shareable: A new developer clones the repo, runs vagrant up, and they’re done — no need to wade through a 20-page README
  • Disposable: vagrant destroy when you’re done, vagrant up again from scratch any time — a perfectly clean environment every time

Installing Vagrant from scratch

Vagrant needs a hypervisor — VirtualBox (free) is the default. Install them in order:

Step 1: Install VirtualBox

# Ubuntu/Debian
sudo apt install virtualbox

# macOS (dùng Homebrew)
brew install --cask virtualbox

Step 2: Install Vagrant

# Ubuntu/Debian — thêm repo chính thức của HashiCorp
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 $(lsb_release -cs) main" | \
  sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install vagrant

# macOS
brew install --cask vagrant

# Kiểm tra cài đặt thành công
vagrant --version

A real-world Vagrantfile for a LEMP web stack

Here’s the Vagrantfile I regularly use for a LEMP stack (Linux + Nginx + MySQL + PHP):

Vagrant.configure("2") do |config|
  # Base box: Ubuntu 22.04 LTS
  config.vm.box = "ubuntu/jammy64"
  config.vm.hostname = "dev-lemp"

  # Port forwarding: truy cập web qua localhost:8080
  config.vm.network "forwarded_port", guest: 80, host: 8080
  config.vm.network "forwarded_port", guest: 3306, host: 33060

  # Private network để SSH và kết nối nội bộ
  config.vm.network "private_network", ip: "192.168.56.10"

  # Sync thư mục code từ máy host vào VM
  config.vm.synced_folder "./src", "/var/www/html"

  # Cấu hình tài nguyên VM
  config.vm.provider "virtualbox" do |vb|
    vb.name   = "dev-lemp"
    vb.memory = "2048"
    vb.cpus   = 2
  end

  # Provisioning: tự động cài phần mềm khi vagrant up lần đầu
  config.vm.provision "shell", inline: <<-SHELL
    apt-get update -y
    apt-get install -y nginx mysql-server php8.1-fpm php8.1-mysql
    systemctl enable nginx
    systemctl start nginx
    echo "Môi trường dev LEMP sẵn sàng!"
  SHELL
end

With this file, a new developer clones the repo and runs exactly two commands:

# Lần đầu: tạo VM và cài đặt tất cả (mất khoảng 5-10 phút)
vagrant up

# SSH vào VM
vagrant ssh

# Tắt VM khi không dùng (giữ nguyên state)
vagrant halt

# Bật lại VM (nhanh hơn, không provision lại)
vagrant up

# Reload và re-provision khi sửa Vagrantfile
vagrant reload --provision

# Xóa hoàn toàn VM
vagrant destroy

Connecting your IDE to the VM via SSH

Both VSCode Remote SSH and JetBrains Gateway support direct connections to a Vagrant VM. Get the SSH config with:

# In ra thông tin SSH config để dùng với IDE
vagrant ssh-config

# Hoặc thêm vào ~/.ssh/config luôn
vagrant ssh-config >> ~/.ssh/config

Then in VSCode, just go to Remote-SSH → Connect to Host → select dev-lemp. You write code on the host machine while it runs directly inside the VM — latency is practically zero since it’s all on the same physical machine.

Vagrant vs. Docker — which should you use?

This question comes up all the time. After years of using both, here’s my condensed take:

  • Use Vagrant when you need to simulate a real server — a full OS, systemd, or a multi-machine topology (e.g., 1 web server + 1 DB server + 1 load balancer)
  • Use Docker when you need a fast, lightweight dev environment and your app is already containerized
  • Use both: Vagrant creates the VM, Docker runs inside it. My current homelab setup: Proxmox creates the VM → VM runs Vagrant → Vagrant provisions Docker. Three layers, but each has a clear, distinct role

Is your team suffering from inconsistent environments? Adding a Vagrantfile to the repo is the fastest fix. Spend 30 minutes writing the file, save dozens of hours of debugging down the road — and more importantly, never again sit through a “whose machine caused this bug” argument before a sprint review.

Share: