The Webcam That Stopped the Container From Booting
In Getting a Webcam and a Microphone Into a Model, Through a Proxmox LXC the camera and its microphone went into the container as
device_passthroughentries, the same mechanism as the GPU. That post was right about everything that happens once the devices are inside. It was wrong about how to get them there, and it took a reboot to show it.
On 2026-10-12 the Proxmox host rebooted and the container did not come back:
Device /dev/snd/pcmC2D0c does not exist
Two facts that combine
1. A Proxmox devN passthrough is mandatory. Each dev0:, dev1: line in the container config names a host device node, and if that node is missing at start, the container does not start. For a GPU that is reasonable: no GPU, nothing useful to do. For a USB webcam that is unplugged, or slow to enumerate, it turns a peripheral into a single point of failure for the whole machine.
2. ALSA card numbers are not stable across boots. The host has two onboard HDA sound controllers plus the camera’s microphone. Which one becomes card 0, 1 or 2 depends on which driver finishes probing first, and that races. The camera’s mic was card 2 on one boot and card 0 on the next. The passthrough named pcmC2D0c, card 2, device 0, capture. After the reboot that node did not exist, because the mic was now pcmC0D0c.
Neither fact alone is a problem. A stable number with a mandatory mount is fine; an unstable number with an optional mount is fine. Together, a routine reboot takes the container down.
The fix: optional bind mounts
LXC has its own mechanism underneath Proxmox’s, and it has a flag for exactly this. In /etc/pve/lxc/<vmid>.conf:
lxc.cgroup2.devices.allow: c 116:* rwm
lxc.cgroup2.devices.allow: c 81:* rwm
lxc.mount.entry: /dev/snd dev/snd none bind,optional,create=dir
lxc.mount.entry: /dev/video0 dev/video0 none bind,optional,create=file
lxc.mount.entry: /dev/video1 dev/video1 none bind,optional,create=file
optionalis the whole point: a missing source is skipped, and the container boots without the camera instead of not booting.- Binding the whole
/dev/snddirectory rather than individual nodes makes the card number irrelevant. Whatever number the mic gets this boot, it is in there. A mic plugged in after boot shows up too. - The two
devices.allowlines open the device cgroup for the character-device majors involved: 116 for ALSA and 81 for video4linux. A bind mount puts the node in the container’s filesystem; the cgroup rule is what lets it be opened.
Permissions: a udev rule, scoped tightly
An unprivileged container maps its users to high UIDs on the host, so a node owned by root:audio with mode 0660 is not openable from inside. The devN mechanism had been setting the mode for us; a bind mount does not.
So a host udev rule sets mode 0666, matched on the camera’s USB vendor and product ID and on the ALSA timer, and nothing else. Binding all of /dev/snd means the container can see the host’s other sound cards, but it cannot open them, because only the camera’s nodes are world-accessible. Matching on USB ID rather than on a device path also survives the camera moving ports.
Open the mic by name, never by number
Binding the directory fixes the container. It does not fix code that says hw:0,0, which is still wrong on every boot where the mic lands on another card. ALSA can address a card by its id string instead:
DEVICE = "plughw:CARD=U2K,DEV=0" # the camera's card id, stable across bootsplughw rather than hw also adds ALSA’s conversion layer, so a request for a sample rate or format the hardware does not offer is converted instead of refused. (The earlier post’s advice still holds: record at the device’s native rate and resample yourself, so you know what conversion happened.)
The same thinking applies to /dev/video0 with less help. If another camera enumerates first, the UGREEN becomes video2. The host has a second webcam that is deliberately not passed through, which is exactly the setup in which that can happen. /dev/v4l/by-id/ gives stable names, and it is the next thing to move to.
And it no longer lives in the IaC
The container is declared in Pulumi, and the GPU passthrough stays there as devN entries. The camera lines cannot: Proxmox only allows raw lxc.* keys to be written by root@pam, and the API token the IaC uses is deliberately not root. So the bind mounts live by hand in the container config, and the Pulumi program carries the exact lines as a comment beside the GPU passthrough, with the date and the error that put them there. There is also a unit test asserting that no capture device is ever added back as a passthrough, so the mistake cannot be re-made by tidying up.
Hand-maintained config next to IaC is a compromise. A comment that says exactly what is out of band, and a test that stops the managed half drifting back, is the honest version of it.
Takeaway
| Device | Mechanism | Why |
|---|---|---|
| GPU | Proxmox devN passthrough |
Mandatory is right: no GPU, nothing to do |
| Webcam, mic | lxc.mount.entry ... bind,optional |
A missing peripheral must not stop the machine |
The general rule: a peripheral should never be able to stop a machine from booting, and anything numbered at enumeration time will eventually be numbered differently. Bind optionally, bind the directory, and address devices by a name the hardware carries rather than an order the kernel assigned.