Contributed by @vk2amv (Lindsay). Adds two related operator features to the Docker VDI driver: 1. Bounded host port range via [vdi] port_range_start / port_range_end. Currently Docker picks an arbitrary high port for the container's RDP listener; this lets operators constrain it which matters for firewalls, reverse proxies, and identity-aware gates that need to know in advance which ports rustguac will use. Port selection inside the range is deterministic-from-username (FNV-1a hash), so reconnects from the same user get the same port. Falls through to the next port on collision. 2. Container lifecycle hook script via [vdi] container_hook_script. Called as 'up <port> <container_id> <container_name>' after Docker assigns the port, again as 'down ...' before removal. Lets deployments wire external preparation/cleanup (firewall opens, service mesh registration, identity-aware gates) without baking the logic into rustguac itself. Bounded timeout (default 10s). Script is invoked via Command::new (no shell), so no injection risk from container metadata. 3 new tests covering port-candidate behaviour. Docs in docs/configuration.md and docs/vdi.md. Thanks Lindsay.
7.9 KiB
VDI Desktop Containers
rustguac can spawn ephemeral Docker desktop containers on demand. Each user gets their own isolated Linux desktop accessible via the browser, with no client software required.
How it works
- An admin creates a VDI entry in the connections, specifying a Docker image
- When a user clicks Connect, rustguac creates a Docker container from that image
- The container runs xrdp on port 3389, and guacd connects to it via RDP
- The user sees a full Linux desktop in their browser
- On disconnect (tab close, network drop), the container keeps running for reconnection
- On logout from the desktop, the container is stopped and removed
- Idle containers (no active session) are automatically cleaned up after a configurable timeout
Prerequisites
VDI requires Docker on the same machine as rustguac. Install Docker and grant access:
# Install Docker (if not already installed)
curl -fsSL https://get.docker.com | sh
# Allow the rustguac user to manage containers
sudo usermod -aG docker rustguac
sudo systemctl restart rustguac
You also need at least one Docker image with xrdp pre-pulled on the host (see Docker image requirements below).
Configuration
Add a [vdi] section to your config file:
[vdi]
enabled = true
# docker_socket = "/var/run/docker.sock" # default
# default_cpu_limit = 2.0 # cores, 0 = no limit
# default_memory_limit = 2048 # MB, 0 = no limit
# ready_timeout_secs = 30 # wait for xrdp to start
# port_range_start = 39000 # optional localhost RDP port range
# port_range_end = 39999
# container_hook_script = "/opt/rustguac/vdi-container-hook.sh"
# container_hook_timeout_secs = 10
# idle_timeout_mins = 60 # container lifetime after disconnect
# home_base = "/vdi-homes" # persistent home directories
# allowed_images = ["myregistry/desktop:latest"] # whitelist, empty = allow all
The rustguac system user must be in the docker group:
sudo usermod -aG docker rustguac
sudo systemctl restart rustguac
Docker image requirements
rustguac supports two patterns for image authentication:
Pattern A: env-var driven (recommended for shared deployments)
The image reads VDI_USERNAME and VDI_PASSWORD from its entrypoint, calls useradd / chpasswd to provision the account, and starts xrdp. This is the default flow:
- rustguac derives
VDI_USERNAMEfrom the operator's identity (everything before@, lowercased, non-alphanumeric replaced with_). VDI_PASSWORDis freshly generated per connect (32 random hex chars).- Container name is deterministic per operator (
rustguac-vdi-<username>), so reconnects reuse the same container. - xrdp listens on port 3389 with TLS certificates configured.
A minimal example image is at contrib/vdi-test-image/ (Debian + xfce4).
Pattern B: baked-in account (for images with fixed credentials)
If your image has a hardcoded user account that does not honour VDI_USERNAME / VDI_PASSWORD, set container_username and container_password on the entry. rustguac uses those values for the RDP login into the container.
VDI_USERNAME / VDI_PASSWORD are still injected with the override values, so an image that also happens to read them gets consistent state. Images that ignore the env vars simply continue to use their baked-in account; rustguac just makes sure the RDP login matches.
The container name is still deterministic from the resolved username, so multiple operators connecting via an entry that uses a fixed container_username will share the same container instance. Confirm this is what you want before using Pattern B at scale.
Both container_username and container_password support credential variables, so the actual credential value can be sourced from each operator's saved credentials rather than stored in plain in the entry.
Example entrypoint
#!/bin/bash
set -e
USERNAME="${VDI_USERNAME:-user}"
PASSWORD="${VDI_PASSWORD:-password}"
if ! id "$USERNAME" &>/dev/null; then
useradd -m -s /bin/bash "$USERNAME"
fi
echo "$USERNAME:$PASSWORD" | chpasswd
echo "xfce4-session" > /home/"$USERNAME"/.xsession
chown "$USERNAME":"$USERNAME" /home/"$USERNAME"/.xsession
# Configure TLS for xrdp
sed -i \
-e 's|^certificate=.*|certificate=/etc/ssl/certs/ssl-cert-snakeoil.pem|' \
-e 's|^key_file=.*|key_file=/etc/ssl/private/ssl-cert-snakeoil.key|' \
/etc/xrdp/xrdp.ini
mkdir -p /run/dbus
dbus-daemon --system --fork 2>/dev/null || true
xrdp-sesman --nodaemon &
exec xrdp --nodaemon
Connections setup
- Create a folder in the connections (or use an existing one)
- Add a new entry with type VDI (Docker)
- Set the Container Image (e.g.
rustguac-vdi-test:latest) - Optionally set CPU limit, memory limit, environment variables, idle timeout
- If the image has a baked-in user account (Pattern B above), set Container username and Container password to match; otherwise leave them blank and the image's entrypoint will provision the account from
VDI_USERNAME/VDI_PASSWORD - Click Save
Users in the folder's allowed groups can now click Connect to get a desktop.
Container lifecycle
| Event | What happens |
|---|---|
| User clicks Connect | Container created (or reused if already running) |
| User closes browser tab | Container keeps running |
| Network drops | Container keeps running (reconnect when back online) |
| User logs out of desktop | Container stopped and removed |
| Idle timeout expires | Container stopped and removed by background reaper |
| Admin terminates session | Session ends, container keeps running |
Persistent home directories
Set home_base in the VDI config to enable persistent user data:
[vdi]
home_base = "/vdi-homes"
Each user gets {home_base}/{username} mounted as /home/{username} inside the container. Files persist across container restarts. The directory is created automatically on first use.
Active Sessions
The connections shows an Active Sessions section with thumbnail previews of running sessions. Thumbnails are captured every 10 seconds from the browser display. Click a thumbnail to reconnect.
Dormant VDI containers (running but no active browser session) also appear with their last captured thumbnail.
Container hook
Set container_hook_script when Rustguac needs an external command to prepare
or tear down access to a container's mapped RDP port. This can be used for
deployment-specific setup that must happen after Docker has assigned the port
and before Rustguac starts probing xrdp.
Rustguac calls the script as:
/opt/rustguac/vdi-container-hook.sh up <port> <container_id> <container_name>
/opt/rustguac/vdi-container-hook.sh down <port> <container_id> <container_name>
up runs after Docker inspect finds the mapped RDP port and before Rustguac
checks whether xrdp is ready on 127.0.0.1:<port>. The script should return
only after the local listener is available. down runs before Rustguac stops
and removes the container. Hook execution is limited by
container_hook_timeout_secs (default: 10 seconds).
Per-entry settings
Each VDI connections entry can override:
- CPU limit (cores) — overrides
default_cpu_limit - Memory limit (MB) — overrides
default_memory_limit - Idle timeout (minutes) — overrides
idle_timeout_mins - Environment variables — custom
KEY=VALUEpairs passed to the container - Banner — message shown before session starts
Security notes
- Container images must be pre-pulled on the Docker host (no automatic pull)
- Use
allowed_imagesto restrict which images can be used - Containers run with default Docker isolation (no
--privileged) - Credentials are auto-generated per session (users never see the RDP password)
- The
rustguacuser needs Docker socket access but no other elevated permissions