If you have ever tried to build or test an infrastructure management tool — something like Kamal or Coolify — you have felt the pain. The pain of testing a deployment, realizing something broke, and then spending the next 20 minutes manually cleaning up Docker containers, orphaned files, and half-finished configurations just to try again.
I ran into this wall repeatedly while working on my own deployment system. I needed a fresh server, fast. I needed to break it, learn from it, and reset it. Over and over again.
Eventually, I stopped fighting and built a solution. A self-contained VM manager that handles the entire lifecycle: create, snapshot, test, restore, destroy. All in one script.

The Problem: Testing Infrastructure is Messy
When you’re building deployment tooling, your test environment is the server itself. You’re not just running unit tests against functions. You’re commissioning real machines, SSHing in, installing Docker, setting up reverse proxies, and deploying applications.
And every time something goes wrong — a misconfigured Traefik rule, a failed container startup, leftover database files — your “test server” becomes polluted. You can’t trust it anymore.
The traditional solutions all have problems:
- Throw away cloud VMs: Works, but it’s expensive and slow. Waiting 2 minutes for DigitalOcean to spin up a new droplet every time you want a clean slate adds up fast.
- Docker-in-Docker: Great for CI, but awkward for testing tools that need to manage real Docker installations.
- Manual cleanup scripts: Fragile. You always miss something. Six months later, that cleanup script becomes its own maintenance nightmare.
What I needed was something closer to the edit → test → undo workflow we take for granted in our IDEs.
The Solution: Local VMs with Snapshot + Restore
Here’s the insight: local VMs are free, fast, and — critically — they support snapshots.
You can spin up a VM, configure it exactly the way you want, save that state as a snapshot, and then restore to that pristine state any time things get messy.

I built a script around Multipass , Canonical’s lightweight VM manager. It’s free, cross-platform, and creates Ubuntu VMs that behave just like real cloud servers.
The workflow becomes beautifully simple:

- Create VMs — Spin up fresh Ubuntu machines with a single command.
- Snapshot — Save the pristine, freshly-commissioned state.
- Test — Run your deployment tool. Break things. Learn.
- Restore — One command and you’re back to clean.
- Repeat — Iterate as many times as you need.
No cloud bills. No waiting. No manual cleanup.
How It Works

The script creates Ubuntu VMs that mimic the behavior of real cloud providers like EC2 or DigitalOcean. You get an ubuntu user with passwordless sudo access, exactly what your deployment tool expects.
It also handles the plumbing that’s easy to forget:
- Generates a dedicated SSH keypair — Stored locally in a
.multipass-statedirectory, not mixed with your personal~/.ssh/keys. - Injects the public key into the VMs — So you can SSH in immediately, or hand the private key to your deployment tool.
- Tracks all created VMs — So cleanup is surgical and complete.
Creating VMs
./multipass-manager.sh create
This spins up three VMs (configurable), generates an SSH keypair if one doesn’t exist, and outputs the private key right there in your terminal for easy copy-pasting.
You’ll see:
✓ Created deploy1
✓ SSH key injected for ubuntu@deploy1
✓ Created deploy2
✓ SSH key injected for ubuntu@deploy2
✓ Created deploy3
✓ SSH key injected for ubuntu@deploy3
ℹ VM IP Addresses:
deploy1: 192.168.64.2
deploy2: 192.168.64.3
deploy3: 192.168.64.4
ℹ SSH PRIVATE KEY (copy this into your deployment tool):
═══════════════════════════════════════════════════════════════
-----BEGIN OPENSSH PRIVATE KEY-----
...
-----END OPENSSH PRIVATE KEY-----
═══════════════════════════════════════════════════════════════
Saving a Clean Snapshot
After you’ve commissioned your servers with your deployment tool (installed Docker, configured firewalls, set up WireGuard mesh, etc.), save that state:
./multipass-manager.sh snapshot
This stops all VMs, takes a snapshot named clean, and restarts them. That’s your baseline.
Breaking and Resetting
Now go wild. Deploy an app, misconfigure something, watch it explode. Then:
./multipass-manager.sh restore
Thirty seconds later, your VMs are back to the exact state they were in after commissioning. No orphaned containers, no leftover databases, no weird state.
Full Cleanup
When you’re completely done testing:
./multipass-manager.sh destroy
This removes all VMs, purges snapshots, removes SSH config entries, and deletes the local state directory. Your system is squeaky clean.
Why This Matters
I didn’t appreciate how much this workflow would change things until I started using it daily. The feedback loop tightened dramatically.
Before, I would avoid testing certain edge cases because the cleanup was too painful. I’d write code and hope it worked rather than breaking things deliberately to see how they failed.
Now I break things on purpose. I deploy, restore, deploy again. I test the happy path, then the sad path, then the really-ugly-edge-case path.
The psychological shift is profound. When cleanup is free, experimentation is free.
Tips From the Trenches
A few things I’ve learned running this setup:
1. Snapshot after commissioning, not before.
You want your clean state to include Docker already installed and your deployment agent already running. This lets you jump straight into testing deployments without re-running initial setup.
2. Run ssh-update after restarts.
VMs keep their IPs across restarts most of the time, but not always. The command updates your ~/.ssh/config so you can just ssh deploy1 without hunting for IPs.
./multipass-manager.sh ssh-update
3. Three VMs is a good default.
One machine for testing simple deployments. Three for testing multi-node setups, mesh networking, or load balancing. You can change this in the script’s configuration section.
4. Keep the script in your project.
I keep vms/multipass-manager.sh at the root of my infrastructure repo. That way the SSH keys and VM state live with the project, not scattered across my home directory.
The Script
Here’s the core of what makes it tick. You can find the full script on GitHub Gist , but the magic is straightforward:
# Configuration
VM_PREFIX="deploy"
NUM_VMS=3
SNAPSHOT_NAME="clean"
# Create a VM and inject SSH key
multipass launch 24.04 --name "$VM_NAME" --memory 2G --cpus 2 --disk 10G
multipass exec "$VM_NAME" -- bash -c "echo '$pubkey' >> ~/.ssh/authorized_keys"
# Snapshot
multipass stop "$VM_NAME"
multipass snapshot "$VM_NAME" --name "$SNAPSHOT_NAME"
multipass start "$VM_NAME"
# Restore
multipass restore "${VM_NAME}.${SNAPSHOT_NAME}"
Multipass handles the heavy lifting. The script just orchestrates the ceremony: tracking VMs, managing keys, updating SSH config, and ensuring cleanup is thorough.
Wrapping Up
Testing infrastructure tools doesn’t have to be painful. The combination of local VMs and snapshot/restore gives you a tight, fast iteration loop that rivals anything you’d get with cloud providers — without the cost or latency.
If you’re building something like Kamal, Coolify, or your own deployment solution, I’d encourage you to try this approach. The time you invest upfront pays dividends every time you hit that restore command and watch your test environment snap back to pristine in seconds.
Happy breaking.
Have thoughts or improvements? Leave a comment below.


