Herd your VMs β and containers β into your tailnet.
Your VMs have escaped. Quick ones sit on your laptop, and big ones sit on the Kubernetes cluster in the closet. There is an Incus server that's too useful to replace, and a libvirt host that answers over SSH. There is also a Proxmox node that you still mean to retire one day. Five sets of tooling, five networking stories, and none of it reachable from the couch.
Corral fixes that. One command works on every backend: local QEMU/KVM, KubeVirt, Incus, libvirt, Proxmox VE, or a federated Corral peer. And every VM lands inside the one network that all your devices already share β your Tailscale tailnet.
corral create web --kubevirt --container-disk quay.io/containerdisks/fedora:42
corral ssh web # from this machine, your laptop, or your phone's terminalVMs are cattle. Don't treat each one like a networking project.
β that's corral web --demo β try the whole dashboard yourself in 30 seconds, no cluster needed.
Your machine runs one task at a time using your signed-in AI coding CLI. The relay alternates between eligible School (OS) and Reef (apps) hives after each task. If one hive has no work, the other can continue.
The guide covers prerequisites, verified just setup, credentials, provider
costs, and how to stop with Ctrl+C. Your subscription quota or API charges apply.
Never post account credentials or API keys in issues or pull requests.
- Same commands everywhere.
create/start/ssh/viewer/clone/deletework the same on every backend context. That means local QEMU/KVM, KubeVirt on your cluster, Incus, libvirt, or a federated Corral peer. A peer tries advertised or direct guest endpoints first, and falls back to an HTTP/WebSocket relay. See the backend support matrix. Corral remembers which is which, so you never specify it again. - One fleet, not five tabs. Corral aggregates every configured context at
once.
corral list, the TUI and the dashboard show the whole fleet.corral context use NAMEonly chooses where new work lands. It never hides the rest, and it never mutates kubectl's or Incus's own global config. Authentication stays boring on purpose. It reuses an existing Incus remote's trust, aqemu+ssh://URI through your OpenSSH agent and config, or a PVE API token.corral doctorruns scoped checks against every target and--context NAMEnarrows it to one. - Move a VM to a different backend.
corral move <vm> --to <backend>exports the disk, converts it, and ingests it on the destination. Then it verifies the result. It's cold and never pretends otherwise β the guest stops, andcorral migrateremains the live, within-one-backend kind. Preflight refuses before it touches anything. It reports every reason at once: firmware, disk bus, free space, the new MAC/IP.--dry-runonly prints the plan. Corral leaves the source stopped, not deleted unless you pass--delete-source. Disk export works on every backend on its own too β qcow2, raw.gz, or an Incus tarball. See ADR-0010. - Your OS is a container image. Point Corral at a bootable container
(
corral create dev --bootc ghcr.io/...). Corral builds the OS disk on-cluster withbootc install to-disk, then boots it as a first-class VM.corral bootc upgraderolls the VM to the image's next build β so your fleet of VMs updates the same way containers do. No other VM platform has this. - A bootc image tester for CI.
corral vmtest --bootc ghcr.io/...builds the image into a disk and boots it. It waits for the guest, runs your assertions, then hands the VM over for your own tests. Test accounts, passwords, extra packages and a boot hook go into a layer above the published image. corral never changes that image. It writes the evidence as it goes: the serial console, screenshots of the boot, a timelapse, andresult.json. It measures whether the screen painted at all. A desktop that boots to nothing therefore fails. Each failure class gets its own exit code. A pipeline can then tell a runner with no KVM from an image that will not boot. See docs/ci-boot-gate.md. - Containers (CT) β distrobox on Kubernetes. Proxmox-style pet pods
alongside VMs (
corral ct create). A privileged CT seeds a full root filesystem onto its own volume, andchroots into it on boot. Soapt/dnf/apkinstalls and dotfiles survive Stop/Start. It works the same way in a real distrobox container when you stop it and enter it again. Unprivileged (default) CTs get only a simple mount of/datainstead. - SSH that works out of the box. Corral injects your public key at create
time. It also generates a fallback password and stores it locally.
corral sshpicks the right path: a Kubernetes API tunnel for cluster VMs, a Tailscale-bound port-forward for local ones. Or use--vsockfor QEMU AF_VSOCK transport on live ISOs. It changes no config files. - VMs that join the tailnet themselves. Drop a Tailscale auth key in
~/.config/tailvm/config.yaml(orTS_AUTHKEY). Then every cloud-init VM runstailscale upon first boot. It shows up as a real machine on your tailnet, MagicDNS name and all. - Extensions with a marketplace. Niche features ship as plugins:
corral plugin search, thencorral plugin install bootc. Thencorral bootc create dev --image ghcr.io/...builds an OS disk on the cluster from a bootable container image. It boots that disk as a VM. Browse/install from the web UI's Extensions tab too. The core binary stays lean. - Point-and-shoot TUI. Run
corralbare for a Bubble Tea interface. Pick a VM and hit Start / Stop / SSH / VNC / Delete. Browse and restore snapshots on any backend that has them, or read a VM's events. Mark a template, or resize CPU/RAM (VMs and CTs). Toggle which ports (SSH, VNC, RDP, HTTP, β¦) Corral publishes to the tailnet as<name>-vm.your-tailnet.ts.net. - A Proxmox-style web UI.
corral webserves a dark, mobile-friendly dashboard. It has a datacenter β node β VM tree, live status, a create wizard, and start/stop/restart/pause. It offers one-click live migration with a target-node picker, and multi-select bulk start/stop. It shows tags (chips + tree filter) and a per-VM CPU usage sparkline. It does disk export (qcow2 or raw.gz). You can add your own image/ISO sources alongside the built-in catalog, and Corral saves them in a ConfigMap. And it gives you real consoles in the browser: noVNC graphics and an xterm.js serial TTY. It also runs on the cluster itself (deploy/corral-web.yaml), and the Tailscale operator exposes it to your tailnet. CLI, TUI, and web all share the same state. - One static binary, written in Go. No daemons, no controllers to install, no
client-side K8s SDK. It drives
kubectl/virtctl/systemctlβ the tools you already trust β and gets out of the way.
One line does it all. The script detects OS/arch and verifies the download
against the release's SHA256SUMS. It installs the newest binary, rebuilt on
every push, to ~/.local/bin, and wires up shell completions (bash/zsh/fish).
It works on Linux and macOS, amd64 and arm64:
curl -fsSL https://raw.githubusercontent.com/tuna-os/corral/main/scripts/install.sh | shCI signs each binary and the container image with Sigstore, and attests an
SPDX SBOM for each. It uses no stored key. When cosign is on your PATH,
the script also verifies the signature. Set CORRAL_REQUIRE_SIGNATURE=1 to
stop the install when cosign is not available. To verify by hand, use the
exact identity of the workflow that signed the file:
iss=https://token.actions.githubusercontent.com
# Rolling binary (from main)
cosign verify-blob --bundle corral-linux-amd64.sigstore.json \
--certificate-identity https://github.com/tuna-os/corral/.github/workflows/ci.yml@refs/heads/main \
--certificate-oidc-issuer "$iss" corral-linux-amd64
# Binary from the release of tag v0.7.0
cosign verify-blob --bundle corral-linux-amd64.sigstore.json \
--certificate-identity https://github.com/tuna-os/corral/.github/workflows/release.yml@refs/tags/v0.7.0 \
--certificate-oidc-issuer "$iss" corral-linux-amd64
# Container image and its SBOM attestations
cosign verify ghcr.io/tuna-os/corral:latest \
--certificate-identity https://github.com/tuna-os/corral/.github/workflows/ci.yml@refs/heads/main \
--certificate-oidc-issuer "$iss"
cosign verify-attestation --type spdxjson ghcr.io/tuna-os/corral:latest \
--certificate-identity https://github.com/tuna-os/corral/.github/workflows/ci.yml@refs/heads/main \
--certificate-oidc-issuer "$iss"β¦or via brew (Linux & macOS)
Tagged releases land in the tuna-os tap,
so brew upgrade keeps you current. The formula is corral-vm (homebrew/core
already has an unrelated corral), but it still installs the corral command:
brew install tuna-os/tap/corral-vmβ¦or grab the binary yourself
This release rolls forward: CI rebuilds it from main on every push. It is
not a CI artifact, so there is no GitHub login or expiry:
curl -fsSL -o corral \
"https://github.com/tuna-os/corral/releases/download/binaries/corral-linux-$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')"
chmod +x corral
install corral ~/.local/bin/β¦or via go install
go install github.com/tuna-os/corral@latestβ¦or build from source
git clone https://github.com/tuna-os/corral
cd corral
go build -o corral .
install corral ~/.local/bin/β¦or pull the container image
The same image that runs corral web in-cluster also ships the CLI binary.
The image is available for both linux/amd64 and linux/arm64.
podman create --name corral-extract ghcr.io/tuna-os/corral:latest
podman cp corral-extract:/usr/local/bin/corral .
podman rm corral-extract
install corral ~/.local/bin/Optional: corral completion fish | source (bash/zsh/fish, via Cobra).
Run the dashboard as a service. One command installs a systemd unit so
corral web starts at boot and restarts on failure:
# Per-user unit (no root).
corral web service install
# Machine-wide unit; run under sudo.
corral web service install --system --addr "$(tailscale ip -4):8006"
# Show the unit status. The other subcommands are uninstall and print.
corral web service statusFor a per-user unit to run after you log out, enable linger:
sudo loginctl enable-linger "$USER".
Development tasks run through just, and a
bare just lists them. They include build, test, vet, ci (the
pre-push gate), and regen-catalog. It checks bootc images on public GHCR
and candidate installer ISOs. New entries and aliases need an upstream rebuild
within 60 days. The script keeps stale or unavailable entries for manual review.
Each week, the catalog-refresh workflow opens a PR and dispatches CI. Repository Actions settings must allow GitHub Actions to create pull requests. Renovate also tracks versions of the selected containerdisks.
--demo runs everything against a built-in fake cluster (a varied VM fleet,
containers, nodes, live metrics β start/stop/create/delete all work):
corral --demo # the TUI, populated
corral web --demo # the Proxmox-style dashboard at http://127.0.0.1:8006
corral list --demo # any CLI command works tooYou can also run the dashboard in a browser with no install. Use just web-demo
to build a static demo site. See web-demo/README.md.
# Local VM on this machine (QEMU/KVM, runs as a systemd user service)
corral create scratch --iso ~/Downloads/ubuntu-24.04.iso
corral start scratch # VNC + SSH bound to this host's Tailscale IP
# Cluster VM (KubeVirt) from a container disk
corral create web --kubevirt --container-disk quay.io/containerdisks/ubuntu:24.04
corral start web
corral ssh web
# Cluster VM from an installer ISO (CDI imports it for you, progress in `corral list`)
corral create bluefin --kubevirt --iso https://download.example/bluefin.iso
# Bootable container β running VM, disk built on-cluster (bootc extension)
corral plugin install bootc
corral bootc create dev --image quay.io/centos-bootc/centos-bootc:stream9
corral start dev && corral ssh dev -u root
# Everything, both backends, one table
corral list
# Container (CT) β distrobox-style persistent rootfs
corral ct create devbox --image docker.io/library/debian:bookworm --privileged
corral ct console devbox
# Container (CT) from a project's devcontainer.json
corral ct create myproj --devcontainer ./myproj| Local (qemu) | Cluster (kubevirt) | |
|---|---|---|
| SSH | host's Tailscale IP, forwarded port | virtctl ssh API tunnel β works with zero exposure |
| VNC | host's Tailscale IP, vnc://β¦ |
virtctl vnc proxy |
| Published ports | β (host is already on the tailnet) | per-VM proxy Service tagged tailscale.com/expose β <name>-vm.<tailnet>.ts.net |
| VM on the tailnet itself | β | automatic via cloud-init when an auth key is configured |
Corral never binds anything to 0.0.0.0. Ports of local VMs attach only to
the host's Tailscale IP.
Corral has a krew-style plugin system. Plugins are standalone corral-<name>
binaries in ~/.local/share/corral/plugins; once installed they run as
corral <name> β¦. A curated marketplace (marketplace/index.json) lists
installable ones:
corral plugin search
corral plugin install bootc
corral plugin listThe web UI has an Extensions tab to browse and install the same plugins.
The flagship extension, corral bootc, turns a bootable container image into a
VM that runs, without any local tooling:
- Corral provisions a block-mode PVC. Then it runs a short-lived builder
VM (not a pod) that runs
bootc install to-diskonto it. So the VM's own kernel does the filesystem work. That lets it install images that the node kernel can't handle. An example is Universal Blue desktops (bluefin/dakota) that need btrfs + composefs. Corral auto-detects the right backend (ostree vs composefs) and filesystem from the image. It bakes in your SSH key and enables sshd. - Build logs stream to your terminal live.
- The finished disk is self-bootable (GPT + ESP + bootloader), so the final VM UEFI-boots it β no kernelBoot, no bootloader gymnastics.
corral bootc rebuild|upgrade|switch re-bakes the disk from a new image
(Corral applies the SSH key again across the --wipe). Rebuild your OS in CI,
corral create it as a VM in minutes.
To make the greeter usable on a desktop image, create a login account before the first boot:
corral bootc create dev --image ghcr.io/tuna-os/marlin:tuna \
--user tuna --password-prompt
# Scripts give the password as the first line of stdin:
printf '%s\n' "$PASSWORD" | corral bootc create dev \
--image ghcr.io/tuna-os/marlin:tuna --user tuna --password-stdin
# Or give a SHA-512 crypt hash:
corral bootc create dev --image ghcr.io/tuna-os/marlin:tuna \
--user tuna --password-hash '$6$rounds=656000$<salt>$<hash>'Corral hides the password prompt and rejects plaintext --password. The
account goes into the installed deployment's /etc, with its home in
/var/home, so it is there at the first boot. The --user,
--password-prompt, --password-stdin, and --password-hash flags are also
available on bootc rebuild, bootc upgrade, and bootc switch. A resumed
build reuses an already-installed disk and cannot add an account.
Faster builds: deploy deploy/registry-cache.yaml (an on-cluster
pull-through cache for ghcr.io). The builder then pulls images through it
automatically, with no config. To turn it off, set
CORRAL_REGISTRY_MIRROR=off.
corral backup ships VM disk backups to any S3/R2 bucket via rclone β
on-demand or scheduled entirely in-cluster (no workstation required):
corral backup create web --dest r2:backups/corral # export + upload
corral backup restore web-restored --src r2:backups/corral/web-β¦.img.gz --size 20Gi
corral backup list --dest r2:backups/corral
corral backup schedule web --every 24h --keep 7 --to r2:backups/corral # in-cluster CronJob
corral backup schedules # list schedules
corral backup unschedule webNeeds rclone configured for your remote (rclone config) and virtctl.
Scheduled backups run as non-root in the pinned rclone/rclone image.
An init container copies kubectl and downloads virtctl into a
shared volume, so no corral-owned image is necessary. Your local rclone
config goes into a namespaced Secret that the CronJob mounts.
corral windows sets up UEFI/TPM/virtio for a first-class Windows guest.
KubeVirt VMs default to a Linux-tuned devices set, and Windows Setup can't
boot from it without extra help:
corral plugin install windows
corral windows create win11 --iso https://example/Win11.iso --cpu 4 --mem 8GiThe plugin imports the installer ISO via CDI. It provisions a UEFI+TPM+q35 VM with Hyper-V enlightenments. It attaches the virtio-win driver ISO as a second CD-ROM, so Setup can see the virtio disk/network. It also sets up proper access to the console.
corral vdi β desktop pools. Phase 1 of RFC-0001:
clone an already-built VM into a pool, hand members to users, connect,
release, delete. Full setup guide: docs/vdi.md.
corral plugin install vdi
corral vdi pool create devpool --from golden-desktop --size 3
corral vdi assign devpool alice
corral vdi connect devpool-1
corral vdi unassign devpool-1There is no broker, no self-serve web page, and no idle reclaim yet. See the RFC and issue #69 for what's next. Pool membership and assignment are plain K8s labels on the VM objects, nothing more.
corral vdi ct-pool keeps a declared pool of CTs: ephemeral Linux desktops
built from a container image. When a user releases a member, Corral makes it
again from the image. See docs/vdi-ct-pools.md.
corral vdi ct-pool apply desk --image ghcr.io/example/desktop:1 --size 3
corral vdi ct-pool claim desk alice
corral vdi ct-pool release desk desk-1 alicecorral create --bootc runs entirely on local QEMU β no cluster, no
tailnet β which makes it a one-command boot gate for bootc images on any
KVM-capable runner. --wait-ssh turns the exit code into the verdict:
corral create gate --bootc ghcr.io/tuna-os/yellowfin:gnome-testing \
--wait-ssh --timeout 900
# exit 0 β image booted and answers SSH (key injected for root at install)
# exit β 0 β it didn't; fail the pipeline
corral delete gateThe same thing, declaratively (corral reads Lima-style YAML natively):
# verify.yaml
bootc: ghcr.io/tuna-os/yellowfin:gnome-testing
cpus: 4
memory: 4GiB
disk: 30GiB
provision:
- mode: system
script: |
#!/bin/sh
systemctl enable sshdcorral create gate -f verify.yaml --wait-ssh --timeout 900Corral chroots provision scripts into the installed disk before first
boot. So they can enable services, drop test users, or plant readiness
markers. The published image stays untouched. Corral installs the disk with
bootc install to-disk --generic-image, so it boots under plain
SeaBIOS/OVMF anywhere.
On GitHub-hosted runners, enable KVM first:
- run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \
| sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules && sudo udevadm trigger --name-match=kvmScratch/build VMs are easy to create and easy to forget: a --bootc
builder, a one-off boot-gate test, a CI throwaway. They don't clean
themselves up if you Ctrl+C out or walk away. --ephemeral marks a VM for
corral gc, so you don't have to remember it:
corral create scratch --kubevirt --image bluefin --ephemeral --ttl 2h
# ... time passes, nobody comes back for it ...
corral gc # stops it β PVCs and disk state survive
corral gc --dry-run # preview without touching anythingTwo stages, on purpose:
- TTL expires β stopped. This frees the scarce resource (cluster
CPU/RAM) at once. The disk stays untouched. If you did need the VM after
all,
corral start scratchbrings it right back. - Stopped by gc, past the grace period (default 72h,
--delete-afterto change it) β deleted. VM and PVCs, for real. Only VMs that gc itself stopped are eligible. If you stop one yourself, this clock doesn't start. So gc never sweeps up an intentionally-parked VM by surprise.
Run corral gc by hand, or point a CronJob at it for hands-off cleanup. gc
never touches a VM without --ephemeral.
corral ct create --devcontainer <path> reads a project's
.devcontainer/devcontainer.json and provisions a Container (CT) from it. It
reads image, postCreateCommand, remoteUser, and forwardPorts. If the
file uses build.dockerfile in place of image, you get an error that points
you at --image:
corral ct create myproj --devcontainer ./myproj
corral ct console myproj<path> is the devcontainer.json itself, or a directory that contains
.devcontainer/devcontainer.json/.devcontainer.json. The CT runs
privileged (persistent rootfs) by default, because that's the whole point of
a dev container. Pass --privileged=false to turn that off. Any of
--image, --cpu, --mem, etc. still override what the json would
otherwise set.
This is a scoped MVP, not full devcontainer-spec/VS Code support. Corral
does not support Features, build.dockerfile, or postCreateCommand's
object form (several named commands that run in parallel). The issue that
tracks this work has the fuller story. For example, VS Code's "Reopen in
Container" could recognize Corral natively.
# ~/.config/tailvm/config.yaml
tailscale:
auth_key: tskey-... # or export TS_AUTHKEY; flag --ts-authkey overridescorral config shows what's active. State lives in
~/.local/share/tailvm/ (registry + local VM disks). The legacy tailvm
tool shares this directory, so existing VMs still work.
Environment overrides (handy for the in-cluster web deployment):
| Variable | Effect |
|---|---|
CORRAL_NAMESPACE |
default VM namespace (code default: corral-vms) |
CORRAL_ADMINS |
comma-separated tailnet logins allowed to mutate; unset = single-user/open (see Web UI) |
CORRAL_REGISTRY_MIRROR |
override/disable (off) the bootc builder's pull-through cache host |
CORRAL_BOOTC_BUILD_TIMEOUT |
minutes to wait for a bootc builder VM (default 45) |
corral web # http://127.0.0.1:8006
corral web --addr "$(tailscale ip -4):8006" # share with your tailnetOr serve it from the cluster (public image built by CI to ghcr.io/tuna-os/corral):
kubectl apply -f deploy/corral-web.yaml
# β https://corral.<tailnet>.ts.netSetting up from scratch? Build your own KubeVirt "Proxmox" walks through the whole stack β KubeVirt + CDI, the feature gates, Longhorn + snapshots, Multus, and Corral itself.
Tailnet membership is the authentication β never bind a public interface.
For authorization, set CORRAL_ADMINS to a comma-separated list of tailnet
logins. Listed users can mutate; everyone else gets a read-only UI, and the
API rejects any call from them that mutates state (403). Unset =
single-user/open (the default). Identity comes from the Tailscale ingress headers β see
ADR-0003. Feature roadmap:
SPEC.md and docs/api.md.
corral TUI (VMs and Containers side by side)
corral web Proxmox-style web UI [--addr host:port]
corral doctor cluster health checks, --fix for safe auto-fixes
corral list all VMs, every configured context
corral context get | use <name> | add <name> --backend <bck> | list | rm <name>
corral create <name> --kubevirt | (default: local qemu)
--mem 4G --cpu 2 --disk 20G --iso β¦ --container-disk β¦
--pvc β¦ --node β¦ --cloud-init β¦ --instancetype β¦ --ts-authkey β¦
--storage-class β¦ --ephemeral --ttl 4h
--lan | --network-nad ns/name [--bridge-iface net1] (LAN bridge NIC)
corral gc [kubevirt] [--dry-run] [--delete-after 72h]
stop --ephemeral VMs past their --ttl (PVCs kept),
delete them once stopped past --delete-after
--builders [--builder-age 2h]: also purge interrupted
bootc builds (builder VM with no final VM)
corral clone <src> <dst> [kubevirt] clone a VM's disk + config to a new name
corral plugin search | install <name> | list | remove <name> (extensions)
corral start|stop <name>
corral restart <name> restart a VM
corral pause|unpause [kubevirt] freeze / resume a running VM
corral scale <name> [kubevirt] --cpu N --mem 8G (live hotplug when possible)
corral migrate <name> [kubevirt] --node X live-migrate to another node
corral move <name> --to <backend> cold cross-backend move (export β
ingest β verify); --dry-run, --delete-source
corral adddisk <name> [kubevirt] --size 10Gi hotplug a new disk
corral rmdisk <name> [kubevirt] --volume PVC detach a hotplugged disk
corral snapshot β¦ [kubevirt] create | ls | restore | rm
corral networks [kubevirt] list Multus NetworkAttachmentDefinitions
corral addnic <name> [kubevirt] --network-nad ns/name --iface net1
bridge a LAN NIC onto an existing VM
corral ssh <name> [-u user] [-i key] [-c cmd] [-p port] [--password β¦]
[-L [bind:]port:host:hostport ...] local port forward(s)
corral viewer <name> VNC via xdg-open
corral key <name> <key>... [qemu|kubevirt] press a key combination (ret, ctrl alt f2)
corral type <name> [text] [qemu|kubevirt] [--file PATH|-] [--powershell] [--enter]
type UTF-8 input; --powershell sends an encoded launcher
corral pointer <name> <x> <y> [kubevirt] [--click left|middle|right] [--double]
corral logs <name> journald (local) / virt-launcher (cluster)
corral info <name> raw JSON
corral delete <name>β¦ [-f] [--wait [--timeout 5m]] [--json] removes each VM
and every object corral made for it (disks, Secrets,
proxy, snapshots, leftover bootc builder), registry entry
corral ct create <name> --image β¦ [--cpu 1] [--mem 512Mi] [--disk 5Gi]
[--privileged] (distrobox-style persistent rootfs)
--devcontainer <path> (scoped MVP β see below)
corral ct list|start|stop|delete|console <name>
corral vdi pool create <name> --from <golden-vm> --size N (plugin, desktop pools)
corral vdi pool list|delete <name>
corral vdi assign <pool> <user>
corral vdi unassign <member>
corral vdi connect <member>
corral type needs one source: positional text, --file PATH, or --file -
for stdin. Ordinary input must map to a US keyboard.
With --powershell, Corral accepts Unicode in PowerShell source, with an
optional UTF-8 BOM at the start. It types
powershell.exe -NoLogo -NoProfile -EncodedCommand with UTF-16LE Base64.
For example: corral type winvm --file script.ps1 --powershell --enter.
Corral refuses empty or BOM-only PowerShell source.
The launcher has a limit of 8,191 characters for Windows cmd.exe compatibility.
Manually focus the guest's command field or shell first.
Corral does not open Run, change focus, elevate through UAC, or handle credentials.
--enter explicitly appends Return; ordinary input's own newlines still press Return.
Success means Corral sent the input, not that a script ran.
Check an observable guest result, such as a file the script creates.
Base64 is reversible, not encryption or secret storage.
Run or shell history can retain the source.
Normal output stays silent.
With --json, Corral reports only the VM, action, source character count, and whether the user chose --enter.
This count excludes a stripped BOM or appended Return.
Corral never prints the source or encoded payload.
Corral exposes the Proxmox-style operations above through both the CLI/TUI and the web UI (editable Hardware tab, Snapshots tab, in-browser consoles). What works depends on the cluster:
- Change CPU / RAM β always works. On a VM that is truly
live-migratable, Corral hotplugs the change with no downtime. Otherwise
Corral applies it in a single stopβpatchβstart. Corral creates new VMs
sockets-based, with
maxSockets/maxGuestheadroom, so they can hotplug. - Live migration / live hotplug β needs
vmRolloutStrategy: LiveUpdateand masquerade networking (Corral sets this). It also needs migratable storage (RWX), and a target node with the same CPU vendor. You cannot live-migrate a VM between an Intel host and an AMD host while it runs. On a mixed-vendor cluster, Corral detects this and falls back to the offline path, so it does not hang. - Add disk (hotplug) β needs the
HotplugVolumesfeature gate. - Online disk expansion β needs a StorageClass with
allowVolumeExpansion: true. - Snapshots / clone / restore β need a
VolumeSnapshotClassfor VMs with persistent disks (ephemeral container-disk VMs can snapshot their definition without one). The web UI greys out controls the cluster can't support.
Full design document: SPEC.md.
CONTRIBUTING.mdβ how to build and test (just ci, the-tags bootcset), code style, and how to submit a changeSPEC.mdβ full specification (commands, flags, types, backends, registry)docs/user-guide.mdβ first commands for each backend, and common workflows: create, connect, clone, move, snapshots, pluginsdocs/troubleshooting.mdβ what eachcorral doctorcheck means, error messages with their fixes, and how to collect evidencedocs/api.mdβ the complete reference for the REST APIdocs/architecture.mdβ package map, design decisions, data flow, build systemdocs/backend-support.mdβ what each backend context can do (local QEMU, KubeVirt, Incus, libvirt, Proxmox VE, Corral peer)docs/backend-parity.mdβ per-operation parity matrix, generated frompkg/backend.Matrixand enforced by conformance testsdocs/ci-boot-gate.mdβ how to gate CI publishes on bootc images that boot (QEMU + KubeVirt), with field-tested fixes for failuresdocs/kubevirt-proxmox-setup.mdβ from-scratch KubeVirt + Longhorn + Corral setup guidedocs/incus-backend-setup.mdβ Incus remotes, trust, contexts, and checksdocs/libvirt-backend-setup.mdβ libvirt URIs (local andqemu+ssh://), permissions, contexts, and checksdocs/testing.mdβ test strategy & plan (unit, integration, E2E)docs/vdi.mdβ setup guide for the VDI plugin (desktop pools)docs/vdi-epic-status.mdβ status of the VDI epic: dependency chain & hardware gates (#69)docs/rfc/0001-vdi-plugin.mdβ VDI plugin design + phased roadmap
- Local backend:
qemu-system-x86_64+ KVM, systemd user session - Cluster backend:
kubectlcontext with KubeVirt (+ CDI for ISO import),virtctl; Tailscale operator for published ports tailscaleon the host;sshpassonly if you use password SSH
A corral is one fence that holds the whole herd. Your VMs are the cattle;
your tailnet is the fence. (Formerly known as tailvm.)



