Saving a dashboard's widget layout rebuilt the stored JSON without a version field, so it was written to the database as an unversioned (v1) layout every time, no matter what version the client actually sent. The next time the dashboard loaded, the client's one-time v1-to-v2 migration (doubling every widget's height and y position for the new row unit) ran again, because the saved layout looked unversioned. Since that migrated result got saved right back the same way, every save-reload cycle doubled each widget's height again - this is what showed up as widgets "randomly" growing or ending up a different size than what was saved. The server now always stamps its own current layout version when saving, instead of depending on the client to round-trip one correctly. Also added minimum/maximum size limits to each widget type so a resize can't be dragged down to an unreadable sliver or blown out to an extreme height by accident, and a regression test for the versioning bug itself.
Ferrum
Fleet control for Proxmox VE — a single dashboard for every cluster and standalone node you run, with live inventory, dashboards, backups, HA, firewall, alerting, and more.
- Repository: https://github.com/anand34577/ferrum
- Website & docs: https://anand34577.github.io/ferrum/ — includes a live click-through demo
- Backend: Go (chi router), SQLite or PostgreSQL
- Frontend: React + TypeScript, Vite, Tailwind CSS v4
Features
- Fleet-wide overview — every connection (PVE cluster, standalone node, or PBS remote) rolled up into one dashboard: node/guest counts, CPU/memory/storage, active alerts, and a drag-and-drop customizable dashboard with 20+ widgets.
- Inventory & operations — nodes, VMs, and LXCs with live consoles/shells (noVNC + xterm.js), snapshots, guest agent file browser, bulk start/stop/migrate, and cross-cluster guest migration.
- Storage & backups — pool usage and Ceph health across every connection, backup job status and replication, and PBS remote integration alongside native PVE storage.
- High availability, firewall & SDN — HA groups/resources, cluster and per-node firewall rules, and SDN zones/VNets/subnets, all per connection.
- Alerting & automation — threshold-based alerts (CPU/memory/disk/guest), config drift detection, guest lifecycle policies, capacity forecasting, a fleet health score, scheduled health-digest emails, and Terraform/Ansible inventory export.
- Integrations — outbound webhooks for real-time events, a REST API and MCP server (scoped API keys, so any MCP-capable agent or script can drive Ferrum), and a built-in AI Assistant that can use any OpenAI-compatible provider — including a zero-config local model (Needle 2) with no API key or network required.
- Access & auditing — user management with role labels (effective access control is admin vs non-admin), optional OIDC single sign-on, session/certificate monitoring, and a full audit log of every mutating action across the UI, REST API, and MCP.
- A dozen look-and-feel presets — Enterprise, Proxmox-native, Terminal, Glass Flight Deck, Midnight, Paper, Glassmorphism, Neumorphism, Brutalist, Solarized, High Contrast, and Aurora — each with light/dark and a choice of accent colors.
Screenshots
A populated fleet — 3 connections, 6 nodes, 26 VMs/LXCs across two clusters and a standalone host, with Ceph and NFS storage. Click any thumbnail for the full-size image.
Fleet overview |
Inventory |
Topology |
Storage |
Backups & replication |
High availability |
Firewall |
Look & feel — Enterprise / Proxmox-native / Terminal |
Connections |
First-run setup |
Getting started
The web UI is embedded in the binary (see web/embed.go), and web/dist is gitignored — so on a fresh clone the frontend must be built before the Go build, or go:embed will fail:
cd web
npm install
npm run build
cd ..
go build ./cmd/ferrum
./ferrum -config config.example.yaml
For frontend development:
cd web
npm install
npm run dev
On first run, open the UI and create the initial admin account, then add a Proxmox connection (host, port, and either an API token or username/password) from Connections.
Docker
Prebuilt multi-arch (amd64/arm64) images are published to GHCR on every release:
docker run -p 8080:8080 -v ferrum-data:/app/data ghcr.io/anand34577/ferrum:latest
Or build locally from source:
docker build -t ferrum .
docker run -p 8080:8080 -v ferrum-data:/app/data ferrum
Deploying a release build
Every GitHub release ships prebuilt, statically-linked archives for Linux (amd64/arm64), Windows (amd64/arm64), and macOS (amd64/arm64) — no Go toolchain or CGO dependencies needed on the target machine. Each archive bundles the binary, config.example.yaml, and the install script for its OS.
Linux (systemd)
One-liner, same idea as get.docker.com — downloads the latest release for your architecture, verifies its checksum, and installs it as a systemd service:
curl -fsSL https://raw.githubusercontent.com/anand34577/ferrum/main/scripts/get.sh | sudo sh
Pin a specific version with FERRUM_VERSION:
curl -fsSL https://raw.githubusercontent.com/anand34577/ferrum/main/scripts/get.sh | FERRUM_VERSION=v1.2.3 sudo sh
Or do it by hand from a downloaded archive — scripts/get.sh just automates these same steps:
tar -xzf ferrum_*_linux_amd64.tar.gz
cd ferrum_*_linux_amd64
sudo ./install.sh
Either way, this creates a dedicated ferrum system user, installs the binary to /usr/local/bin/ferrum, seeds /etc/ferrum/config.yaml, and enables + starts the ferrum.service systemd unit (packaging/systemd/ferrum.service) — data lives in /var/lib/ferrum, logs go to journalctl -u ferrum -f.
To uninstall, grab scripts/linux/uninstall.sh and run it as root — add -- --purge to also remove config/data:
curl -fsSL https://raw.githubusercontent.com/anand34577/ferrum/main/scripts/linux/uninstall.sh | sudo bash -s --
Windows (Windows Service)
Expand-Archive ferrum_*_windows_amd64.zip
cd ferrum_*_windows_amd64
.\install-service.ps1 # run as Administrator
This installs the binary to %ProgramFiles%\Ferrum, seeds %ProgramData%\Ferrum\config.yaml, and registers a "Ferrum" Windows service — ferrum.exe detects it's running under the Service Control Manager and manages its own start/stop lifecycle, no wrapper (NSSM etc.) needed. Since Windows services don't capture stdout/stderr the way systemd does, logs are written to %ProgramData%\Ferrum\ferrum.log. Uninstall with .\uninstall-service.ps1 (add -Purge to also remove config/data).
Building from source
scripts/build.sh # current platform only, output in dist/
scripts/build.sh linux/amd64 windows/amd64
scripts/build.sh all # every platform the release workflow builds
.\scripts\build.ps1 # Windows-native equivalent, current platform only
Both build the frontend, cross-compile with version info baked in (ferrum -version), and package each target as a .tar.gz/.zip with its install script — the same thing .github/workflows/release.yml runs when a vX.Y.Z tag is pushed, publishing the resulting archives (and a checksums.txt) as GitHub Release assets.
Configuration
Copy config.example.yaml to config.yaml and adjust as needed, or set the equivalent FERRUM_* environment variables (see the comments in that file for the full list, including SQLite/PostgreSQL, TLS cookie behavior, reverse-proxy support, and optional OIDC single sign-on).
Everything else — notifications, SSO details, security policy, system settings, AI providers, and the REST API/MCP enable switches below — is configured from the admin Settings UI once Ferrum is running, not from environment variables.
Built-in LLM (Needle 2)
The AI Assistant and MCP tool-calling loop can use any OpenAI-chat-completions-compatible provider (OpenAI, Ollama, LM Studio, LocalAI, OpenRouter, ...) configured under Settings > AI Providers. There's also an optional zero-config, no-API-key, fully local option backed by Needle 2 — a small (45M-parameter) tool-calling model that runs as a self-contained CLI binary with no GPU and no network access required at inference time.
Needle 2 is Apache-2.0 licensed, so on Windows, Linux, and macOS (amd64 or arm64) Ferrum ships its official CLI binary baked into the ferrum binary itself (internal/needle/bundled_*.go, one per platform via go:embed) — nothing to download, nothing to configure. On a fresh install (no AI provider configured yet), Ferrum extracts it to a cache file and registers it automatically as the default assistant the first time it starts — no manual "Add provider" step needed. If you've already configured a provider, or want to add/re-add it yourself, use Settings > AI Providers > "Add provider" > the Needle 2 (built-in, local) preset.
On any other platform (32-bit, RISC-V, Windows/ARM64, ...) there's no bundled binary — Ferrum still never fetches executable content from the network on its own. To enable it there:
- Download the
needleCLI binary for your platform from the Needle 2 files. - Point Ferrum at it: set
FERRUM_NEEDLE_BIN=/path/to/needle(orneedleBinPathinconfig.yaml) before starting Ferrum. This also overrides the bundled binary on a supported platform, if you'd rather run a different build.
Ferrum starts the binary itself (as a local subprocess, 127.0.0.1-only) the first time it's used, and stops it on shutdown. If no binary is bundled for the platform and FERRUM_NEEDLE_BIN isn't set (or doesn't exist), this provider simply isn't usable — every other provider is unaffected.
Troubleshooting on small ARM boards (Raspberry Pi and similar SBCs): Needle does a one-time "tool retrieval" pass over Ferrum's full tool catalog (~60 tools) the first time it's used, which briefly uses noticeably more memory and CPU than steady-state chat — Ferrum already waits up to 60s for that first startup. On a board with very little RAM this pass can get the subprocess killed by the kernel's OOM-killer instead; the error will say needle process exited: signal: killed. If you hit that, set FERRUM_NEEDLE_MAX_TOOLS=<n> to cap how many tools Needle is given (e.g. 10 keeps just the original read-only lookup tools), trading away the newer create/manage tools for a much lighter embedding pass. If it still fails, the board likely doesn't have enough RAM to run even this small a model reliably — use a real OpenAI-compatible provider instead (a cheap/free hosted API, or Ollama on a separate, beefier machine).
API access, MCP, and audit logging
Any user can generate long-lived API keys under Profile > API Keys — scoped to either the general REST API (Authorization: Bearer <key> against /api/v1/..., for 3rd-party integrations and scripts) or the MCP endpoint only (/mcp, for Claude Code/Desktop or any other MCP-capable agent — see Profile > MCP integration for ready-to-paste config). Both surfaces are off by default and must be turned on by an admin under Settings > API & MCP, which also caps how many tool calls the AI Assistant's agent loop can make per message. Every mutating action — through the UI, the REST API, or MCP — is recorded with who, what, and when under Audit Log (admin-only).









