Kasm Workspaces Deployment: Stream Containerized Desktops via Docker
Complete guide to self-hosting Kasm Workspaces using Docker Compose. Stream secure, containerized desktop environments and browsers inside a web page.
Host OS Configuration & Kernel Tuning
Before deploying Kasm Workspaces, the host operating system (typically Ubuntu 22.04 LTS or Debian 12 on a VPS) must be optimized. Kasm runs isolated desktop environments as sibling Docker containers. Without kernel tuning and adequate swap space, heavy concurrent workloads will trigger the Linux Out-Of-Memory (OOM) killer or suffer from scheduling bottlenecks.
1. Enable and Configure Swap Space
Kasm sessions are memory-intensive. Even on an 8GB or 16GB RAM VPS, swap space is required to handle transient spikes in RAM usage without terminating user sessions. Run the following commands to provision an 8GB swap file:
# Disable active swap if needed
sudo swapoff -a
# Allocate an 8GB swap file
sudo fallocate -l 8G /swapfile
# Set strict permissions
sudo chmod 600 /swapfile
# Format as swap space
sudo mkswap /swapfile
# Enable the swap file
sudo swapon /swapfile
# Make the configuration persistent across reboots
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
2. Configure Kernel Virtual Memory Limits
Desktop containers running graphical applications (like Chromium or VS Code) allocate large amounts of virtual memory mappings. Increase the default limits to prevent system resource exhaustion:
# Set runtime virtual memory map limit
sudo sysctl -w vm.max_map_count=262144
# Persist the change across reboots
echo "vm.max_map_count=262144" | sudo tee -a /etc/sysctl.conf
Verify the system limits have updated correctly:
cat /proc/sys/vm/max_map_count
Kasm Workspaces Docker Compose Deployment
The LinuxServer.io Kasm image runs a complete single-server Kasm instance inside a Docker container. Because Kasm provisions and manages its own workspace containers, it runs a nested Docker daemon (Docker-in-Docker / DinD). Thus, the container must run with privileged execution rights.
1. Define the docker-compose.yml File
Create a directory named /opt/kasm-setup and place the following configuration into /opt/kasm-setup/docker-compose.yml:
version: '3.8'
services:
kasm:
image: lscr.io/linuxserver/kasm:latest
container_name: kasm
privileged: true
environment:
- KASM_PORT=443
# Optional: Prevent Docker Hub rate limiting by providing credentials
# - DOCKER_HUB_USERNAME=your_username
# - DOCKER_HUB_PASSWORD=your_password
volumes:
- /opt/kasm/data:/opt
- /opt/kasm/profiles:/profiles
- /dev/input:/dev/input
- /run/udev/data:/run/udev/data
ports:
# Bind setup wizard to localhost for security
- "127.0.0.1:3000:3000"
# Bind Kasm UI to localhost to run behind reverse proxy
- "127.0.0.1:8443:443"
restart: unless-stopped
2. Start the Service and Run the Setup Wizard
Launch the container in detached mode:
docker compose -f /opt/kasm-setup/docker-compose.yml up -d
Verify that the container is healthy and downloading initial packages:
docker logs -f kasm
The initial initialization writes the admin credentials to the container log output. Keep a record of these credentials.
Establish an SSH tunnel to your VPS to access the wizard securely on port 3000 without opening it to the public internet:
ssh -L 3000:127.0.0.1:3000 user@your-vps-ip
Open https://127.0.0.1:3000 in your web browser, bypass the self-signed certificate warning, and complete the Kasm configuration wizard.
Nginx Reverse Proxy Configuration
To expose Kasm Workspaces securely to the public internet using Let's Encrypt SSL/TLS, route all incoming traffic through a properly configured Nginx instance. Kasm relies heavily on WebSockets; the reverse proxy must pass the HTTP upgrade headers and handle persistent connections.
1. Configure the Nginx Server Block
Create a configuration file at /etc/nginx/sites-available/kasm.conf (replace kasm.example.com with your domain):
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
listen [::]:80;
server_name kasm.example.com;
# Redirect all HTTP requests to HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name kasm.example.com;
# SSL Certificate Paths
ssl_certificate /etc/letsencrypt/live/kasm.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/kasm.example.com/privkey.pem;
# HSTS & Security Headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Content-Security-Policy "frame-ancestors 'self';" always;
# Maximum file size for workspace file transfers
client_max_body_size 512M;
location / {
# Forward traffic to the local Kasm port
proxy_pass https://127.0.0.1:8443;
proxy_http_version 1.1;
# WebSockets support
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# Host and Client IP Headers
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Extended timeouts for persistent workspace sessions
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
}
2. Enable Configuration and Reload Nginx
Link the site configuration to sites-enabled and reload Nginx:
sudo ln -s /etc/nginx/sites-available/kasm.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Caddy Reverse Proxy Configuration (Alternative)
If using Caddy as your web server, automatic SSL generation is handled out of the box. Since Kasm's internal upstream server serves a self-signed certificate, Caddy must be instructed to bypass verification when proxying local traffic.
Add the following block to your Caddyfile:
kasm.example.com {
reverse_proxy https://127.0.0.1:8443 {
transport http {
tls_insecure_skip_verify
}
# Preserve client headers
header_up Host {host}
header_up X-Real-IP {remote}
header_up X-Forwarded-For {remote}
header_up X-Forwarded-Proto {scheme}
}
# Configure body size limits for uploads
request_body {
max_size 512MB
}
# Set headers
header {
Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
X-Frame-Options "SAMEORIGIN"
X-Content-Type-Options "nosniff"
Content-Security-Policy "frame-ancestors 'self';"
}
}
Reload Caddy to apply:
sudo systemctl reload caddy
CPU & RAM Limits for Virtual Desktops
If left unconfigured, a single Kasm workspace container can consume all CPU cycles and RAM on the host VPS, rendering the entire environment unresponsive. Resource allocation must be configured inside the Kasm Admin Control Panel.
1. Configure Limits per Image
- Log in to your Kasm Admin Panel (
https://kasm.example.com). - Navigate to Admin -> Images.
- Select an image (e.g., Chrome or Ubuntu Desktop) and click the Edit (pencil) icon.
- Expand the Docker Run Config Override (JSON format) and specify the hard constraints:
{
"memory": 3221225472,
"memory_swap": 4294967296,
"nano_cpus": 2000000000,
"cpu_shares": 1024
}
Configuration Breakdown:
memory:3221225472bytes limits the container to 3GB of RAM.memory_swap:4294967296bytes limits total RAM + Swap to 4GB. Once the container goes past 3GB, it writes excess memory pages to the swapfile.nano_cpus:2000000000nano-CPUs limits execution time to exactly 2.0 CPU cores.cpu_shares:1024acts as a relative weight indicator. If multiple containers compete for CPU, those with higher shares receive proportional scheduling priority.
2. Auto-Pruning and Inactivity Lifetimers
Idle workspace containers accumulate memory overhead over time. Configure auto-termination policies: 1. Navigate to Admin -> Groups -> Edit your target user group. 2. Under Settings, add the following configs: * keepalive_expiration: Set to 3600 (1 hour). If a user closes the tab, the container is destroyed after 1 hour. * session_lifetime: Set to 28800 (8 hours). Force-terminates a session after 8 hours of continuous use to prevent orphaned processes.
Security Hardening
To secure your self-hosted Kasm deployment:
- Change Default Passwords Immediately: Kasm generates random passwords on startup, but default accounts (like
admin@kasm.localanduser@kasm.local) should have their passwords rotated immediately in Admin -> Users. - Disable Registration: If public sign-ups are not required, go to Admin -> Settings and toggle
Disable SignupstoTrue. - Restrict Clipboard & Upload Paths: For high-security environments, edit your user group settings to disable bidirectional clipboards and restrict file uploads to prevent data exfiltration. Set
allow_clipboard_downandallow_clipboard_upbased on role requirements.