Operator documentation

Hyperion docs

Everything to install, run and script a Hyperion node or cluster. Grounded in the project README and operator runbook β€” source on GitHub.

Introduction

Hyperion is a self-hosted, multi-node hosting control panel written in Rust. One binary on each server, one web UI on the master. It provisions PHP / static / Node.js sites end-to-end β€” nginx + FPM pool + database + TLS + WordPress β€” in a single atomic transaction, and manages a fleet of VPSes from one screen.

Two layers per box

  • hyperion-agent β€” runs as root, owns all system state (users, dirs, nginx vhosts, FPM pools, DBs, certs, FTP, cron, backups). Listens on a local Unix socket /run/hyperion.sock (mode 0660, group hyperion-admin). On worker nodes it also listens on 0.0.0.0:9443 for signed RPC from the master.
  • hyperion-web β€” master only. axum + askama + HTMX, runs unprivileged in the hyperion-admin group. Owns the audit log, web users, sessions ledger, enrolled-nodes registry and the Ed25519 master signing key.
  • hctl β€” a thin CLI over the same Unix socket, plus hctl remote over the HTTP API.
Status: Beta. Implemented and demo-tested in VMs, unit-tested throughout β€” not yet hardened by real production traffic. Bug reports and ideas welcome.

Install

One-liner β€” fresh Debian 12+ VPS, as root

In ~3–5 minutes the script apt-installs nginx + MariaDB + PostgreSQL + PHP 8.3, installs Rust if missing, builds Hyperion from source, lays down /etc/hyperion/{agent,web}.toml, installs systemd units, starts both services and prompts for an admin password.

curl -fsSL https://raw.githubusercontent.com/nechodom/hyperion/main/packaging/install/install-master.sh \
  | sudo bash

sudo usermod -aG hyperion-admin "$USER"   # log out / in, then visit https://<host>:8443

Add a worker node (cluster mode)

In the web UI: Nodes β†’ Generate invite, copy the printed curl … | sudo bash … command and paste it on a fresh Debian 12+ VPS. The node enrolls within ~30 seconds and appears in the Nodes table.

In-place updates

sudo /opt/hyperion/packaging/install/update.sh
# or, from the web UI: /install β†’ row for the node β†’ Update…

The script stops services, fast-forwards /opt/hyperion, rebuilds, reinstalls binaries, refreshes systemd units only if they changed, and tails journalctl on a health-check failure. Schema migrations are compiled into the binary and applied on startup.

Local development (macOS / dev VPS)

git clone https://github.com/nechodom/hyperion
cd hyperion
cargo build --release --workspace
# binaries land in target/release/{hyperion-agent,hyperion-web,hctl}

Configuration

The installer writes these for you; this is the manual reference. First lay down the group, directories and the agent config.

sudo groupadd --system hyperion-admin
sudo usermod -aG hyperion-admin "$USER"    # log out / back in to pick up

sudo install -d -m 0700 /etc/hyperion
sudo install -d -m 0700 /etc/hyperion/secrets
sudo install -d -m 0700 /var/lib/hyperion
sudo install -d -m 0750 /var/log/hyperion
sudo install -d -m 0755 /var/lib/hyperion/acme-challenges

/etc/hyperion/agent.toml

[agent]
socket_path  = "/run/hyperion.sock"
socket_group = "hyperion-admin"
state_db     = "/var/lib/hyperion/state.db"
secrets_dir  = "/etc/hyperion/secrets"
log_path     = "/var/log/hyperion/agent.log"
home_root    = "/home"

[acme]
directory_url = "https://acme-v02.api.letsencrypt.org/directory"
contact_email = "you@example.com"   # required for LE
challenge_dir = "/var/lib/hyperion/acme-challenges"

systemd unit

The agent runs as root through systemd, hardened with NoNewPrivileges, ProtectSystem=full, PrivateTmp and friends.

[Service]
Type=simple
ExecStart=/usr/sbin/hyperion-agent --config /etc/hyperion/agent.toml
Restart=on-failure
RestartSec=3s
NoNewPrivileges=true
ProtectSystem=full
ProtectKernelTunables=true
PrivateTmp=true
LogsDirectory=hyperion
RuntimeDirectory=hyperion
sudo systemctl daemon-reload
sudo systemctl enable --now hyperion-agent
sudo systemctl status hyperion-agent
The socket appears at /run/hyperion.sock (group hyperion-admin). Add your user to that group to talk to it without root.

Hostings

A hosting is created in one transaction: Linux user, PHP-FPM pool, MariaDB / Postgres DB, nginx vhost and a self-signed cert. Failure at any step rolls back the rest on a LIFO stack β€” no orphan rows, no zombie users.

$ hctl hosting create example.com --php 8.3 --db mariadb
βœ“ created example_com (id=01K4Z…)
  root: /home/example_com/example.com/htdocs
  db:   lm_a8c_examplecz (user=lm_a8c_u, password=Hx9k…RnG2)
  cert: issuer=self-signed, not_after=2027-06-01
  • PHP 8.1 / 8.2 / 8.3 / 8.4 side by side via deb.sury.org. Static-only sites, and a reverse-proxy mode for Node.js / Python / Docker.
  • Suspend / resume β€” 503 page, FPM stop, DB lock, user processes killed. Fully reversible.
  • Let's Encrypt β€” HTTP-01 one-click + auto-renewal, and DNS-01 wildcard (*.domain) via guided manual TXT or a Cloudflare API token.
  • Quotas β€” kernel-level disk quota via setquota, per-pool memory_limit, monthly bandwidth alerts.
  • Per-hosting controls β€” HTTP basic auth, HSTS presets, a validated custom nginx snippet, FastCGI page cache, per-hosting Redis object cache, .user.ini php override, live log tail.
  • WordPress β€” plugin/theme manager via wp-cli, Wordfence vulnerability scan, staging β†’ push-to-prod with a pre-push safety backup.
Every slow action is a detached job. Create, migrate, clone, backup, restore, cert issue, WP install and panel import all redirect to a live /jobs/<id> page. Close the tab and the work keeps running; an orphan reaper fails anything a restart interrupts.

Backups

  • Local β€” tar.gz of htdocs + mysqldump / pg_dump + JSON manifest under /var/lib/hyperion/backups.
  • Off-site S3 + age encryption β€” multi-target (Wasabi / Backblaze B2 / Minio / AWS), per-target retention (daily / weekly / monthly). Client-side age encryption keeps the private key off the node.
  • Granular restore β€” full, database-only (roll back a bad plugin update without touching media) or files-only. Restore any archive as a brand-new domain.

Foundation-level manual backup of the state DB + secrets:

sudo tar -czf /root/hyperion-backup-$(date +%F).tar.gz \
  /etc/hyperion /var/lib/hyperion

Multi-node cluster

The master holds the web UI, audit log and enrolled-nodes registry. Workers run an agent the master drives over an Ed25519-signed RPC channel β€” a signed envelope over self-signed HTTPS on port 9443. Integrity comes from the signature, not the TLS; there's no DNS dependency between nodes (IP-based).

  • Auto-placement β€” pick β˜… auto on create and the master scores every node (load + memory + hosting count) and picks the best fit.
  • One-click migration β€” move a hosting between any two nodes with live progress and a version preflight (catches a stale worker before cryptic failures). Source is suspended, not deleted, so you can verify first.
  • Cross-node clone β€” duplicate example.com as staging.example.com on a different node in two clicks.
  • Remote node update from the master UI β€” apt + Hyperion rebuild runs on the worker, log streams into the panel.
  • Test-node mode β€” test hostings get auto-subdomains and blog_public = 0; prod hostings refuse to land there.

Panel import (migrate in)

Move existing sites off another panel without a weekend of manual work. Wizard at /import (admin only), or hctl hosting import-panel.

  • Sources: HestiaCP and CloudPanel. Reads the source panel's own state directly β€” Hestia's flat *.conf files, CloudPanel's SQLite store. No scraping, no API.
  • In-place or remote. Remote imports from another machine over SSH: give Hyperion the host + a private key, files come across with rsync and databases are dumped over ssh. The key is used for that one run, written 0600, then deleted β€” never stored.
  • Dry-run first. A plan shows exactly what would be created / skipped / conflict before anything is touched. An existing domain is skipped, never overwritten.
  • WordPress included. Files copied, DB dumped and restored, wp-config.php repointed at the new credentials automatically.
Honest scope β€” mail & DNS are out. Hyperion runs no mail server or authoritative nameserver, so mailboxes and DNS zones are reported, never migrated. Move those separately.

REST API

A Bearer-authenticated /api/v1 mirrors the panel as JSON. Every OpenAPI path is generated straight from the handlers, so the contract can't silently drift.

Authentication

Send an API key as a Bearer token. Keys are shown once at creation and stored only as a hash.

curl -H "Authorization: Bearer hyp_…" \
     https://master.example.com:8443/api/v1/hostings
  • Scoped, owner-clamped keys β€” a capability bitmask + tenant scope, clamped to ≀ the owner at mint and re-clamped on every call. Demote a user and its keys shrink instantly.
  • Per-key IP allowlist (CIDR) and a per-key rate limit β€” over budget returns 429 with a Retry-After.

OpenAPI & docs

  • Machine-readable contract: GET /api/v1/openapi.json (unauthenticated).
  • Rendered docs UI: /api/v1/docs β€” self-hosted, no SaaS, no upload.

Example β€” create a hosting

Slow actions return a job id; poll it for progress.

$ curl -X POST https://master.example.com:8443/api/v1/hostings \
       -H "Authorization: Bearer hyp_…" \
       -d '{"domain":"new.example.com","php":"8.3","db":"mariadb"}'

{ "job_id": "01K5…", "poll": "/api/v1/jobs/01K5…" }

Endpoint reference

GET/me
GET/nodes
GET/jobs/{id}
GET/hostings
POST/hostings
GET/hostings/{id}
DEL/hostings/{id}
POST/hostings/{id}/suspend
POST/hostings/{id}/resume
PATCH/hostings/{id}/limits
PATCH/hostings/{id}/php
PATCH/hostings/{id}/vhost
PATCH/hostings/{id}/expiry
PATCH/hostings/{id}/quota
POST/hostings/{id}/backup
GET/hostings/{id}/backups
POST/hostings/{id}/cert
POST/hostings/{id}/wp/install
POST/hostings/{id}/restore
POST/certs/renew-all

All paths are prefixed with /api/v1.

CLI β€” hctl

A thin client over the same Unix socket as the web UI β€” the "ssh in and poke" path when a node is too broken for the web to help.

$ hctl info
agent: master.example.com version=v1.2.0-5-gf718fd1 schema=54 hostings=12

$ hctl hosting suspend example.com --reason="payment overdue"
βœ“ suspended

$ hctl hosting backup-now example.com
βœ“ backup 17 ok

$ hctl audit --limit 5

Off-box β€” hctl remote

Drive any master over the /api/v1 HTTP API with the same verbs β€” point it at a URL and an API key instead of the local socket.

Security

  • #![forbid(unsafe_code)] in every crate β€” the compiler enforces it.
  • Argon2id passwords at OWASP parameters; constant-time secret & username compare on every login and heartbeat.
  • Ed25519 session cookies with a DB-backed revocation ledger β€” kill a stolen cookie from /settings/sessions.
  • TOTP 2FA enforced for admin+ roles, with backup codes and a 30-day remember-device option.
  • Native brute-force protection β€” the agent scans access logs for wp-login / xmlrpc floods and auto-bans IPs via an nftables set; bans survive reboots.
  • Tamper-evident audit log β€” a BLAKE3 hash chain over every state change, verified on agent startup (a broken chain refuses to start).
  • CSP + HSTS + X-Frame-Options + Permissions-Policy + Referrer-Policy on every response; per-form CSRF tokens.
sudo sqlite3 /var/lib/hyperion/state.db \
  "SELECT ts, action, result FROM audit_log ORDER BY id DESC LIMIT 20"

Troubleshooting

  • Permission denied on socket β€” make sure your user is in hyperion-admin and you've started a new shell since the usermod -aG change.
  • useradd: not in sudoers β€” the agent is meant to run as root through systemd. If running from a shell, prefix with sudo.
  • nginx fails nginx -t β€” the agent restores the backup vhost on failure; check journalctl -u hyperion-agent -e.
  • MariaDB socket auth β€” Debian default uses unix_socket auth for root. If you changed it, add credentials to /root/.my.cnf.

Logs

# structured agent log (JSON Lines)
sudo tail -f /var/log/hyperion/agent.log

# service journal
journalctl -u hyperion-agent -e
Need the full runbook?

Manual production deploy, MariaDB hardening, removal and more.

RUNBOOK.md β†—