Updated October 2026 for provider 2.0.1.
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-specific setup
Vagrant supports several providers, including Hyper-V. Each backend still comes with its own setup and configuration.
📦 Heavy box downloads
The first use of a new box meant a large image download.
🤔 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"
version = "~> 2.0.1"
}
}
}
resource "multipass_instance" "web" {
name = "web-server"
cpus = 2
memory = "2G"
disk = "10G"
image = "jammy"
wait_for_cloud_init = true
cloud_init = <<-EOT
#cloud-config
package_update: true
packages:
- nginx
EOT
}
- Standard HCL (same as production Terraform)
- Cloud-init provisioning (same as cloud VMs)
- Image aliases (Multipass downloads and caches the selected image)
Both install Nginx. The Multipass example uses its default network; matching Vagrant's static private IP needs additional network configuration.
Part 2: Setting Up Multipass + Terraform
Step 1: Install Multipass
You'll need Terraform 1.6+ and Multipass 1.13+ on the same host. The provider calls the local Multipass CLI.
# macOS
brew install multipass
# Ubuntu/Debian (with snapd installed)
sudo snap install multipass
# Windows
# Download from https://multipass.run/install
winget install Canonical.Multipass
Verify it works:
multipass version
# Check that Multipass is 1.13 or newer
Step 2: Create Your First Terraform Config
Create a new directory and main.tf:
terraform {
required_providers {
multipass = {
source = "todoroff/multipass"
version = "~> 2.0.1"
}
}
}
provider "multipass" {
# Optional: explicit path to multipass binary
# multipass_path = "/usr/local/bin/multipass"
# Optional: timeout for commands (default 600s)
command_timeout = 600
}
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. This example provisions PostgreSQL and Nginx, and passes the database address to the web VM. A real application still needs its database connection and authentication configured.
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 "<h1>Web server</h1><p>Database: 192.168.56.10</p>" > /var/www/html/index.html
SHELL
end
end
After: Multipass + Terraform
Use a new directory for this example, with main.tf and web-init.yaml together.
# main.tf
terraform {
required_providers {
multipass = {
source = "todoroff/multipass"
version = "~> 2.0.1"
}
}
}
provider "multipass" {}
# Database Server
resource "multipass_instance" "db" {
name = "db-server"
cpus = 2
memory = "2G"
disk = "10G"
image = "jammy"
wait_for_cloud_init = true
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
timeouts {
create = "20m"
}
}
# 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]
})
wait_for_cloud_init = true
timeouts {
create = "20m"
}
}
# Outputs
output "db_ip" {
value = multipass_instance.db.ipv4[0]
}
output "web_ip" {
value = multipass_instance.web.ipv4[0]
}
#cloud-config
package_update: true
packages:
- nginx
write_files:
- path: /var/www/html/index.html
permissions: '0644'
defer: true
content: |
<h1>Web server</h1>
<p>Database: ${db_host}</p>
runcmd:
- systemctl restart nginx
Save the YAML as web-init.yaml. The DB reference creates Terraform's dependency automatically. wait_for_cloud_init = true also waits for guest provisioning to finish; depends_on alone would not do that. The page displays the DB address to demonstrate templating.
Part 4: Advanced Features You'll Love
Resize an Existing VM
Provider 2.x defaults to resize_policy = "in_place": CPU and memory changes, and disk growth, resize the existing VM. Running VMs stop and restart; already stopped VMs stay stopped. Disk shrink is rejected before mutation, and guest filesystem expansion may require a separate step.
Use resize_policy = "replace" to keep the previous behavior of rebuilding on size changes. Name, image, cloud-init, and network changes still force recreation. Changing the policy alone leaves the VM as it is.
When upgrading from 1.x, change the version constraint and run terraform init -upgrade, then review the plan. ~> 1.5 excludes 2.x. See the 2.0 upgrade notes.
Snapshots (Save Your Progress!)
Multipass has native snapshots, which the provider can manage. Vagrant also supports snapshots with compatible backends.
resource "multipass_instance" "test_env" {
name = "test-env"
image = "jammy"
# ... config
}
Apply the instance configuration first. Then stop the VM before adding the snapshot resource:
multipass stop test-env
# Take a snapshot before risky operations
resource "multipass_snapshot" "before_upgrade" {
instance = multipass_instance.test_env.name
name = "pre-upgrade-backup"
}
Apply again to create the snapshot, then run multipass start test-env. A snapshot requires a stopped instance; the resource does not stop it automatically.
Now you can:
- Run your tests
- Break things
- Restore to snapshot
- Repeat
Terraform tracks the snapshot. To restore it, use the CLI:
multipass stop test-env
multipass restore test-env.pre-upgrade-backup
multipass start test-env
Follow the restore prompt, then review terraform plan because restoration can change the VM relative to your configuration.
File Transfers (No More Provisioners!)
For config files and log downloads, the provider has dedicated transfer resources. Create configs/app.yaml locally before applying this example:
# Upload config files
resource "multipass_file_upload" "app_config" {
instance = multipass_instance.web.name
source = "${path.module}/configs/app.yaml"
destination = "/home/ubuntu/app.yaml"
}
# Download logs for analysis
resource "multipass_file_download" "app_logs" {
instance = multipass_instance.web.name
source = "/var/log/nginx/access.log"
destination = "${path.module}/logs/app.log"
triggers = {
refresh = "1" # Change this value to download a fresh copy.
}
}
Uploads need a writable destination; the transfer does not elevate permissions for paths under /etc. Downloads need readable source files. Transfers are discrete copies, and remote edits do not automatically trigger another transfer. Destroying these resources removes their respective remote or local destination.
For a shared source directory, use a mounts block inside your instance resource:
mounts {
host_path = abspath("${path.module}/src")
instance_path = "/workspace"
}
Create src on the host before applying. This is the closer equivalent to config.vm.synced_folder; mounts can be added or removed without recreating the VM.
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:
multipass db-psql # Opens psql on db server
multipass web-logs # Tails nginx logs
For the shorter db-psql and web-logs commands, add the Multipass alias directory to your host's PATH as described in the alias guide.
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 to create; multipass start <name> to start an existing stopped VM |
vagrant destroy |
terraform destroy |
vagrant ssh |
multipass shell <name> |
vagrant halt |
(use multipass stop <name>) |
vagrant reload |
multipass restart <name> to reboot; terraform apply for configuration changes |
config.vm.box |
image = "jammy" |
vb.memory |
memory = "2G" |
vb.cpus |
cpus = 2 |
config.vm.provision "shell" |
cloud_init = <<-EOT ... EOT at creation; changing it recreates the VM |
config.vm.synced_folder |
mounts { } block |
config.vm.network |
networks { } for host interfaces; configure static IPs inside the guest |
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 rely on a particular Vagrant provider or plugin
- Your team has heavy investment in existing Vagrantfiles
- You need VirtualBox-specific features
✅ Use Multipass + Terraform if:
- You're working primarily with Ubuntu
- 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 a quickstart. The first image download and package installation can take a few minutes.
1. Install Dependencies
# Install Multipass
brew install multipass # or snap install multipass
# Verify
multipass version
2. Create Your First Config
The following file-creation commands use Bash. In PowerShell, save the HCL between EOF markers as main.tf using your editor.
mkdir my-dev-env && cd my-dev-env
cat > main.tf << 'EOF'
terraform {
required_providers {
multipass = {
source = "todoroff/multipass"
version = "~> 2.0.1"
}
}
}
resource "multipass_instance" "dev" {
name = "dev-box"
cpus = 2
memory = "4G"
disk = "20G"
image = "jammy"
wait_for_cloud_init = true
cloud_init = <<-EOT
#cloud-config
package_update: true
packages:
- git
- curl
- build-essential
runcmd:
- echo "Dev environment ready!" > /home/ubuntu/welcome.txt
EOT
timeouts {
create = "20m"
}
}
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! 👇
Top comments (0)