Kasm Workspaces is a self-hosted containerized desktop/browser streaming platform β browser-based VDI. Pointed at Proxmox VE, it can automatically clone a template VM on demand as session load increases ("autoscale") instead of running a fixed pool of always-on agents. Getting there took several rounds of real trial and error β here's the whole install path plus every gotcha found along the way.
# Resource pool for Kasm-managed VMs
pveum pool add kasm-workspaces --comment "Kasm Autoscale Pool"
# Dedicated user + API token
pveum user add kasm@pve --comment "Kasm Workspaces Provisioner"
pveum passwd kasm@pve
pveum user token add kasm@pve kasm-token --privsep 0
Permissions needed are broader than they first appear. Custom privilege lists kept hitting
invalid-privilege-name errors (e.g. VM.Monitor isn't a real privilege on current Proxmox) β
Proxmox's built-in roles turned out simpler and more reliable than hand-rolling a custom one:
pveum aclmod /pool/kasm-workspaces -user kasm@pve -role PVEVMAdmin
pveum aclmod /pool/kasm-workspaces -user kasm@pve -role PVEDatastoreUser
pveum aclmod / -user kasm@pve -role PVEPoolAdmin
This is the minimum permission set found through live trial and error β each error below only surfaced once the previous one was fixed, at successive stages of the actual clone operation:
| Error | Root cause | Fix |
|---|---|---|
No Resource Pools found matching name(<pool>) |
Template VM was never added as a member of the pool β creating the pool doesn't auto-populate it | pvesh set /pools/<pool> -vms <template-vmid> |
No Storage Pools found matching name(<storage>) |
Token had zero visibility into node-level storage | pveum aclmod / -user kasm@pve -role PVEDatastoreAdmin |
403 Forbidden ... SDN.Use |
No SDN permission granted at all | PVESDNUser, plus explicit grants at the zone and zone/bridge paths β root-level alone didn't fully propagate |
Error uploading startup script |
The "Startup Script Path" field expects a directory, and appends its own filename | Use a bare directory path (e.g. /tmp/), never a filename |
curl with the token, to isolate whether a problem is Proxmox-side permissions or a
Kasm-side bug β and read the manager's real traceback from container logs
(docker logs kasm_manager --since 10m), since the UI's error banner is too generic to debug from.
qemu-guest-agent installed on the base OS.qemu-guest-agent.service can show inactive (dead) if the
virtio-serial device isn't present β caused by the QEMU Guest Agent option not being enabled on the VM in
Proxmox. Enabling it requires a full VM restart, not just a guest reboot.sudo truncate -s 0 /etc/machine-id
sudo rm -f /var/lib/dbus/machine-id
sudo ln -s /etc/machine-id /var/lib/dbus/machine-id
sudo shutdown -h now
qm template <vmid>.#!/bin/bash
systemctl enable --now docker
systemctl enable --now qemu-guest-agent
cd /tmp
curl -O https://kasm-static-content.s3.amazonaws.com/kasm_release_<version>.tar.gz
tar -xf kasm_release_<version>.tar.gz
MY_IP=$(hostname -I | cut -d' ' -f1)
bash kasm_release/install.sh --role agent \
--accept-eula \
--manager-hostname <manager-ip> \
--manager-token <token-from-Settings-Global> \
--public-hostname "$MY_IP"
$1-style shell syntax gets misread as a Kasm template variable, not literal
shell. awk '{print $1}' in a startup script broke every single provisioning attempt with
Exception during provisioning ... : 'print $1'. Use cut -d' ' -f1 instead β no
$N positional syntax for the templating engine to misfire on.
--accept-eula is mandatory. Without it, install.sh hangs
forever at an interactive EULA prompt with no TTY to answer it. The agent software never starts, so the
manager's health check times out and destroys the VM β then retries forever. This produces the exact same
symptom as several other, completely unrelated bugs below. Don't assume a recurrence is the same root
cause as last time just because the symptom looks identical β re-diagnose from the actual logs each time.
The manager destroys and retries any VM that doesn't check in within its timeout, logging generic messages
(Timed out trying to establish agent connection, has not checked in. Destroying!).
These don't tell you why β three completely different root causes produced this exact symptom in
testing (a bad startup script, a stale API token, and an unrelated network flake). Every time, the fix was to
catch a live clone before it gets destroyed and inspect it directly, using Proxmox's own
guest-agent channel β works even with no SSH key on the ephemeral clone:
qm guest exec <clone-vmid> -- uptime
qm guest exec <clone-vmid> -- sudo docker ps --format '{{.Names}}: {{.Status}}'
qm guest exec <clone-vmid> -- sudo sh -c 'tail -40 /tmp/kasm_install_*.log'
Zero running containers and no install log at all β the startup script never executed (usually a Proxmox
permissions/token problem at the guest-agent-exec layer). An install log that exists but stops partway
through β a real failure inside install.sh itself; its last lines are the actual error.
401 means the
credential itself is bad/stale; 403 means it's valid but lacks a specific permission. This is
decisive where reading permission grants alone can be ambiguous.
If Standby Cores/Memory are set high relative to a single agent's own footprint, the autoscaler's math can perpetually conclude "one more agent is needed" even with an already-healthy agent running β continuous clone/destroy cycling that looks identical to a broken-clone loop but has nothing to do with clone success or failure. Setting Standby Cores/Memory to 0 (provision only on actual session demand) is the most reliable way to stop churn while debugging anything else, independent of whatever else might also be wrong.
If the template and every dynamic clone both live on the same physical storage pool as another I/O-sensitive workload, every clone/destroy cycle does a full disk-image read+write burst that measurably competes for I/O. The clone-target storage (the AutoScale Config's "Storage Pool Name") is a separate setting from the template's own storage location β moving just the clone target elsewhere (leaving the template's reads on the original pool, since reads are far cheaper than writes) can resolve contention without migrating the whole template.
Kasm encrypts sensitive config fields (like a stored cloud-provider API token) at the application layer, not just at rest β a pattern common across self-hosted apps for anything credential-shaped. Writing a plaintext value directly into such a column via raw SQL silently breaks the app's ability to decrypt it on every subsequent read.
Before writing to any DB column directly, check whether the application encrypts that specific field. If it
does, use the application's own UI/API instead β plain, unencrypted columns on the very same table remain
safe to edit directly via SQL. If a column does get broken this way with no way to obtain the app's encryption
key: delete the row if the foreign key allows it safely (ideally SET NULL, not CASCADE)
and re-enter the config through the UI, or better β restore the affected component from a backup taken before
the mistake, which brings back the intact row in one shot.
docker system df -v to see
what's actually consuming space, then docker image prune -f (dangling/untagged images only β
never removes a tagged image still in use).
The manager process appears to cache provider/autoscale config at container startup, not read it fresh on every provisioning attempt. After any direct database change to this config, restart the manager container before expecting the change to take effect β confirmed necessary more than once in testing. Don't assume a DB-level fix is live just because the write succeeded.