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 locking —
package.jsonlocks Node packages, but who locks the OS version, OpenSSL version, orglibc? - 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 upon 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 destroywhen you’re done,vagrant upagain 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.

