Terraform for Local VMs: A Modern Alternative to Vagrant
After 8 years of using Vagrant, I finally made the switch. Not because Vagrant stopped working – it still does what it’s always done. But because I found something that fits better into my modern infrastructure-as-code workflow.
Here’s the story of how I replaced Vagrant with Multipass + Terraform and never looked back.
The Problem: Vagrant is Showing Its Age
Don’t get me wrong – Vagrant revolutionized local development. The idea of “infrastructure as code” for your laptop was groundbreaking in 2010.
But in 2026, I kept hitting the same pain points:
🔧 Provider lock-in
Tied to VirtualBox (slow) or VMware (expensive). Hyper-V support was always “experimental.”
📦 Heavy box downloads
Every vagrant up on a new project meant downloading multi-GB box files.
🤔 Different workflow
Vagrantfile syntax is Ruby DSL. My production infrastructure is Terraform. Why maintain two mental models?
Sound familiar?
The Discovery: Multipass + Terraform
Then I discovered Canonical Multipass – a lightweight VM manager from Canonical.
Combined with a Terraform provider, I could:
- ✅ Use the same IaC approach locally and in production
- ✅ Native cloud-init support (just like AWS/GCP/Azure)
- ✅ Simpler setup – no VirtualBox Guest Additions hassles
- ✅ One workflow to rule them all
Let me show you how to make the switch.
Part 1: Understanding the Difference
Vagrant’s Approach
# Vagrantfile
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/jammy64"
config.vm.network "private_network", ip: "192.168.56.10"
config.vm.provider "virtualbox" do |vb|
vb.memory = "2048"
vb.cpus = 2
end
config.vm.provision "shell", inline: <<-SHELL
apt-get update
apt-get install -y nginx
SHELL
end
- Ruby DSL
- Provider-specific configurations
- Shell provisioners (or Ansible/Chef/Puppet)
- Box-based images
Multipass + Terraform Approach
# main.tf
terraform {
required_providers {
multipass = {
source = "todoroff/multipass"
}
}
}
resource "multipass_instance" "web" {
name = "web-server"
cpus = 2
memory = "2G"
disk = "10G"
image = "jammy"
cloud_init = <<-EOT
#cloud-config
packages:
- nginx
EOT
}
- Standard HCL (same as production Terraform)
- Cloud-init provisioning (same as cloud VMs)
- Image aliases (no giant downloads)
Same result, different philosophy.
Part 2: Setting Up Multipass + Terraform
Step 1: Install Multipass
# macOS
brew install multipass
# Ubuntu/Debian
sudo snap install multipass
# Windows
# Download from https://multipass.run/install
winget install Canonical.Multipass
Verify it works:
multipass version
# multipass 1.14.0
Step 2: Create Your First Terraform Config
Create a new directory and main.tf:
terraform {
required_providers {
multipass = {
source = "todoroff/multipass"
version = "~> 1.5"
}
}
}
provider "multipass" {
# Optional: explicit path to multipass binary
# multipass_path = "/usr/local/bin/multipass"
# Optional: timeout for commands (default 120s)
command_timeout = 300
}
resource "multipass_instance" "dev" {
name = "my-dev-vm"
cpus = 2
memory = "4G"
disk = "20G"
image = "jammy" # Ubuntu 22.04 LTS
}
output "ip_address" {
value = multipass_instance.dev.ipv4[0]
}
Step 3: Deploy
terraform init
terraform apply
That’s it. Your VM is running. ⚡
# Check it
multipass list
# Name State IPv4 Image
# my-dev-vm Running 192.168.64.5 Ubuntu 22.04 LTS
# SSH into it
multipass shell my-dev-vm
Part 3: Migrating Your Vagrantfile
Let’s convert a real-world Vagrant setup to Multipass + Terraform.
Before: Vagrant Multi-Machine Setup
# Vagrantfile
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/jammy64"
config.vm.define "db" do |db|
db.vm.hostname = "db-server"
db.vm.network "private_network", ip: "192.168.56.10"
db.vm.provider "virtualbox" do |vb|
vb.memory = "2048"
end
db.vm.provision "shell", inline: <<-SHELL
apt-get update
apt-get install -y postgresql postgresql-contrib
sudo -u postgres createuser --superuser vagrant
sudo -u postgres createdb myapp
SHELL
end
config.vm.define "web" do |web|
web.vm.hostname = "web-server"
web.vm.network "private_network", ip: "192.168.56.11"
web.vm.provider "virtualbox" do |vb|
vb.memory = "1024"
end
web.vm.provision "shell", inline: <<-SHELL
apt-get update
apt-get install -y nginx
echo "upstream api { server 192.168.56.10:5432; }" > /etc/nginx/conf.d/upstream.conf
SHELL
end
end
After: Multipass + Terraform
# main.tf
terraform {
required_providers {
multipass = {
source = "todoroff/multipass"
version = "~> 1.5"
}
}
}
provider "multipass" {}
# Database Server
resource "multipass_instance" "db" {
name = "db-server"
cpus = 2
memory = "2G"
disk = "10G"
image = "jammy"
cloud_init = <<-EOT
#cloud-config
package_update: true
packages:
- postgresql
- postgresql-contrib
runcmd:
- sudo -u postgres createuser --superuser ubuntu
- sudo -u postgres createdb myapp
EOT
}
# Web Server (depends on DB for its IP)
resource "multipass_instance" "web" {
name = "web-server"
cpus = 1
memory = "1G"
disk = "5G"
image = "jammy"
cloud_init = templatefile("${path.module}/web-init.yaml", {
db_host = multipass_instance.db.ipv4[0]
})
depends_on = [multipass_instance.db]
}
# Outputs
output "db_ip" {
value = multipass_instance.db.ipv4[0]
}
output "web_ip" {
value = multipass_instance.web.ipv4[0]
}
# web-init.yaml
#cloud-config
package_update: true
packages:
- nginx
write_files:
- path: /etc/nginx/conf.d/upstream.conf
content: |
upstream api {
server ${db_host}:5432;
}
runcmd:
- systemctl restart nginx
Part 4: Advanced Features You’ll Love
Snapshots (Save Your Progress!)
One killer feature the Vagrant workflow always lacked: native snapshots.
resource "multipass_instance" "test_env" {
name = "test-env"
image = "jammy"
# ... config
}
# Take a snapshot before risky operations
resource "multipass_snapshot" "before_upgrade" {
instance = multipass_instance.test_env.name
name = "pre-upgrade-backup"
}
Now you can:
- Run your tests
- Break things
- Restore to snapshot
- Repeat
All managed through Terraform state.
File Transfers (No More Provisioners!)
Remember the pain of synced folders and file provisioners?
# Upload config files
resource "multipass_file_upload" "app_config" {
instance = multipass_instance.web.name
source = "${path.module}/configs/app.yaml"
destination = "/etc/myapp/config.yaml"
}
# Download logs for analysis
resource "multipass_file_download" "app_logs" {
instance = multipass_instance.web.name
source = "/var/log/myapp/app.log"
destination = "${path.module}/logs/app.log"
}
No more:
-
config.vm.synced_folderheadaches - NFS permission issues
- VirtualBox Guest Additions breaking
- rsync timing problems
Aliases (Quick Access)
Create host-level shortcuts to jump into your VMs:
resource "multipass_alias" "db_shell" {
name = "db-psql"
instance = multipass_instance.db.name
command = "sudo -u postgres psql myapp"
}
resource "multipass_alias" "web_logs" {
name = "web-logs"
instance = multipass_instance.web.name
command = "tail -f /var/log/nginx/access.log"
}
Now from your host terminal:
db-psql # Instantly opens psql on db server
web-logs # Tails nginx logs
No more vagrant ssh web -c "..." gymnastics.
Part 5: Real-World Use Cases
Local Kubernetes Clusters
Want to spin up a full multi-node K3s cluster for testing? I wrote a complete step-by-step tutorial:
📚 Build a Local Kubernetes Cluster in Minutes with Terraform and Multipass
The tutorial walks through:
- 1 master + 2 worker nodes
- Automatic cluster joining via cloud-init
- kubectl access from your host machine
- Complete working code you can deploy today
Other Common Use Cases
This stack is also perfect for:
🔧 Infrastructure Testing
- Test Ansible playbooks across multiple nodes
- Validate HAProxy or Nginx load balancer configs
- Simulate distributed systems locally
🎓 Learning Labs
- Practice Terraform without cloud costs
- Learn Linux system administration
- Experiment with databases and replication
💻 Development Environments
- Reproducible team dev environments
- Test upgrades in isolation
- Quick throwaway VMs for experimentation
Part 6: Migration Cheat Sheet
| Vagrant | Multipass + Terraform |
|---|---|
vagrant up |
terraform apply |
vagrant destroy |
terraform destroy |
vagrant ssh |
multipass shell <name> |
vagrant halt |
(use multipass stop <name>) |
vagrant reload |
terraform apply (with changes) |
config.vm.box |
image = "jammy" |
vb.memory |
memory = "2G" |
vb.cpus |
cpus = 2 |
config.vm.provision "shell" |
cloud_init = <<-EOT ... EOT |
config.vm.synced_folder |
multipass_file_upload resource |
config.vm.network |
networks { } block |
Part 7: When to Stick with Vagrant
To be fair, Vagrant still has its place:
✅ Use Vagrant if:
- You need non-Ubuntu operating systems (Windows, CentOS, etc.)
- You’re locked into VMware or Hyper-V for specific reasons
- Your team has heavy investment in existing Vagrantfiles
- You need VirtualBox-specific features
✅ Use Multipass + Terraform if:
- You’re working primarily with Ubuntu/Linux
- You want a simpler, lighter setup
- You want one workflow for local and cloud
- You’re already using Terraform
- You want modern cloud-init provisioning
- Resource efficiency is important
Getting Started Today
Ready to make the switch? Here’s your 5-minute quickstart:
1. Install Dependencies
# Install Multipass
brew install multipass # or snap install multipass
# Verify
multipass version
2. Create Your First Config
mkdir my-dev-env && cd my-dev-env
cat > main.tf << 'EOF'
terraform {
required_providers {
multipass = {
source = "todoroff/multipass"
}
}
}
resource "multipass_instance" "dev" {
name = "dev-box"
cpus = 2
memory = "4G"
disk = "20G"
image = "jammy"
cloud_init = <<-EOT
#cloud-config
packages:
- git
- curl
- build-essential
runcmd:
- echo "Dev environment ready!" > /home/ubuntu/welcome.txt
EOT
}
output "ip" {
value = multipass_instance.dev.ipv4[0]
}
EOF
3. Deploy
terraform init
terraform apply -auto-approve
4. Access Your VM
multipass shell dev-box
cat ~/welcome.txt
# Dev environment ready!
That’s it. You’re running a modern, fast, Terraform-managed local development environment.
Resources
- Terraform Provider: registry.terraform.io/providers/todoroff/multipass
- GitHub Repository: github.com/todoroff/terraform-provider-multipass
- Multipass Official: multipass.run
- Example Configurations: GitHub Examples
Conclusion
Switching from Vagrant to Multipass + Terraform was one of the best decisions I made for my local development workflow.
What I gained:
- 🔧 Simpler setup (no Guest Additions, NFS issues, etc.)
- 🔄 One IaC workflow for everything
- 📸 Native snapshot support
- 🎯 Cloud-native provisioning with cloud-init
What I lost:
- Multi-OS support (Ubuntu-only)
- …that’s about it
If you want a modern, Terraform-native approach to local VMs, give this a try.
Have questions about migrating your Vagrant setup? Drop them in the comments! I’m happy to help with specific migration scenarios.
Found this useful? Consider ⭐ starring the provider on GitHub – it helps others discover it!
What’s your local development setup? Still on Vagrant, Docker Compose, or something else entirely? Let me know in the comments! 👇