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:

  1. Run your tests
  2. Break things
  3. Restore to snapshot
  4. 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_folder headaches
  • 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

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! 👇

Similar Posts