virtuOSo is a KVM-based homelab appliance. Launch and manage Linux virtual machines from your browser, automate infrastructure through a REST API and Terraform provider, and connect AI assistants like Claude Code or Codex via a built-in MCP server.
The built-in AI Assistant supports Claude Code and OpenAI Codex.
codex / claude
Hello! I'm your virtuOSo AI assistant.
I can help you manage VMs, generate Terraform configs, create Stack definitions, and more.
What would you like to do?
Claude CodeOpenAI Codex15 MCP tools
WireGuard VPN
Built-in WireGuard server for secure remote access to your VMs from anywhere. Manage peer configs directly from the web UI.
AI-Safe Guardrails
Credentials stay out of AI responses, delete protection is respected, and ambiguous destructive prompts are treated as conversation instead of bulk VM operations.
Delete Protection
Guard important VMs against accidental deletion. Protection must be explicitly disabled before a VM can be removed.
ISO Boot
Upload custom ISOs and boot VMs from them for manual OS installations. VNC is auto-enabled for installer interaction.
Get started
virtuOSo is distributed as a bootable ISO. Install it on dedicated hardware and you're managing VMs from your browser in minutes.
System Requirements: x86_64 CPU · UEFI boot · 16 GB+ RAM · SSD recommended
1
Download & Install
Download the virtuOSo ISO and install it on dedicated hardware.
2
Open the UI
Navigate to your server's IP in a browser. Log in with the admin credentials created during install.
3
Launch a VM
Click Launch, pick an OS and size, and your VM will be running in seconds. Use Quick Launch for one-click creation.
virtuOSo is a web-based KVM virtual machine manager for homelabs. It lets you create, manage, and access Ubuntu, Fedora, and Amazon Linux VMs from your browser using libvirt/QEMU.
Key features:
Launch VMs from cloud images or custom ISOs
Browser-based SSH shell, serial console, and VNC desktop access
Multi-VM stacks with YAML templates and dependency ordering
Built-in AI Assistant with Claude Code and OpenAI Codex support
NAT and bridged networking modes
REST API and Terraform provider for automation
Multi-user access control with per-VM permissions
AI Assistant
The /ai page provides a built-in chat interface backed by the virtuOSo MCP server. It supports both Claude Code and OpenAI Codex, with provider-specific login handled from the web UI.
Structured tools — the AI calls typed VM tools like list_vms, launch_vm, and change_vm_network
Built-in auth flows — Claude Code uses claude auth login; Codex uses codex login --device-auth
Guardrails — credentials are never exposed to the AI, bulk deletion is blocked, and delete protection is enforced
Dashboard
The dashboard (/vms) shows all your virtual machines with live status updates. Each VM row displays:
Name — click to open the VM detail page
State — running, shutoff, creating, or paused
IP address — appears once the VM is online
Resources — vCPUs, memory, disk usage
Cloud-init status — tracks first-boot provisioning progress
The dashboard auto-refreshes every few seconds via HTMX polling so you can watch VMs boot in real time.
Launching VMs
Navigate to Launch to create a new VM. Fill in the form:
Name — alphanumeric characters, hyphens, underscores, and dots (1-63 chars)
OS — choose from Ubuntu 25.10, Ubuntu 24.04 LTS, Ubuntu 22.04 LTS, Fedora 43, or Amazon Linux 2023
Size — micro (1 vCPU / 1 GB), small (1 / 2 GB), medium (2 / 4 GB), large (2 / 8 GB), or xlarge (4 / 16 GB)
Disk — override the default disk size in GB
SSH Key — select a key from your profile or paste one directly
User Script — shell commands to run on first boot via cloud-init
Desktop — installs a desktop environment and enables VNC
Bridged — places the VM on the LAN instead of NAT
Booting from ISO
To install an OS manually, select an ISO from the dropdown instead of a cloud image. ISOs can be uploaded via Settings (Storage tab). ISO-booted VMs automatically get VNC enabled so you can interact with the installer.
Tip: After launching, a password is generated and shown once. Copy it before navigating away. You can also view it later on the VM detail page.
VM Management
Click a VM name on the dashboard to open its detail page. Available actions:
Delete — permanently destroys the VM and its disk image
Resize Disk — grow the disk while the VM is shut off (disks can only be increased, not shrunk)
Autostart — toggle whether the VM starts automatically when the host boots
Delete Protection — prevent accidental deletion until protection is disabled
Change Network — switch between NAT and bridged networking (requires VM to be shut off)
The detail page also shows the VM password, SSH command, OS info, resource usage, and cloud-init status.
Quick Launch
The dashboard includes quick-launch buttons for common VM configurations. Click a quick-launch option to instantly create a VM with pre-configured settings (OS, size, and name are auto-generated). This is the fastest way to spin up a VM for testing.
Networking
VMs can use one of two network modes:
NAT (default) — VMs get an IP on the 192.168.122.x subnet via libvirt's default network. VMs can reach the internet but are not directly accessible from the LAN
Bridged — VMs get an IP directly from your router's DHCP, appearing as regular devices on your LAN. Requires a host bridge (br0) to be configured on the server
You can switch a VM's network mode from the VM detail page while it's shut off. The admin can configure the default network mode and bridge settings in Settings.
Shell & Console Access
virtuOSo provides three ways to access VMs from the browser:
SSH Shell — browser-based SSH terminal. Uses the VM's IP and default user (e.g., ubuntu for Ubuntu, fedora for Fedora, ec2-user for Amazon Linux). Requires the VM to be running with an IP assigned
Serial Console — direct serial console connection via libvirt. Works even when networking is down or the VM hasn't finished booting. Useful for debugging boot issues
VNC — graphical desktop access. Available when the VM was launched with VNC or desktop mode enabled. Required for ISO-booted VMs to interact with the OS installer
SSH Keys — upload public keys that can be injected into VMs at launch. Supports ssh-rsa, ssh-ed25519, and ecdsa-sha2 formats
API Keys — create bearer tokens for the REST API. Keys are shown once at creation and cannot be retrieved later
Secrets — encrypted key-value pairs that can be referenced in stack templates using ${secret.NAME} syntax. Values are encrypted at rest with AES-256-GCM
WireGuard — if enabled by the admin, manage VPN peer configurations for secure remote access to your VMs
Stacks let you define multi-VM deployments as a single YAML template and manage them as a unit. Instead of launching VMs one at a time, you describe all the VMs you need in a template, then deploy and tear them down together.
A stack has three components:
Name — a unique identifier for the stack (same rules as VM names)
Template — a YAML document defining the VMs
State — tracked automatically: undeployed, deployed, or partial
VM names are automatically prefixed with the stack name. For example, a stack called my-app with a VM named web creates a VM called my-app-web.
Template Format
Templates are YAML documents with a single top-level key vms containing a list of VM specifications:
VM name within the stack. Combined with the stack name to form the full VM name ({stack}-{name}). Must be unique within the template.
osrequired
string
Yes
—
OS image ID. Available options: ubuntu-24.04, ubuntu-22.04, ubuntu-25.10, fedora-43, amazon-2023. Append +desktop for a desktop environment (e.g. ubuntu-24.04+desktop).
Override disk size in GB. When set, overrides the default disk size for the chosen instance size.
user-scriptoptional
string
No
—
Shell commands to run on first boot (via cloud-init). Use YAML | for multi-line scripts. Also accepts user_script.
ssh-keyoptional
string
No
—
SSH public key to inject into the VM. Also accepts ssh_key.
vncoptional
bool
No
false
Enable VNC graphics. Automatically enabled when desktop is true.
desktopoptional
bool
No
false
Install a desktop environment. Implies vnc: true. Can also be set by appending +desktop to the os value.
depends_onoptional
list
No
[]
List of VM names (from this template) that must be launched and have an IP address before this VM starts. Used to ensure ${vm.X.ip} variables resolve to real IPs. Also accepts depends-on.
Template Variables
Use variables in user-script to reference other VMs in the same stack. Variables are resolved at deploy time.
Variable
Resolves To
When
${vm.SPECNAME.name}
Full prefixed VM name (e.g. mystack-database)
Deploy time (immediate)
${vm.SPECNAME.ip}
Actual IP address of the sibling VM (e.g. 192.168.122.237)
Deploy time (requires depends_on — the dependency is launched first and its IP is collected before this VM starts)
${secret.NAME}
Decrypted value of the secret named NAME
Deploy time (secret must exist in Profile > Secrets or as a per-stack secret)
SPECNAME is the name field of a VM in the same stack template (not the full prefixed name).
Example — web server connecting to database
vms:
- name: database
os: ubuntu-24.04
size: medium
user-script: |
apt-get update
apt-get install -y postgresql
- name: web
os: ubuntu-24.04
size: small
depends_on: [database]
user-script: |
apt-get update
apt-get install -y nginx
# database IP is substituted directly by the host before cloud-init
echo "DB_HOST=${vm.database.ip}" >> /etc/environment
echo "DB_VM=${vm.database.name}" >> /etc/environment
Note: To use ${vm.X.ip}, add depends_on: [X] to the VM that needs the IP. The host launches dependencies first, waits for them to get an IP (up to 2 minutes), then substitutes the actual IP address directly into the user-script before the dependent VM is created. No guest-side DNS resolution is needed. Name variables (${vm.X.name}) are replaced instantly and don't require depends_on.
Secrets
Secrets let you inject sensitive values (database passwords, API keys, tokens) into VM user-scripts without hardcoding them in the YAML template. Secret values are encrypted at rest using AES-256-GCM and only decrypted at deploy time.
Scopes
Global secrets — set in Profile > Secrets. Available to all your stacks.
Per-stack secrets — set on the stack detail page > Secrets tab. Override global secrets of the same name for that specific stack.
At deploy time, global secrets are loaded first, then per-stack secrets overlay them. If a per-stack secret has the same name as a global secret, the per-stack value wins.
Usage
Reference secrets in user-script using ${secret.NAME} syntax:
Retrieve the decrypted value of a per-stack secret. Response: {"value": "the-secret-value"}
PUT/api/v1/stacks/{name}/secrets/{id}API Key
Update the value of a per-stack secret. Same request body as global update.
DELETE/api/v1/stacks/{name}/secrets/{id}API Key
Delete a per-stack secret by ID.
Available OS Images
Use these values for the os field:
OS ID
Name
Default User
Desktop Available
ubuntu-25.10
Ubuntu 25.10
ubuntu
Yes — ubuntu-25.10+desktop
ubuntu-24.04
Ubuntu 24.04 LTS
ubuntu
Yes — ubuntu-24.04+desktop
ubuntu-22.04
Ubuntu 22.04 LTS
ubuntu
Yes — ubuntu-22.04+desktop
fedora-43
Fedora 43
fedora
Yes — fedora-43+desktop
amazon-2023
Amazon Linux 2023
ec2-user
No
Tip: The default user is the SSH login username for each OS. For Ubuntu VMs, SSH in with ssh ubuntu@<ip>. For Fedora, use fedora. For Amazon Linux, use ec2-user. A random password is also generated for each VM at deploy time.
Examples
Minimal — two VMs with defaults
vms:
- name: app
os: ubuntu-24.04
size: small
- name: db
os: ubuntu-24.04
size: medium
The stack name and template are saved to the database. No VMs are launched yet.
Deploy
VMs are launched in dependency order. VMs with no depends_on start first (in parallel), then VMs that depend on them, and so on. Each VM gets an auto-generated password. Non-admin users are automatically granted access to the VMs.
Update Template
The template YAML is updated in the database. Already-deployed VMs are not affected — changes take effect on the next deploy.
Tear Down
All VMs belonging to the stack are deleted (destroyed + disk removed). The stack itself remains so you can redeploy later.
Delete Stack
Tears down all VMs (if any) and then removes the stack from the database entirely.
Note: Deploying a stack does not skip VMs that already exist. If you need to redeploy, tear down first, then deploy again.
REST API
Stacks can be managed programmatically via the REST API. All endpoints require an API key (Authorization: Bearer vmk_...).
List Stacks
GET/api/v1/stacksAPI Key
Returns all stacks visible to the authenticated user (admins see all, users see their own).
Deletes all VMs belonging to the stack. The stack itself is preserved.
Response 200 OK: {"ok": true}
Delete Stack
DELETE/api/v1/stacks/{name}API Key
Tears down all VMs and removes the stack from the database.
Response 200 OK: {"ok": true}
Template Library
The template library lets you save stack templates for reuse. Saved templates persist independently of stacks, so you can delete a stack and still reuse its template later.
Saving a Template
On any stack detail page, use the Save as Template form below the YAML editor. Enter a name for the template and click save. The stack's current YAML is saved to your personal library.
Loading a Saved Template
When creating a new stack, a dropdown appears above the YAML editor if you have saved templates. Select a template and click Load to populate the editor with the saved YAML. You can edit it before creating the stack.
Deleting a Saved Template
On the new stack page, select a template from the dropdown and click the Delete button to remove it from your library.
Template Library API
GET/api/v1/stack-templatesAPI Key
Returns all saved templates belonging to the authenticated user.
Boot a VM from a custom ISO file. Upload ISOs via the Settings page (Storage tab), then reference the full path. VNC is automatically enabled for ISO boots.
Use bridged: true to place the VM on the LAN with a real IP from your router's DHCP, instead of the default NAT (192.168.122.x). Requires a host bridge (br0) to be configured on the server.
#!/usr/bin/env python3
"""virtuOSo API client example — launch a VM, wait for it, then SSH in."""
import requests, time
API = "https://your-server/api/v1"
KEY = "YOUR_API_KEY"
s = requests.Session()
s.headers["Authorization"] = f"Bearer {KEY}"
s.verify = False
# Launch a VM
resp = s.post(f"{API}/vms", json={
"name": "test-vm",
"size": "small",
"os": "ubuntu-24.04",
})
print(f"Launched: {resp.json()}")
# Poll until it's running and has an IP
for _ in range(60):
vm = s.get(f"{API}/vms/test-vm").json()
if vm.get("ip") and vm["state"] == "running" and vm.get("cloud_init") == "done":
print(f"Ready! IP: {vm['ip']}, password: {vm.get('password', 'N/A')}")
break
time.sleep(5)
else:
print("Timed out waiting for VM")
# Clean up
# s.delete(f"{API}/vms/test-vm")
Error Format
All errors return JSON with error and status fields:
{"error": "VM not found", "status": 404}
Status
Meaning
400
Bad request (invalid input)
401
Unauthorized (missing or invalid auth)
403
Forbidden (admin access required)
404
Not found
409
Conflict (duplicate name)
500
Internal server error
Quick Reference
Method
Endpoint
Auth
Description
POST
/api/v1/auth/apikeys
Session
Create API key
GET
/api/v1/auth/apikeys
Session
List API keys
DELETE
/api/v1/auth/apikeys/{id}
Session
Delete API key
GET
/api/v1/me
API Key
Current user info
GET
/api/v1/vms
API Key
List VMs
POST
/api/v1/vms
API Key
Launch VM
GET
/api/v1/vms/{name}
API Key
Get VM details
DELETE
/api/v1/vms/{name}
API Key
Delete VM
POST
/api/v1/vms/{name}/start
API Key
Start VM
POST
/api/v1/vms/{name}/stop
API Key
Graceful shutdown
POST
/api/v1/vms/{name}/kill
API Key
Force stop
POST
/api/v1/vms/{name}/restart
API Key
Reboot VM
POST
/api/v1/vms/{name}/resize
API Key
Resize disk
PUT
/api/v1/vms/{name}/autostart
API Key
Set autostart
PUT
/api/v1/vms/{name}/network
API Key
Change network mode
PUT
/api/v1/vms/{name}/protected
API Key
Set delete protection
GET
/api/v1/users
Admin
List users
POST
/api/v1/users
Admin
Create user
GET
/api/v1/users/{id}
Admin
Get user
DELETE
/api/v1/users/{id}
Admin
Delete user
PUT
/api/v1/users/{id}/password
Admin
Reset password
GET
/api/v1/vms/{name}/access
Admin
List VM access
POST
/api/v1/vms/{name}/access
Admin
Grant access
DELETE
/api/v1/vms/{name}/access/{user_id}
Admin
Revoke access
GET
/api/v1/sshkeys
API Key
List SSH keys
POST
/api/v1/sshkeys
API Key
Add SSH key
DELETE
/api/v1/sshkeys/{id}
API Key
Delete SSH key
API Keys
API key management uses session cookie auth (not Bearer tokens). Create keys from the Profile page or via these endpoints.
The network field is "default" for NAT or "bridge:br0" for bridged networking. The protected field indicates delete protection — protected VMs cannot be deleted until protection is disabled.
Launch VM
POST/api/v1/vmsAPI Key
Launches asynchronously. Returns immediately with status "creating".
Field
Type
Required
Default
Description
name
string
Yes
—
VM name (alphanumeric + hyphens)
size
string
No
small
micro, small, medium, large, xlarge
os
string
No*
—
ubuntu-24.04, ubuntu-22.04, ubuntu-25.10, fedora-43, amazon-2023 (append +desktop for GUI). Required unless iso is set
iso
string
No
—
Absolute path to an ISO file on the server. When set, the VM boots from the ISO instead of a cloud image (os is ignored, VNC is auto-enabled). ISOs uploaded via Settings are stored in /var/lib/vm-lab/vms/isos/
ssh_key
string
No
—
SSH public key to inject
disk_gb
int
No
20
Disk size in GB
password
string
No
auto
VM user password
vnc
bool
No
false
Enable VNC graphics
desktop
bool
No
false
Desktop environment (implies VNC)
user_script
string
No
—
Cloud-init script for first boot
bridged
bool
No
false
Use bridged networking instead of NAT. The VM gets an IP directly from the LAN router's DHCP. Requires a host bridge (br0) to be configured
All attributes can be set via environment variables as an alternative to hardcoding in config files.
Resource: virtuoso_vm
Manages a virtual machine. Most fields are immutable — changing them destroys and recreates the VM.
resource "virtuoso_vm" "web" {
name = "web-server"
size = "medium"
os = "ubuntu-24.04"
disk_gb = 40
ssh_key = virtuoso_ssh_key.deploy.public_key
started = true
autostart = true
}
Arguments
Attribute
Type
Description
namerequiredforce new
String
VM name (1-63 chars, alphanumeric/hyphens/underscores/dots)
osoptionalforce new
String
OS image: ubuntu-24.04 (default), ubuntu-22.04, ubuntu-25.10, fedora-43, amazon-2023. Ignored when iso is set
isooptionalforce new
String
Absolute path to an ISO file on the server. When set, the VM boots from the ISO instead of a cloud image (os is ignored, VNC is auto-enabled). Upload ISOs via Settings > Storage; they are stored at /var/lib/vm-lab/vms/isos/
sizeoptionalforce new
String
micro (1 vCPU/1 GB), small (1/2 GB), medium (2/4 GB), large (2/8 GB), xlarge (4/16 GB). Default: small
ssh_keyoptionalforce new
String
SSH public key to inject
disk_gboptional
Int
Disk size in GB. Can be increased in-place; cannot be shrunk
passwordoptionalsensitiveforce new
String
VM password. Auto-generated if omitted
vncoptionalforce new
Bool
Enable VNC graphics
desktopoptionalforce new
Bool
Install desktop environment (implies VNC)
user_scriptoptionalforce new
String
Shell commands to run on first boot via cloud-init
bridgedoptional
Bool
Use bridged networking (VM gets a LAN IP from router DHCP) instead of NAT. Default: false. Updateable in-place (stops VM, switches network, restarts automatically)
autostartoptional
Bool
Start VM automatically on host boot. Default: false. Updateable in-place
protectedoptional
Bool
Delete protection. When enabled, the VM cannot be deleted until protection is disabled. Default: false. Updateable in-place. Terraform auto-unprotects before destroy
startedoptional
Bool
Whether the VM should be running. Default: true. Updateable in-place
Read-Only Attributes
Attribute
Type
Description
ip
String
VM IP address
state
String
VM state: running, shutoff, creating
vcpus
Int
Number of virtual CPUs
memory_mb
Int
Memory in MB
disk_cap_gb
Float
Disk capacity in GB
disk_used_gb
Float
Disk used in GB
has_vnc
Bool
Whether VNC is available
cloud_init
String
Cloud-init status (running, done, error)
network
String
Network identifier: "default" for NAT or "bridge:br0" for bridged
os_id
String
OS identifier from server
passwordsensitive
String
VM password (captured at creation)
Update Behavior
In-place updates (no VM replacement):
autostart — toggles autostart on/off
started — starts or gracefully stops the VM
disk_gb — grows the disk (stops VM, resizes, restarts automatically)
bridged — switches between NAT and bridged networking (stops VM, switches, restarts automatically)
protected — enables or disables delete protection (Terraform auto-unprotects before terraform destroy)
Disk resize requires a VM restart. The provider handles the stop → resize → start sequence automatically. Disks can only be grown, not shrunk.
Import
terraform import virtuoso_vm.web web-server
Import by VM name. Config-only fields (size, os, etc.) will be empty after import — fill them in or use ignore_changes.
Resource: virtuoso_ssh_key
Manages an SSH public key associated with your account. Immutable — any change recreates the key.
virtuOSo includes a built-in Model Context Protocol server so AI assistants can manage VMs through typed JSON tools instead of parsing shell output. The MCP server runs as vm mcp-serve and exposes structured VM lifecycle, discovery, and configuration operations.
AI Assistant
The /ai page provides a built-in chat interface with provider tabs for Claude Code and OpenAI Codex. Authentication is handled from the web UI using each provider's CLI login flow.
Claude Code — signs in with claude auth login
Codex — signs in with codex login --device-auth
Status dots — green means authenticated, red means login required, gray means the CLI is not installed
External Setup
You can connect external MCP clients to the same server over stdio: