Qcontroller
#QEMU VM Controller
QEMU VM Controller (or qcontroller) is a flexible, API-driven tool for managing QEMU-based virtual machine instances on Linux and macOS. It is designed for users who need precise control over VM networking, image management, and orchestration—whether for local development, testing, or reproducible infrastructure setups.
qcontroller provides a unified interface for VM operations:
- Create – Create and optionally start a new VM from a known image, on any node.
- Start – Resume a stopped VM (async — returns immediately, transitions via events).
- Stop – Gracefully or forcefully stop a running VM.
- Remove – Delete a VM and clean up its resources.
- Info – Query the status, configuration, and runtime info of VMs.
- ListNodes – List all configured nodes in the cluster.
Operations are defined using Protocol Buffers and exposed via both gRPC and a RESTful HTTP gateway, making integration with scripts, dashboards, or automation frameworks straightforward.
The architecture separates node-level services from multi-node coordination. Each node runs qemu, fileregistry, eventservice, and controller — all communicating via gRPC on localhost. The orchestrator sits on top, providing a unified REST API, WebSocket event streaming, image distribution, and the web UI. This is the same architecture regardless of the number of nodes — even a single-node setup runs through the orchestrator.
#✨ Highlights
- 🛠 Single static binary: All logic is bundled into one Go binary with multiple subcommands.
- 🖥 Cross-platform support: Works on Linux and macOS (Intel tested; Apple Silicon supported via QEMU).
- 🌐 Multi-node support: Manage VMs across multiple physical nodes from a single control plane.
- 🎯 Modern web UI: Full-featured React-based interface available at qcontroller-ui.
- 🧠 Declarative VM descriptions: Define VM specs via JSON configs matching Protobuf definitions.
- 📡 gRPC + REST API: Communicate via a structured protocol or plain HTTP—your choice.
- Real-time event streaming: Live VM state changes via WebSocket at
/ws, aggregated from all nodes by the orchestrator. - Automatic image distribution: Orchestrator pushes images to remote nodes before VM creation.
- 📜 Auto-generated OpenAPI schema: Serves OpenAPI specs.
- 🔒 Optional mTLS and HTTPS: gRPC services can run with mutual TLS, and the orchestrator can serve HTTPS — all opt-in via config.
- 🧩 Easily extendable: Add support for snapshots, cloning, or additional QEMU flags with minimal effort.
#🚀 Getting Started
#macOS Package Installation
For macOS users, we provide a convenient installer package that handles service setup automatically:
# Build the macOS package ./build-macos-pkg.sh # Install the package (creates system services) sudo installer -pkg build/qcontrollerd.pkg -target /
This will:
- Install
qcontrollerdto/usr/local/bin/ - Create LaunchDaemon (system service) for QEMU
- Create LaunchAgent (user services) for controller and orchestrator
- Auto-start all services after installation
To uninstall:
sudo /usr/local/share/com.github.qcontroller.qcontrollerd/uninstall.sh
#Debian/Ubuntu Package Installation
For Debian/Ubuntu users, a .deb package can be built and installed:
# Build the binary and package it. Set GOARCH for cross-arch. make install-tools # one-time setup make deb PKG_VERSION=0.0.1 # native arch GOARCH=arm64 make deb PKG_VERSION=0.0.1 # cross-compile # apt drops privileges to the unprivileged _apt user when reading the # .deb (sandboxing), which can't read files in your source tree. Move # the package to a world-readable location like /tmp first. cp build/qcontrollerd_0.0.1_amd64.deb /tmp/ # Install (this also creates system users and starts all services) sudo apt install /tmp/qcontrollerd_0.0.1_amd64.deb
The package installs:
- Binary at
/usr/bin/qcontrollerd - Service configs at
/etc/qcontrollerd/<service>/config.json(preserved on upgrade as Debian conffiles) - systemd units at
/lib/systemd/system/qcontrollerd-<service>.service - Per-service data under
/var/lib/qcontrollerd/<service>/ - Logs via journald (
journalctl -u qcontrollerd-<service>)
A single dedicated qcontroller system user is created and used for the controller, fileregistry, eventservice and orchestrator services. The qemu service runs as root because it creates network namespaces and bridges.
To remove (keeps configs):
sudo apt remove qcontrollerd
To purge (removes configs, data, users):
sudo apt purge qcontrollerd
#Manual Build Instructions
To build the binary manually, run:
make install-tools make
#Subcommands
The compiled binary provides the following subcommands:
qemu– Manages VM process execution. Requires root for networking (TAP on Linux, vmnet on macOS).controller– Manages VM lifecycle on the local node. Polls QemuService for state changes and publishes them to the event service via gRPC.eventservice– Standalone pub/sub hub for VM and image events. Controllers and file registries publish events to it; the orchestrator subscribes.orchestrator– Coordinates multiple nodes. Subscribes to each node's event service, aggregates state, distributes images, and serves the REST API, WebSocket event stream, and web frontend via gRPC-gateway.fileregistry– Manages VM image storage. Provides chunked upload/download via gRPC. Runs on each node and on the orchestrator.
Separation of Controller and QEMU: The qemu service requires elevated privileges for networking (TAP/vmnet). To avoid granting root to the entire application, it runs as a separate process. The controller and other services run as non-root users.
Architecture: Each node runs
qemu,fileregistry,eventservice, andcontroller— all communicating via gRPC on localhost. The orchestrator connects to each node's controller, file registry, and event service via gRPC. Images are uploaded to the orchestrator and automatically pushed to nodes on demand. The same setup works for one node or many.
#Running the App
#Packaged Installation (macOS)
If you installed via the macOS package, services are automatically started and managed by launchd. Access the API at:
- Web UI:
http://localhost:8080/ui/ - OpenAPI specs:
http://localhost:8080/openapi.yaml
#Manual Execution
Each subcommand expects a JSON configuration file matching its Protobuf definitions.
A startup script is provided for running all services together during development:
./start.sh --rundir /tmp/qcontroller --bin ./build/qcontrollerd
Run ./start.sh --help for full usage details (interface, CIDR, DHCP range, macOS mode, certs).
To add remote nodes, edit the nodes array in the orchestrator config. Each node entry needs a name, endpoint (the node's controller gRPC address), fileRegistryEndpoint, and eventsEndpoint (the node's event service gRPC address).
For multi-node setups with overlay networking, see the helper scripts:
setup-nebula.sh— generates Nebula CA, certificates, and configs for two nodessetup-overlay.sh— adds/removes nft forwarding rules between the QEMU bridge and the overlay interface
Default service ports:
- orchestrator:
http://localhost:8080(HTTP) - eventservice:
localhost:8011(gRPC) - controller:
localhost:8009(gRPC) - qemu:
localhost:8008(gRPC) - fileregistry:
localhost:8010(gRPC)
Then access the interfaces:
- Web UI:
http://localhost:8080/ui/ - OpenAPI Specs:
http://localhost:8080/openapi.yaml
#🔒 TLS
Every gRPC service and the orchestrator's HTTP server accept an optional tls block pointing at a CA certificate, server certificate, and private key. When present, gRPC servers enforce mTLS (TLS 1.3, client cert verified against the CA) and the orchestrator serves HTTPS. When absent, services run plaintext — useful for inside a trusted cluster where a service mesh or ingress already handles encryption.
Example tls block:
"tls": { "ca": "/path/to/ca.pem", "cert": "/path/to/service.pem", "key": "/path/to/service-key.pem" }
Client-side TLS is configured per outbound connection. For example, a controller config dialing qemu and the event service:
"qemuTls": { "ca": "...", "cert": "...", "key": "..." }, "eventsTls": { "ca": "...", "cert": "...", "key": "..." }
Node entries in the orchestrator config follow the same pattern with controllerTls, fileRegistryTls, and eventsTls fields.
#Dev setup with --certs
For local development, start.sh can generate a self-signed CA and per-service certificates automatically:
./start.sh --rundir ./build/run --bin ./build/qcontrollerd --certs
This generates a CA plus per-service certs (qemu, controller, fileregistry, eventservice, orchestrator) under <rundir>/certs/ and injects tls blocks into every config. SANs match each server's actual dialed address (bridge IP for qemu, host IP for fileregistry on Linux; localhost on macOS).
With --certs enabled, the web UI is at https://localhost:8080/ui/. Call the REST API with:
curl --cacert ./build/run/certs/ca.pem https://localhost:8080/v1/nodes
#Production
--certs is strictly for development. In production, use a proper PKI (internal CA, Let's Encrypt, or cert-manager in Kubernetes) and point the tls blocks at those certs.
#Example Base Image
This repo includes tooling to build a base Ubuntu Cloud image with the QEMU Guest Agent (QGA), compatible with qcontroller's QAPI integration. Use Packer to build it:
packer init . packer build .
Default values are configured for Linux on x86_64. If you're using a different platform, you'll need to adjust these settings. For example, on macOS with Apple Silicon, build the image using:
packer build -var arch=arm64 -var machine=virt -var accelerator=hvf .
See qga for details on building QGA.
#📎 API Access
The gRPC gateway automatically generates an OpenAPI schema that is served at:
http://localhost:8080/openapi.yaml
For real-time VM state updates, connect to the WebSocket endpoint:
ws://localhost:8080/ws
When the orchestrator is configured with TLS, use https:// and wss:// instead.
All REST endpoints follow the schema defined in /src/protos/. WebSocket messages use Protocol Buffers for efficient binary communication.
#🧪 Development Setup
Use the provided Dockerfile to ensure a consistent dev environment.
To run commands inside the container:
./exec.sh make lint
This wraps the environment with all Go tools and build dependencies preinstalled.
#Build Dependencies
makegitgoprotoc- Go plugins:
protoc-gen-goprotoc-gen-go-grpcbufprotoc-gen-grpc-gatewayprotoc-gen-openapigolangci-lint
#Runtime Dependencies
qemu-system-x86_64(x86_64 VMs are supported and tested)qemu-system-aarch64(ARM64 VMs are supported and tested)