
Your Incus VM shows as RUNNING in incus list, but incus exec and SSH no longer answer? When the guest agent is dead, the VM operating system never finished booting: it is most likely stuck in emergency mode. The short answer: stop going through incus exec (which needs the agent), capture the serial console with incus console, and if that is not enough, mount the VM disk from the host to read its systemd journal offline. This guide walks that diagnosis end to end, including a real case where hardening bricked the EFI boot.
What you will learn
- Tell a dead agent from a plain SSH issue: why a
RUNNINGVM stays unreachable. - Capture the serial console of a VM that no longer answers, despite having no real TTY.
- Mount the disk of a stopped VM from the host (ZFS, dir, qcow2) to read its journal.
- Read the offline journalctl of a failed boot with
journalctl -D. - Analyse a real case where a hardening sysctl broke the
/boot/efimount. - Put the safety nets in place: snapshots, persistent journald, a break-glass root.
Prerequisites
- An Incus host with at least one virtual machine (not a container).
- Root access on the host, which the disk mounting operations require.
- Some knowledge of systemd (targets, units) and Linux partitioning.
The symptom: RUNNING but unreachable
You restart a VM, then nothing. incus list still shows it started:
incus list <vm># +------+---------+------+------+-----------------+-----------+# | NAME | STATE | IPV4 | IPV6 | TYPE | SNAPSHOTS |# +------+---------+------+------+-----------------+-----------+# | <vm> | RUNNING | | | VIRTUAL-MACHINE | 0 |# +------+---------+------+------+-----------------+-----------+Note the missing IPv4 address: the operating system never configured its network. An execution attempt confirms the diagnosis:
incus exec <vm> -- whoami# Error: VM agent isn't currently runningThe RUNNING state in Incus only means QEMU is running the machine. It says nothing about the guest system. Here the agent (incus-agent) never started: the operating system stopped along the way. This is not an SSH problem, it is a boot that never completes.
Why the usual reflexes fail on a VM
On an Incus container, troubleshooting is direct. On a virtual machine, several usual commands fall flat, because they assume a working guest or a real terminal.
| Command | On a stuck VM | Why |
|---|---|---|
incus exec <vm> | VM agent isn't currently running | needs the agent, so a booted OS |
incus console <vm> --show-log | empty or partial serial log | the serial buffer is not populated like a container log |
incus info <vm> --show-log | of little use for a VM | no systemd boot detail |
incus console <vm> in a pipe | inappropriate ioctl for device | no real TTY inside a pipe |
incus exec died with the agent. --show-log, very useful for a container, does not give the internal boot journal of a VM, so another approach is needed. And piping incus console fails, because the command demands a pseudo-terminal.
Level 1: capturing the serial console
When you simply want to see the screen of the VM (the boot error message), attach the serial console. The trap: incus console demands a pseudo-terminal, so a naive redirection fails. You force a pty with script:
timeout 14 script -qec 'incus console <vm>' /dev/null </dev/null 2>&1 | tail -45script -qec '...' /dev/nullruns the command in a pty with no file recorded (-qquiet,-epropagates the exit code,-cruns the command).timeout 14cuts after 14 seconds, enough to capture the current screen.tail -45keeps only the last lines, where the final state appears.
What you will read, depending on the failure:
You are in emergency mode. After logging in, type "journalctl -xb" to viewsystem logs, "systemctl reboot" to reboot, or "exit" to continue bootup.or a start job that never finishes:
[*** ] A start job is running for /boot/efi (1min 30s / no limit)or a kernel panic. To detach the interactive console cleanly, press Ctrl+a then q.
Level 2: reading the journal offline by mounting the disk
This is the right reflex. Rather than fighting a blind console, you stop the VM, mount its disk from the host and read its systemd journal calmly. The procedure depends on the storage backend.
Identify the driver first:
incus storage list# +---------+--------+-------------+---------+# | NAME | DRIVER | DESCRIPTION | USED BY |# +---------+--------+-------------+---------+# | default | zfs | | 12 |# +---------+--------+-------------+---------+On ZFS, the VM disk is a zvol. Once the VM is stopped, that zvol sits at volmode=none: no /dev/zdN device is exposed. You expose it, read its partitions with kpartx, then mount the root read only:
incus stop <vm> --force
# Identify the disk zvol (the .block suffix)zfs list -t volume | grep <vm>Z=<pool>/virtual-machines/<vm>.block
# Expose the zvol as a block devicesudo zfs set volmode=dev "$Z"
# kpartx creates the partition mappings (/dev/mapper/zd0p1, zd0p2 and so on)sudo kpartx -av /dev/zd0
# Mount the root read only (p2 = ext4 root, p1 = EFI vfat)sudo mkdir -p /mnt/vmsudo mount -o ro /dev/mapper/zd0p2 /mnt/vmAfter the diagnosis, clean up in reverse order:
sudo umount /mnt/vmsudo kpartx -d /dev/zd0sudo zfs set volmode=none "$Z"On the dir backend, the disk is a raw image (root.img). You attach it to a loop device with partition scanning (-P):
incus stop <vm> --force
IMG=/var/lib/incus/storage-pools/default/virtual-machines/<vm>/root.imgsudo losetup -fP "$IMG"losetup -j "$IMG" # find the assigned loopN (for example /dev/loop0)
sudo mkdir -p /mnt/vmsudo mount -o ro /dev/loop0p2 /mnt/vmCleanup:
sudo umount /mnt/vmsudo losetup -d /dev/loop0For a qcow2 image, you go through the Network Block Device:
incus stop <vm> --force
sudo modprobe nbd max_part=8sudo qemu-nbd --connect=/dev/nbd0 <image>.qcow2
sudo mkdir -p /mnt/vmsudo mount -o ro /dev/nbd0p2 /mnt/vmCleanup:
sudo umount /mnt/vmsudo qemu-nbd -d /dev/nbd0Once the root is mounted, read the failed boot journal. On a mounted disk the "current boot" is not necessarily the last one: list the boots, then target the offset you want.
# List the recorded bootssudo journalctl -D /mnt/vm/var/log/journal --list-boots# IDX BOOT ID FIRST ENTRY LAST ENTRY# -1 a1b2... 2026-06-29 09:14 2026-06-29 09:31# 0 c3d4... 2026-06-29 09:32 2026-06-29 09:32 <- the failed boot
# Read the errors of that bootsudo journalctl -D /mnt/vm/var/log/journal -b 0 -p err --no-pager
# Find the cause directlysudo journalctl -D /mnt/vm/var/log/journal -b 0 --no-pager \ | grep -iE 'Dependency failed|Failed to mount|emergency'That final grep almost always surfaces the chain of causality: a unit that fails, an unsatisfied dependency, then the switch to emergency.
Case study: hardening that bricks the EFI boot
Here is a real, reproducible case that illustrates the whole chain. The offline journal reveals this:
systemd[1]: Mounting boot-efi.mount - /boot/efi...mount[420]: mount: /boot/efi: unknown filesystem type 'vfat'.systemd[1]: boot-efi.mount: Mount process exited, code=exited, status=32systemd[1]: Failed to mount boot-efi.mount - /boot/efi.systemd[1]: Dependency failed for local-fs.target - Local File Systems.systemd[1]: Reached target emergency.target - Emergency Mode.The root of the problem: hardening had set kernel.modules_disabled=1 through a /etc/sysctl.d/ drop-in. That sysctl is a one-way switch: once at 1, no kernel module can be loaded any more, and it cannot go back to 0 without a reboot.
systemd applies sysctls very early at boot, through systemd-sysctl. Its ordering relative to mounting /boot/efi is not guaranteed: both converge on sysinit.target with no strict mutual ordering. If kernel.modules_disabled=1 is applied before mounting /boot/efi triggers loading the vfat module, and if that module is not already loaded, the mount fails. No vfat, no EFI partition mounted, local-fs.target fails, and the system drops into emergency mode.
The lesson: a one-way disabling sysctl such as kernel.modules_disabled must never live in a sysctl.d drop-in applied at boot. Set it late, through a systemd oneshot unit ordered After=local-fs.target (or even after the network), once every needed module is already loaded.
Recovery and safety net
Before any risky operation on a VM, take a snapshot. That is your way back:
# Before a risky boot changeincus snapshot create <vm> before-hardening
# Roll back if the VM no longer bootsincus snapshot restore <vm> before-hardeningOn the access side, never lock root without leaving a way back in. Emergency mode is precisely when you need it:
- a local recovery account with a password,
- or a documented GRUB password to edit the kernel command line at boot.
Prevention: the checklist
The best debugging is the kind you never have to do. Three reflexes avoid most of these lockups:
-
Persistent journald from the start
On your lab VMs, enable
Storage=persistent(or create/var/log/journal/) at creation time. Without it, the failed boot journal goes up in smoke at reboot. -
Test boot changes on a disposable VM
Anything touching boot (
/etc/fstab, the kernel command line, a one-way sysctl) gets tested on a disposable VM protected by a snapshot, never directly on a machine that matters. -
Find the culprit through the trace, not through blind reboots
Prefer a dry run of your configuration (the converged state without rebooting) to spot a dangerous change before it bricks the boot, rather than restarting at random.
Key points
- A
RUNNINGVM with no agent has not finished booting: it is not an SSH problem. incus execneeds the agent; on a stuck VM it returnsVM agent isn't currently running.- For a VM,
--show-loggives an often empty serial log: capture the serial console (incus console, detach with Ctrl+a q), forced into a pty throughscript. - The right reflex: stop the VM, mount its disk from the host (ZFS through
volmode=devpluskpartx, dir throughlosetup -fP, qcow2 throughqemu-nbd) and read the journal withjournalctl -D. volmode=devhides the zvol partitions, hencekpartx.- A one-way sysctl such as
kernel.modules_disabled=1set too early can break a mount (/boot/efiinvfat) and cause emergency mode. - Snapshots, persistent journald and a break-glass root are your three safety nets.
FAQ: common questions about debugging an Incus VM
The guest agent is dead
incus exec <vm> relies on the incus-agent running inside the virtual machine. That agent only starts once the guest operating system is far enough into its boot.
If the VM is stuck at boot (emergency mode, a frozen start job, a kernel panic), the agent never started:
incus exec <vm> -- whoami
# Error: VM agent isn't currently running
The RUNNING status in incus list only means QEMU is running the machine, not that the guest OS works. So this is not an sshd or networking problem: the VM stopped earlier in the boot.
Mount the disk, then journalctl -D
When the agent is dead, you read the journal offline, from the host:
incus stop <vm> --force
# Expose and mount the disk (ZFS case)
sudo zfs set volmode=dev <pool>/virtual-machines/<vm>.block
sudo kpartx -av /dev/zd0
sudo mount -o ro /dev/mapper/zd0p2 /mnt/vm
# Read the failed boot
sudo journalctl -D /mnt/vm/var/log/journal --list-boots
sudo journalctl -D /mnt/vm/var/log/journal -b 0 -p err --no-pager
The -D option (or --directory) points journalctl at an external journal directory. The indispensable condition: the VM journal has to be persistent (Storage=persistent), otherwise it lived in RAM and vanished at reboot.
Of little use on a VM
For a container, incus console <name> --show-log shows the console journal, which is very handy.
On a virtual machine that serial log is often empty or partial: the serial buffer is not populated the way a container journal is, and it holds no systemd boot detail.
For a VM, attach the interactive serial console instead:
incus console <vm>
# Detach: Ctrl+a then q
It stays available even when the agent is not running, which lets you see the emergency mode screen. But there is no scrollback: for the complete, reproducible detail, switch to the offline journal (journalctl -D).
volmode=dev, then kpartx
On ZFS, a VM disk is a zvol. With the VM stopped it sits at volmode=none, exposing no device.
incus stop <vm> --force
Z=<pool>/virtual-machines/<vm>.block
sudo zfs set volmode=dev "$Z" # creates /dev/zd0
sudo kpartx -av /dev/zd0 # creates /dev/mapper/zd0p2
sudo mount -o ro /dev/mapper/zd0p2 /mnt/vm
The key point: volmode=dev exposes /dev/zd0 but hides its partitions, which is why kpartx is needed (partprobe alone is not always enough).
Clean up in reverse order:
sudo umount /mnt/vm
sudo kpartx -d /dev/zd0
sudo zfs set volmode=none "$Z"
A mount that fails
Emergency mode fires when local-fs.target fails, usually because of an impossible mount: a wrong /etc/fstab, a missing partition, or a filesystem that cannot be mounted.
One concrete, reproducible case: hardening sets kernel.modules_disabled=1 through sysctl.d. That sysctl is a one-way switch; applied early at boot, it prevents the vfat module from loading, which breaks the /boot/efi mount:
mount: /boot/efi: unknown filesystem type 'vfat'.
Dependency failed for local-fs.target.
Reached target emergency.target.
The offline journal (journalctl -D ... | grep -iE 'Dependency failed|Failed to mount') reveals the exact chain of causality. A one-way sysctl has to be applied late, not in a boot drop-in.
Next steps
- Securing Incus: the resource limits and isolation that stop a drifting instance taking the host with it.
- Managing instances: the console commands this guide relies on, in their ordinary context.
- Incus OS without a shell: a system with no shell, where diagnosis goes entirely through the serial console and the API.