zzantares/proxmoxve
A Laravel/PHP client for the Proxmox VE API that lets you manage nodes, virtual machines, LXC containers, storage, and cluster resources directly from your application using a clean, fluent, object-oriented interface. Includes support for VM lifecycle operations, snapshots, and task monitoring.
This is a plain PHP library, not a Laravel package — there's no service provider, facade, or config file to publish. Expect to wire it up yourself, which is usually 20–30 lines.
composer require zzantares/proxmoxve
Requires PHP 5.5+ and the cURL extension (Proxmox's API is HTTPS on port 8006, not HTTP).
use ProxmoxVE\Proxmox;
$proxmox = new Proxmox([
'hostname' => 'pve.example.com',
'username' => 'root@pam', // realm is mandatory
'password' => 'secret',
'port' => 8006,
]);
$proxmox->get('/nodes'); // [{node: "pve", status: "online", ...}]
$proxmox->get('/nodes/pve/qemu'); // QEMU VMs on that node
$proxmox->get('/nodes/pve/lxc'); // LXC containers
The four generic verbs — get(), create(), set(), delete() — map to Proxmox's GET/POST/PUT/DELETE and cover the entire API. That's the whole surface area; the rest of the library is convenience wrappers.
Where to look first in the source: src/ProxmoxVE/Proxmox.php for the transport, then the Access/ classes (Nodes, VirtualMachines, Containers, Pools, Storage). The Access/ classes give you typed helpers like $proxmox->getVirtualMachines()->getVMList($node) instead of hand-writing paths.
Realistic first use case: a /admin/servers page in your app that lists nodes and VMs by querying /nodes and /nodes/{node}/qemu, then starts/stops a VM with create('/nodes/{node}/qemu/{vmid}/status/start').
Bind it as a singleton in a service provider. Don't new Proxmox(...) in controllers — you'll re-login (extra ticket round-trip) on every request and you can't mock it in tests.
// config/services.php
'proxmox' => [
'hostname' => env('PROXMOX_HOST'),
'username' => env('PROXMOX_USER'),
'password' => env('PROXMOX_PASS'),
'port' => env('PROXMOX_PORT', 8006),
],
// AppServiceProvider::register()
$this->app->singleton(Proxmox::class, fn () => new Proxmox(config('services.proxmox')));
Prefer API tokens over username/password. Ticket auth expires (~2h) and breaks if TFA is enabled on the account; tokens don't expire and can be scoped read-only for dashboards. Check the current README for the exact token config key — the library added token support after its ticket-only era, and the shape differs between versions.
Wrap it in your own domain service, not the vendor class. The library returns raw decoded API payloads and throws one generic exception type. Put your business methods behind an interface so you can swap implementations or fake it:
interface ProxmoxGateway {
public function listVms(string $node): array;
public function startVm(string $node, int $vmid): string; // returns UPID
}
Task polling is on you. Every mutating call (start, stop, create, clone, resize) returns a UPID, not a result. The library does not wait. Poll /nodes/{node}/tasks/{upid}/status until status === 'stopped' and check exitstatus === 'OK'. Wrap this in a helper — you'll write it once and reuse it everywhere:
public function waitForTask(string $node, string $upid, int $timeout = 60): void
{
$deadline = time() + $timeout;
do {
usleep(500_000);
$status = $this->proxmox->get("/nodes/$node/tasks/$upid/status");
if ($status['status'] === 'stopped') {
if ($status['exitstatus'] !== 'OK') {
throw new ProxmoxTaskFailed($status['exitstatus']);
}
return;
}
} while (time() < $deadline);
throw new ProxmoxTaskTimeout($upid);
}
Push provisioning into queued jobs. Creating or cloning a VM takes 10–60s. A synchronous controller action will time out and the polling loop will block a PHP-FPM worker. Dispatch a job that creates the VM, stores the UPID, then dispatches a second job (or uses retry/backoff) to poll.
Use the Access/ helpers for read paths, the generic verbs for everything else. The helpers are thin and the API is enormous; don't expect a typed method for migrate, resize, or snapshot. Raw paths are the norm.
Batch reads for dashboards. get('/nodes') then one get("/nodes/{$node}/qemu") per node is N+1 network calls. If you're rendering many rows, cache the raw responses for 30–60 seconds in Redis — Proxmox's API is not fast and you'll hammer it from page loads otherwise.
SSL certificate problem. Check the constructor options and the cURL setup in Proxmox.php for the verify toggle, or supply a CA bundle. Do not globally disable peer verification.root will fail; root@pam works. Same for tokens: user@realm!tokenname.config:cache will bake your credentials into the cached config. That's fine if you accept it, but if you rotate Proxmox passwords/tokens you'll need php artisan config:clear. Consider reading the secret via env() in a dedicated non-cached binding./api2/json. Don't include the prefix in your path strings, and don't include a trailing slash — /nodes/pve/ and /nodes/pve behave differently in Proxmox.ProxmoxException on cURL failures or non-2xx responses. Proxmox returns structured errors in the body (permission denied, invalid VMID, locked VM); inspect the response to distinguish "your credentials are wrong" from "that VM is locked for backup." Catch and translate into your own exception types at the boundary.locked state silently rejects every mutating call. Check $vm['lock'] before you try to start/stop/delete, or you'll get confusing 500s that look like auth problems.root@pam has TFA, ticket login fails outright. Use a dedicated API token for automation — this is the recommended production setup anyway.final-ish and takes an options array, inject your gateway interface and use a fake; you cannot meaningfully mock the vendor class's internals.Http:: client of your own against the documented Proxmox API — for nontrivial workloads, that's often the better call.How can I help you explore Laravel packages today?