DEEP END
Tested for real: I ran every command in this guide top-to-bottom on a fresh Ubuntu 24.04 virtual machine (4 CPUs, 8 GB RAM) on 13 August 2026. The run continued directly from the end state of Headless VMs. Versions at test time: Ubuntu 24.04.4 LTS, libvirt 10.0.0, QEMU 8.2.2, virt-clone 4.1.0, virt-sysprep 1.52.0.
Found a problem? Tell me via the contact page.
Placeholders: anything written in CAPS-WITH-DASHES, like YOUR-VM-IP-HERE or YOUR-VM2-IP-HERE, is a value you replace with your own — here that means the addresses from your lease table. $USER is different — leave it exactly as written; the shell fills in your username for you.
What you'll end up with
Snapshots you can rewind to, and a clone that you break and then properly fix. The breaking is deliberate: cloning has a common identity trap. I hit it for real in testing, and you will watch the whole diagnosis.
The finished state from my test run — two independent machines, born a few minutes apart from one image:
$ virsh net-dhcp-leases default
Expiry Time MAC address Protocol IP address Hostname Client ID or DUID
------------------------------------------------------------------------------------------------------
2026-08-13 18:03:49 52:54:00:04:09:f8 ipv4 192.168.122.82/24 vm1 ff:56:...:20:99
2026-08-13 18:15:37 52:54:00:42:32:0e ipv4 192.168.122.153/24 vm2 ff:56:...:dd:d0
Who this is for
DEEP END. You have done Headless VMs: virt-install, cloud-init and snapshots without a desktop and kept its end state: vm1 running, reachable over SSH with the vm-lab-key. This guide starts exactly there.
Time and cost
The commands for this guide and the headless guide took me about 7 minutes of measured run time in total, as one continuous run. Budget 45 minutes for this half. Cost: nothing; every tool is in Ubuntu's archive.
One honesty note: my test machine is itself a virtual machine, so my guests are nested and their disk speed is the worst case. Your times may be better on real hardware, but storage and network speed still matter.
Words you'll meet
- Internal snapshot — a saved moment (disk, and RAM if running) stored inside the qcow2 file. There are also external snapshots, discussed honestly below. (basics)
- machine-id — a unique identity string every Linux install carries in
/etc/machine-id. Clones inherit it, which causes real trouble. (basics) - DUID — the identifier a machine presents to DHCP. On Ubuntu it is derived from the machine-id — which is why cloned machine-ids poison DHCP. (basics)
- DHCP lease — the network's record of "this machine has this IP".
virsh net-dhcp-leasesis how you find a headless guest. (basics)
Before you start
- The end state of the headless guide —
vm1running, the~/.ssh/vm-lab-keypair, and the libvirt group membership. - Around 4 GB free disk and 4 GB free RAM while both guests run.
The steps
Step 1 — snapshots: save a moment, ruin things, rewind
An honest orientation first. Libvirt has two snapshot styles.
Internal snapshots live inside the qcow2 file: one file to manage, one command each way, and they can include RAM while the guest runs. The trade-offs: qcow2-only, slower on big disks, and some tooling (block commit, incremental backup workflows) does not support them.
External snapshots write changes to a new overlay file. Faster and more flexible — but you manage a chain of files, and merging them back is on you.
For learning and for small lab guests, internal is the honest, simple choice — that is what we test here. Know that external exists before you build production backups on either. This guide does not teach external snapshot cleanup or production backup design.
Take a snapshot of the running guest — RAM included:
virsh snapshot-create-as vm1 clean-state --description "Fresh vm1 before experiments"
The important line is:
Domain snapshot clean-state created
Two seconds, in my test — the guest never stopped running.
Now damage the future: create a file that the snapshot knows nothing about (from the host, in one line):
ssh -i ~/.ssh/vm-lab-key ubuntu@YOUR-VM-IP-HERE "echo do-not-lose-me > ~/precious.txt; ls -l ~/precious.txt"
Your output will vary, but look for:
-rw-rw-r-- 1 ubuntu ubuntu 15 Aug 13 17:02 /home/ubuntu/precious.txt
List snapshots:
virsh snapshot-list vm1
You should see something like:
Name Creation Time State
----------------------------------------------------
clean-state 2026-08-13 17:02:18 +0000 running
Rewind:
virsh snapshot-revert vm1 clean-state
A passing run includes:
Domain snapshot clean-state reverted
Three seconds, and the guest is running again immediately — reverting to a running snapshot restores the RAM too, no reboot.
Check it worked. The file made after the snapshot must be gone:
ssh -i ~/.ssh/vm-lab-key ubuntu@YOUR-VM-IP-HERE "ls -l ~/precious.txt"
You should see something like:
ls: cannot access '/home/ubuntu/precious.txt': No such file or directory
That error is the pass mark. Time travel, demonstrated. And the obvious warning made explicit: everything after a snapshot dies when you revert — snapshots are a lab tool and a pre-upgrade parachute, not a backup.
Step 2 — clone it, and watch the clone break
You want a second VM. Copying feels free — one command, no download. Do it, and then watch closely, because what happens next is the most valuable part of this guide.
Know the shape of the problem before you meet it. A clone is a full copy — including its identity. Making it a genuinely new machine means fixing four things: the machine-id, the netplan MAC pin, the hostname, and the SSH host keys. This step springs the trap; the next step fixes all four.
virt-clone needs the source shut off:
virsh shutdown vm1
Clone (8 seconds in my test):
virt-clone --original vm1 --name vm2 --auto-clone
The important line is:
Clone 'vm2' created successfully.
Two notes from my test before we boot anything. The snapshot did not come along — virsh snapshot-list vm2 is empty. And the seed CD was not copied — vm2 points at vm1's seed file (it is read-only, so virt-clone shares it). Both matter later.
Boot both:
virsh start vm1
virsh start vm2
Now the trap springs. vm1 comes back on its old address. vm2 boots, runs — and never appears:
virsh net-dhcp-leases default
You should see something like:
Expiry Time MAC address Protocol IP address Hostname Client ID or DUID
------------------------------------------------------------------------------------------------------
2026-08-13 18:03:49 52:54:00:04:09:f8 ipv4 192.168.122.82/24 vm1 ff:56:...:20:99
One lease. I waited over four minutes in testing — vm2 never asked for an address at all.
Here is the diagnosis, exactly as I ran it. Read the clone's inherited network config through vm1 (they are identical clones, after all):
ssh -i ~/.ssh/vm-lab-key ubuntu@YOUR-VM-IP-HERE "sudo cat /etc/netplan/50-cloud-init.yaml"
Your output will vary, but look for:
network:
version: 2
ethernets:
enp1s0:
match:
macaddress: "52:54:00:04:09:f8"
dhcp4: true
There it is. Cloud-init pinned the network card by MAC address on first boot. virt-clone — correctly — gave vm2 a fresh MAC:
virsh domiflist vm2
Your output will vary, but look for:
Interface Type Source Model MAC
-------------------------------------------------------------
vnet4 network default virtio 52:54:00:42:32:0e
Different MAC. The clone's netplan matches no interface, so it configures nothing — it never even asks for DHCP.
And lurking one layer down: the clone also inherited vm1's /etc/machine-id, from which Ubuntu derives its DHCP DUID. Even with netplan fixed, the two machines would present the same identity to the DHCP server. A clone is not a new machine until you make it one.
Step 3 — give the clone its own identity
Shut the clone down; we operate on its disk from outside:
virsh shutdown vm2
Install libguestfs — the toolkit for editing guest disks without booting them (110 packages, 42 seconds in my test):
sudo apt-get install -y libguestfs-tools
Eject the seed CD it shares with vm1 — the clone must not re-read vm1's first-boot config:
virsh change-media vm2 sda --eject --config
The important line is:
Successfully ejected media.
Reset the machine-id and set the hostname. One deliberate flag choice here: --enable machine-id,customize runs only those operations. virt-sysprep's default operation set also wipes authorized_keys — which would lock us out of our own clone:
sudo virt-sysprep -d vm2 --enable machine-id,customize --hostname vm2
A passing run includes:
[ 0.0] Examining the guest ...
[ 8.1] Performing "machine-id" ...
[ 8.1] Performing "customize" ...
[ 8.2] Setting the machine ID in /etc/machine-id
[ 8.2] Setting the hostname: vm2
Un-pin the network card — rewrite the clone's netplan to plain "DHCP on enp1s0, any MAC":
sudo virt-customize -d vm2 --run-command "printf 'network:\n version: 2\n ethernets:\n enp1s0:\n dhcp4: true\n' > /etc/netplan/50-cloud-init.yaml && chmod 600 /etc/netplan/50-cloud-init.yaml"
A passing run includes:
[ 0.0] Examining the guest ...
[ 19.8] Running: printf [...snipped...]
[ 19.9] Finishing off
Boot it:
virsh start vm2
Check it worked. 34 seconds later in my test:
virsh net-dhcp-leases default
You should see something like:
Expiry Time MAC address Protocol IP address Hostname Client ID or DUID
------------------------------------------------------------------------------------------------------
2026-08-13 18:03:49 52:54:00:04:09:f8 ipv4 192.168.122.82/24 vm1 ff:56:...:20:99
2026-08-13 18:15:37 52:54:00:42:32:0e ipv4 192.168.122.153/24 vm2 ff:56:...:dd:d0
Two leases, two hostnames, two different DUIDs — that last column differing is the machine-id reset doing its job. Prove it end to end:
ssh -i ~/.ssh/vm-lab-key ubuntu@YOUR-VM2-IP-HERE "hostname; cat /etc/machine-id"
Your output will vary, but look for:
vm2
2336d8cbf5e0a8015956a0b344b0a4ea
vm1 reported b93b68cff1c749298b89ce14210e5df1 in my test — different identity, as required.
One last inherited secret: the clone still carries vm1's SSH host keys (I checksummed them — byte-identical). For a disposable lab this is acceptable; for anything real, regenerate them — otherwise two servers claim the same cryptographic identity. Regenerate inside vm2:
ssh -i ~/.ssh/vm-lab-key ubuntu@YOUR-VM2-IP-HERE "sudo rm /etc/ssh/ssh_host_*; sudo dpkg-reconfigure -f noninteractive openssh-server; sudo systemctl restart ssh; sudo md5sum /etc/ssh/ssh_host_ed25519_key.pub"
You should see something like:
4bfddc6cc86a97a47acccf89392dde2d /etc/ssh/ssh_host_ed25519_key.pub
Different checksum from vm1's (mine was a36e8aa9...). Now they are genuinely two machines.
Something went wrong?
Everything below happened in my recorded run — most of it on purpose.
- Your clone is
runningbut never gets an IP → it means netplan is pinned to the original's MAC (and the machine-id is cloned underneath) → this is Steps 2-3's whole story; apply Step 3. - You see
error: ... cannot delete inactive domain with 1 snapshotswhen undefining → it means snapshot metadata blocks the delete → add--snapshots-metadata(the headless guide's Undo shows it).
Undo all of this
Tested for real — as one combined undo with the headless guide. Here is the part that removes what this guide added; that guide's undo then clears vm1, the packages, and the folders.
Force the clone off and delete it:
virsh destroy vm2
virsh undefine vm2 --remove-all-storage
(No snapshots on vm2, so no extra flag needed.)
Two things this guide added live on vm1's side, and the headless guide's undo handles both. The clean-state snapshot goes when vm1 is undefined — that is why its undo carries --snapshots-metadata; I captured the refusal without it. And libguestfs-tools, installed here in Step 3, is included in that guide's package purge.
Check it worked.
virsh list --all
Only vm1 remains in the list. Then run the headless guide's Undo; I ran the two back-to-back as one sequence and ended with a completely clean machine.
Where to go next
- The clone workflow you just learned is the seed of a real home lab: one golden image, sysprep, many guests. Pair it with Backups that actually restore before any guest holds data you care about — snapshots are not backups.
- Headless VMs: virt-install, cloud-init and snapshots without a desktop — the first half of this pair, if you landed here directly.
- Official docs: Ubuntu Server virtualisation documentation — plus
man virt-sysprepfor the full operations list (worth a read before trusting defaults).
Last tested: 13 August 2026 on Ubuntu 24.04.4 LTS. Versions: libvirt 10.0.0, QEMU 8.2.2, virt-clone 4.1.0, libguestfs/virt-sysprep 1.52.0. Tested as one continuous run with the headless VMs guide; transcripts live in ../guide-09-headless-vms/.
Tried it? Improved it?
Tell the forum what worked and what didn’t: real experience beats recommendations, and the best answers get folded back into this guide with credit.
Related guides
SSH keys, properly: never type a server password again
Your laptop will hold a small pair of files called an SSH key. Your server will trust it, so logging in never asks for a password — then we turn password logins off completely.
12 min read
First hour with a new Ubuntu server: locking the doors
A fully updated server, a personal account with admin rights, a firewall that blocks everything except SSH, and security updates that install themselves.
10 min read