cloudalone
cloudalone is a small cloud you run on a single Linux box. It speaks the Hetzner Cloud API, so the official hcloud CLI and the Hetzner client libraries work against it unchanged: point them at your own machine instead of api.hetzner.cloud, and hcloud server create boots a real VM.
Inside, it's a small REST API backed by SQLite, a fixed catalog of server types and images, and libvirt/QEMU for the actual VMs. Every VM gets its own public IPv6 address. It's a single process on one host: no clustering or message broker.
Status
It's a working prototype.
The core loop works on a real host: hcloud server create boots a Debian VM, cloud-init brings up its IPv6, and list, describe, delete, and the power actions behave the way the CLI expects. Responses are checked against the real hcloud Python client, and the request and polling behaviour against the Go CLI. See Limitations for what's missing.
How it works
cloudalone is a Litestar app on SQLite (SQLAlchemy async via Advanced Alchemy), served by uvicorn. VMs run on libvirt and QEMU/KVM and are configured with cloud-init.
The Actions model. The Hetzner API is asynchronous: a call that changes something returns an action, and the client polls that action until it succeeds or fails. cloudalone works the same way. A create request writes the server and action rows, returns 201 straight away, and runs the blocking libvirt work in the background; the CLI polls GET /v1/actions just as it would against Hetzner.
IPv6. Every VM gets a real, public IPv6 address. cloudalone takes one IPv6 prefix that is routed to the host, gives the br-cloud bridge the first address, turns on forwarding, and hands each VM its own slice of the prefix through cloud-init. That's ordinary L3 routing, no NDP proxying. The only provider-specific part is obtaining the routed prefix. On a Scaleway Dedibox you order a /48 and a DHCPv6-PD client keeps a /56 routed to the box (the deploy sets this up); with a provider that routes you a /64 directly, or your own static route, you just point ipv6_prefix at it. Guests are IPv6-only.
Backends. The default stub backend does no real virtualization, so you can run and test the whole thing (API, actions, CLI) on a Mac. The libvirt backend is the real one and needs a Linux host with KVM. libvirt-python is only installed on that host, so Mac development doesn't pull it in.
Requirements
- To develop: Python 3.12+ and uv, on macOS or Linux. The stub backend means no VMs are involved.
- To run VMs: a Linux host with KVM, libvirt, QEMU, and
cloud-image-utils, plus an IPv6 prefix routed to it. The deploy installs the packages.
Quickstart
Run the API against the stub backend and poke it with curl:
uv sync
CLOUDALONE_BACKEND=stub CLOUDALONE_TOKEN=dev-token uv run cloudalone serve &
curl -s -H "Authorization: Bearer dev-token" http://localhost:8000/v1/server_types | python3 -m json.tool
To drive the real hcloud client through a full create → poll → delete against the stub:
uv run python scripts/conformance.py # prints CONFORMANCE OK
The CLI
cloudalone is both the server and a small admin tool:
cloudalone init # set up /home/cloudalone and write a config with a fresh token
cloudalone migrate # create or upgrade the database schema
cloudalone token # rotate the bootstrap token in config.toml
cloudalone token create ci # create a named API token (printed once)
cloudalone token list # list named tokens
cloudalone token revoke ci # revoke a named token
cloudalone image list # list catalog images and which are downloaded
cloudalone image pull debian-13 # download a base image (or `all`)
cloudalone dns # print the VM name -> IPv6 mapping (hosts/dnsmasq format)
cloudalone serve # run the API
The catalog is fixed: server types small, medium, large, and images debian-13 and ubuntu-24.04.
Dashboard
A read-only web dashboard lives at /dashboard: servers, per-server action history, ssh-keys, and the catalog. Log in with a valid API token; it's server-rendered HTML with no build step.
Deploying to a real host
The deploy is a single pyinfra run that turns a bare Linux box into a running service. It installs the virtualization stack, creates the cloudalone user, sets up the br-cloud bridge and IPv6 forwarding (and, on Dedibox, the DHCPv6-PD client that keeps the prefix routed), syncs the code, writes the config and token, pulls the Debian image, and installs the systemd unit. Re-running it upgrades in place; it's idempotent.
-
Edit
deploy/inventory.py: your host, the routedipv6_prefix, and optionallyapi_host(for TLS) anddhcpv6_duid(for Dedibox). -
Run it:
uv run pyinfra deploy/inventory.py deploy/deploy.py
Before pointing it at a real box, you can run the whole deploy against a throwaway container:
uv run pytest tests/c_e2e/test_deploy_docker.py # deploys into a systemd container and checks the API serves
TLS
Set api_host in the inventory and the deploy also installs Caddy as a TLS reverse proxy in front of cloudalone (https://<api_host>/v1, automatic Let's Encrypt), binding the app to localhost behind it. Point an A record at the box and open ports 80 and 443 before deploying, so Caddy can complete the certificate challenge.
Using the hcloud CLI
Point the official CLI at your box and use it normally:
export HCLOUD_TOKEN=$(ssh root@<host> grep token /home/cloudalone/config.toml | cut -d'"' -f2)
export HCLOUD_ENDPOINT=https://api.example.com/v1 # or http://[<host-ipv6>]:8000/v1
hcloud ssh-key create --name me --public-key-from-file ~/.ssh/id_ed25519.pub
hcloud server create --name t1 --type small --image debian-13 --ssh-key me
hcloud server list
ssh -6 root@$(hcloud server ip -6 t1) # your key is installed for root
hcloud server delete t1
You need -6 on the ssh line (or AddressFamily inet6 in ~/.ssh/config), because the address is a bare IPv6 literal.
Configuration
Settings are read from, in order of precedence: an environment variable, the TOML config file, then a built-in default. The config path is CLOUDALONE_CONFIG (default /home/cloudalone/config.toml), and cloudalone init writes one for you.
| Setting | Env var | Default |
|---|---|---|
| Bearer token | CLOUDALONE_TOKEN |
dev-token |
| Backend | CLOUDALONE_BACKEND |
stub |
| Routed IPv6 prefix for VMs | CLOUDALONE_IPV6_SLICE |
2001:bc8:1200:19:c10d::/80 (dev placeholder; the deploy sets the real routed prefix) |
| Base directory | CLOUDALONE_HOME |
/home/cloudalone |
| SQLite path | CLOUDALONE_DB |
cloudalone.sqlite |
| Host bridge | CLOUDALONE_BRIDGE |
br-cloud |
| DNS zone for VM names | CLOUDALONE_DNS_ZONE |
vms.local |
| libvirt URI | CLOUDALONE_LIBVIRT_URI |
qemu:///system |
| Bind host / port | CLOUDALONE_HOST / CLOUDALONE_PORT |
::1 / 8000 |
See config.toml.example for the full file.
Testing
Tests are split into three tiers; uv run pytest runs the fast ones.
tests/a_unit/: fast and isolated (renderers, config, pure logic). Runs anywhere.tests/b_integration/: the real libvirt backend. Each test skips unless what it needs is present (libvirtd,qemu-img,cloud-localds,/dev/kvm), so on a Mac or in CI they skip rather than fail.tests/c_e2e/: the API through Litestar's test client on the stub backend, plus two Docker tests: one runs the suite inside a Linux container, the other runs the full deploy.
make test, make lint, and make test-cov are the usual entry points. CI runs on GitHub Actions and SourceHut.
Project layout
src/cloudalone/
app.py Litestar app, bearer-auth guard, hcloud error envelope
controllers/ hcloud-compatible REST controllers (one module each)
schemas.py hcloud wire-format builders (the external contract)
catalog.py server types, images, locations, datacenters
models.py SQLAlchemy models (Server, Action, SSHKey, Token)
worker.py background action runner (the Actions model)
backend.py VM backend Protocol + stub
libvirt_backend.py real backend: QEMU/KVM + cloud-init (Linux-only)
net.py IPv6 allocation from the routed prefix
dns.py VM DNS naming (<name>.<zone>)
dashboard.py server-rendered HTML for the read-only dashboard
config.py runtime config (env > TOML > default)
cli.py the cloudalone CLI
migrations/ alembic schema migrations
deploy/ pyinfra deploy + the Docker deploy test
scripts/ conformance check, IPv6 probe, host setup
Limitations
cloudalone implements the core of the hcloud API and stops there. The main gaps:
- No networks, volumes, floating IPs, load balancers, or firewalls. Guests are IPv6-only.
- Flat tokens, no projects. Multiple named tokens work, but there's no project scoping of servers or keys.
- DNS needs a resolver. Each server has a name (
<name>.<zone>) andcloudalone dnsemits the mapping, but you still have to point a resolver at it (dnsmasq or/etc/hosts) and delegate the zone beforessh <name>resolves. If the box already runs a resolver on port 53 (systemd-resolved, a stock BIND), pin dnsmasq's bind first; see the deploy guide.
On a Dedibox specifically, you have to order the /48 block and enable "Activate IPv6 SLAAC" in the console before the routed prefix works.
License
Apache 2.0. © Abilian SAS.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file cloudalone-0.2.0.tar.gz.
File metadata
- Download URL: cloudalone-0.2.0.tar.gz
- Upload date:
- Size: 34.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.11.33 {"installer":{"name":"uv","version":"0.11.33","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
59684c78ed9f43df84f9765b0d95cef5a9e8e7150eed9b7ed1f0b16d14f8942a
|
|
| MD5 |
0001a090016aa69dfc454603869c7514
|
|
| BLAKE2b-256 |
3fbf0cdfb0a2cc03727dc4dfe94433f080e23cb619ad73284d115685e635adb3
|
File details
Details for the file cloudalone-0.2.0-py3-none-any.whl.
File metadata
- Download URL: cloudalone-0.2.0-py3-none-any.whl
- Upload date:
- Size: 43.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.11.33 {"installer":{"name":"uv","version":"0.11.33","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
913a21ec6bea93ccb62fa9fc2e7707606d4054263cbfce51eab2874fba8fa737
|
|
| MD5 |
9c39b2851c49545799cd4c87c0578db2
|
|
| BLAKE2b-256 |
bc48d3f883b8dbd0157f1aeb6a1ff3dd60dd54e81dc29fcbe749dbbc3749c1b5
|