A modern hosting control panel for WordPress and PHP hosting, built with Go and React.
Website | Live demo | Install | Architecture | CLI
Multi-tenant isolation. Root-safe automation. Database-driven reconciliation. A single panel binary serves the API and embedded SPA, while privileged host operations are delegated to a root-owned Unix-socket agent.
Note
Jabali Panel is currently a release candidate. Expect rapid iteration and breaking changes until 1.0.
- Website: https://jabali-panel.com/
- Demo: https://jabali-panel.com/demo/
The public demo at https://jabali-panel.com/demo/ runs demo mode, which
lives on the long-lived feat/demo-mode branch (open PR, e.g. #103) and is
intentionally never merged to main.
What demo mode adds:
- write-blocking middleware — every non-idempotent
/api/v1/*request (POST/PUT/PATCH/DELETE) returns403 {"error":"demo_mode"}, so visitors can browse every read endpoint without ever reaching the agent or a DB write; - a
/infoendpoint that exposes the seeded demo credentials; - a fixed DEMO banner + "Enter as admin / Enter as user" buttons that replace the real login form.
It is config-gated off by default ([demo] enabled = false), so it is inert in
a normal install. It is still kept off main on purpose: merging would ship the
demo middleware, the DEMO banner/login override, and especially the
credential-exposing /info endpoint into every production binary + SPA —
one mis-set toggle away from leaking seeded creds on a real host. Keeping it on
its own branch means production installs never carry that code at all.
Operating the demo: deploy the feat/demo-mode branch to the demo host,
rebase it on main when you want newer features, and leave the PR open as the
deploy/tracking branch — do not merge it. Production fixes go to main and
are picked up on the next rebase.
One-line install on a fresh Debian 13 box:
curl -fsSL https://get.jabali-panel.com | sudo bash
Direct URL (fallback, always works):
curl -fsSL https://raw.githubusercontent.com/shukiv/jabali-panel/main/install.sh | sudo bash
The installer fetches Go 1.25, builds the panel + agent binaries, builds the
SPA with Vite, writes systemd units, provisions MariaDB + Redis + PowerDNS +
Stalwart + Bulwark + CrowdSec, and smoke-tests /health. Idempotent — re-run
to upgrade.
Optional flags:
--debugshow full output instead of spinner--hostname <fqdn>override auto-detected hostnameJABALI_HOSTNAME=<fqdn>env-var equivalent for unattended installs
Uninstall (rolls back system packages, optionally keeps /home):
curl -fsSL https://get.jabali-panel.com | sudo bash -s -- --uninstall
After install:
- Admin panel:
https://your-host:8443/jabali-admin - User panel:
https://your-host:8443/jabali-panel - Webmail:
https://mail.your-domain/
The panel API listens on a Unix socket; nginx terminates TLS on :8443 and
proxies upstream. If nginx goes down, the panel and agent stay running so
operators can recover via jabali CLI without losing in-flight state.
- Per-user Linux accounts with per-user PHP-FPM master + cgroup v2 + POSIX quota
- SSH shell access via nspawn containers with auto-start and idle timeout
- Root agent for SSL, mail, DNS, backups, migrations — fronted by a typed
NDJSON RPC contract over
/run/jabali-agent/agent.sock(no shelling out from the panel) - DB-as-truth model: a 60s reconciler reads the panel DB and converges nginx vhosts, PHP pools, mailboxes, DKIM, DNS, SSL, mta-sts, and per-user limits
- cPanel and WHM migrations (analyse → fix-perms → validate → restore) with preserved MySQL users + password hashes
- IMAP sync for migrating mail from external servers
- Stalwart Mail Server with browser-trusted IMAPS/465/587 (LE cert pushed into Stalwart Certificate object) and self-deleting SSO file (Installatron pattern) for one-click webmail
- Per-mailbox forwarders, autoresponders, shared folders, disclaimers
- Bulwark (Next.js JMAP) webmail with same-origin per-tenant routing
(nginx
sub_filterrewrites panel hostname →$hostso the SPA stays same-origin onmail.<tenant>) - PowerDNS authoritative + recursor with native DNSSEC (per-domain toggle)
- Per-domain listen-IP binding (M24 IP Manager) with reserved-word-safe migrations against MariaDB 11.x
- Per-domain opt-in nginx FastCGI micro-cache with safe-bypass for cart / admin / authenticated cookies
- Restic backups (account_full + system_backup) with encryption, dedup, SFTP / S3 destinations, scheduled + on-demand
- WordPress 1-click install / delete / clone (M10) — 15-app catalogue (M19) incl. Moodle / Joomla / NextCloud / OpenCart / Mautic / Drupal
- Per-user resource limits: cgroups v2 slice drop-in, nginx limit_req, POSIX quota — admin toggleable, reconciler-converged
- Integrated security suite: CrowdSec parsers + AppSec WAF + per-user egress firewall (nftables + cgroupv2-vmap) + ModSecurity replaced by CrowdSec AppSec (ADR-0060) + LMD + ClamAV-on-demand + YARA + Tetragon for malware detection + jabali quarantine + M14 notifications dispatch
- 6-channel notifications: Discord, ntfy, Web Push (VAPID), SMS, Email, Webhook, Slack, in-app bell — 4 event sources incl. cert renew, disk full, service down, CrowdSec spike
- One-time login tokens (CLI + dashboard) with IP binding
- Magic-link-free SSO between panel and webmail (no Hydra / OIDC overhead — ADR-0040 supersedes the M16 Hydra rollback)
- Audit logs, account activity feed, encrypted diagnostic-log sharing to support, in-app updates + support tabs
- Dashboard with stats, health, recent activity, notifications bell
- User management with suspension, packages, quotas, impersonation
- Server settings (hostname, nameservers, public IPs, panel cert)
- Service manager for systemd services + start/stop/restart
- PHP version and per-user pool management (server-wide extensions tab)
- DNS zones, templates, DNSSEC, secondary NS
- SSL issuance and renewals (panel cert + per-domain)
- IP address assignments (managed IP pool, per-domain bind)
- Backups: account_full + system_backup, local + remote (SFTP/S3), schedules, encrypted destinations
- Migrations (cPanel restore, WHM downloads, IMAP sync)
- Security: CrowdSec allowlists / alerts / console / captcha + UFW + AppSec geoblock + per-user egress firewall + malware quarantine
- Updates + Support tabs: live
jabali updatewith transient systemd units, enclosed-encrypted diagnostic sharing to webmaster - Server status (CPU / mem / disk / queues / 5s polling)
- Database admin ops (curated tuner, root password, processlist, pmaAdmin SSO)
- Email queue, throttles, MTA-STS, outbound reports
- Audit logs, notifications dispatcher, jabali-isolator events
- Notification channel admin (test-send, scopes, throttles)
- Domains, redirects, custom nginx rules, listen-IP, FastCGI micro-cache
- DNS records editor with conflict detection (CNAME exclusivity per RFC 1034 §3.6.2) and dedup
- Mail: domains, mailboxes, forwarders, autoresponders, catch-all, disclaimers (HTML), shared folders, mail logs
- IMAP sync (single + bulk)
- Webmail SSO (Bulwark, Next.js JMAP)
- WordPress (install, update, scan, SSO) + 14 other 1-click apps
- File manager (AntD-native) + SFTP + SSH keys
- SSH shell access via nspawn containers (idle timeout)
- Databases (MariaDB + Postgres in tabbed view) with phpMyAdmin SSO
- PHP settings per account
- SSL management
- Cron jobs (systemd-user timers + allowlist)
- Backups + restore (account_full)
- Logs, statistics, bandwidth usage (daily nginx-log sync)
- Support access link generator (one-time IP-bound tokens)
- Notification preferences (Discord, ntfy, Web Push, SMS, Email)
- Root-level agent (
panel-agent) with typed NDJSON RPC handler registry - Reconciler (60s tick) converges domain.create / SSL / DKIM / vhosts / PHP pools / nginx rate-limits / mailboxes / mtasts / ssh keys / cron
- Job queue: async backup + migration steps + WordPress install
- Health monitor with notification dispatch on service down / cert near- expiry / disk-full / CrowdSec spike / queue depth
- Redis (Unix socket, ACL-scoped) for cache, sessions, notifications dispatcher streams
- Per-domain opt-in FastCGI micro-cache + manual purge
- Multi-language UI (en default; i18n harness ready)
- Control plane: Go binary
panel-api(Gin) listening on/run/jabali-panel/api.sock. Embeds the SPA and serves it from/ - Data plane: Go binary
panel-agentrunning as root, listening on/run/jabali-agent/agent.sock(0660, groupjabali). Typed NDJSON RPC registry — every privileged op (nginx reload, certbot, systemctl, mysql DDL, file ops) is a named handler the panel calls by name. No shelling out from the panel itself - State plane: MariaDB
jabali_panel(single DB, single writer = panel- api). Reconciler reads the DB every 60s and converges the host - Job plane: Redis Streams dispatcher (notifications, backups, mail- scan)
- Frontend: React 19 + Ant Design 5 + TanStack Query, built by Vite, served from the Go binary's embedded FS — single deploy unit, no Node runtime on the host
- Webmail: Bulwark (Next.js JMAP) at
/opt/jabali-webmail, served onmail.<tenant>per-domain via nginx → Unix socket - SSH shell: nspawn containers (debian-13-v1 image) for SSH access isolation; jabali-isolator handles container lifecycle
- Security: CrowdSec parsers + AppSec (nginx-bouncer Lua, WAF) + per-user egress firewall (nftables + cgroupv2-vmap, ADR-0084) + LMD + ClamAV-on-demand + YARA + Tetragon
- Logging: structured JSON via slog; nginx access logs feed CrowdSec
- Server metrics: live
/procreads, no Prometheus exporter dependency
Service stack (single-node default):
- panel-api (Go, Unix socket, embedded SPA)
- panel-agent (Go, Unix socket, root)
- nginx (TLS terminator on
:8443, user vhosts on:80/:443, per-domain mail vhost on:443, FastCGI cache keyzone shared, AppSec bouncer Lua) - MariaDB (Unix socket only —
skip-networking) - Redis (Unix socket, mode 0660, group
jabali-sockets) - PowerDNS authoritative (split-port :5300, MySQL backend) + pdns-recursor (loopback :53, resolver chain)
- Stalwart Mail Server (SMTP / IMAP / 465 / 587 / 993 / JMAP / ManageSieve, LE-cert pushed into Certificate object)
- Bulwark (Next.js JMAP webmail, Unix socket, served per-tenant)
- Kratos (Unix socket admin + public, sole auth source — M20)
- CrowdSec (LAPI socket + AppSec :7422 + nginx-bouncer Lua)
- Restic (encrypted, dedup, backup destinations)
- jabali-isolator (nspawn container lifecycle)
- systemd-user (cron jobs as user-scope timers)
- Fresh Debian 13 install (no pre-existing web or mail stack)
- 2 GB RAM minimum (4+ recommended; small VM gets auto-swap during build)
- A domain for panel + mail (with glue records if hosting DNS)
- PTR (reverse DNS) for mail hostname
- Open ports: 22, 80, 443, 8443, 25, 465, 587, 993, 995, 53
See docs/adr/ for the full architectural-decision record
(110+ ADRs covering every load-bearing design choice). Highlights:
| Variable | Purpose | Default |
|---|---|---|
JABALI_HOSTNAME |
Override auto-detected panel hostname during install | (auto) |
JABALI_PANEL_BIND |
Override panel-api listen socket | /run/jabali-panel/api.sock |
JABALI_AGENT_SOCKET |
Override agent RPC socket | /run/jabali-agent/agent.sock |
JABALI_TEST_DATABASE_URL |
Real MariaDB DSN for integration tests | (unset) |
JABALI_LOG_LEVEL |
Slog level (debug / info / warn / error) | info |
TLS_CERT / TLS_KEY |
Cleaned from panel.env on update — nginx terminates | (auto-cleaned) |
- Panel never runs as root. Every privileged op crosses the agent Unix
socket as a typed RPC call; agent verifies caller via
SO_PEERCRED - Shell arguments validated + escaped per-handler (no
sh -c $argpatterns); domain names validated againstvalidateDomainNameForShell - DKIM private keys + SSO tokens + mailbox plaintexts encrypted at rest
via AES-GCM with a per-host SSO key (
/etc/jabali-panel/sso.key) - One-time admin SSO tokens are 256-bit, single-use, 5-minute TTL, reaped every 30s by systemd timer (ADR-0040 webmail SSO file pattern)
- Stalwart Certificate object pushed from LE-renewed cert on each certbot deploy-hook — IMAPS / 465 / 587 always serve browser-trusted cert (no rcgen self-signed fallback)
- CrowdSec AppSec WAF + per-user egress firewall (cgroupv2-vmap, ADR-0084)
- Self-healing reconciler — config drift on disk is reverted on next tick; operator hand-edits to nginx vhosts are lost-by-design
- CSP, HSTS, SameSite cookies, X-Forwarded-Proto handled by nginx
- Migrations are schema-only (no app-populated tables seeded by SQL)
- Audit log on every admin write + impersonation start/stop
- Pre-commit + CI gates:
go vet,go test -race ./...,npx tsc -b,bash -n install.sh, Playwright E2E, AppSec geoblock golden tests
Update the panel (code, dependencies, DB migrations, infrastructure):
jabali update
This pulls the latest code, rebuilds the panel + agent binaries, rebuilds the SPA, applies golang-migrate migrations, syncs nginx vhosts + systemd units + PHP config + CrowdSec acquis, and restarts the panel + agent. Safe to run on a live server — the reconciler tolerates a brief panel restart and converges state on the next tick.
Self-heal a broken install (7 detectors, --diagnose default, --auto safe, --all --yes destructive):
jabali repair --diagnose # report only
jabali repair --auto # fix safe issues
jabali repair --all --yes # destructive recovery
The jabali command uses a noun:verb pattern. All commands support
--json for machine-readable output and --yes to skip confirmations.
jabali user list|create|delete|show|password|suspend|unsuspend|admin
jabali domain list|create|delete|show|enable|disable|email-enable|email-disable
jabali db list|create|delete|users|user-create|user-delete|tune|root-password
jabali mailbox list|create|delete|passwd|set-quota|forwarder|autoresponder|shares
jabali ssl list|status|check|issue|renew|panel|panel-issue
jabali dns list|records|add|delete-record|sync|dnssec-enable|dnssec-disable
jabali backup list|create|delete|info|restore|password|destinations|schedules
jabali cron list|create|delete|toggle|run
jabali php list|install|uninstall|default|extensions
jabali service list|status|start|stop|restart|enable|disable
jabali system info|status|disk|memory|hostname|kill
jabali wp list|install|delete|update|scan|import
jabali agent ping|status|restart|log
jabali cpanel analyze|restore|fix-permissions
jabali login token [--user=] [--ttl=15] [--panel=]
jabali logs share [--raw] [--ttl=86400]
jabali ufw migrate-ip-bans # M43 CrowdSec single IP-trust
jabali repair --diagnose|--auto|--all
jabali panel-primary set|show # ADR-0048 primary mail domain
jabali nspawn list|build|update|delete
jabali malware-purge # M33 retention sweep
jabali update [--force]
See docs/CONVENTIONS.md for the full repo-wide
patterns (route families, SearchableTable, Drawer-for-CRUD, list envelope,
rate limits) and docs/adr/ for every load-bearing decision.
make build # compile panel-api + panel-agent
make run # run panel-api (dev, embedded SPA)
make test # all Go tests, race detector on
make test-coverage # coverage report (internal packages)
make test-integration # needs JABALI_TEST_DATABASE_URL + real MariaDB
make coverage-check # fail if combined coverage < 80%
make lint # golangci-lint v2
make fmt # go fmt + vet
Frontend dev (from panel-ui/):
npm install
npm run dev # Vite on http://localhost:5173
# proxies /api and /health to 127.0.0.1:8443
E2E (from panel-ui/):
npm run test:e2e # Playwright against the dev server
See docs/CONTRIBUTING.md for the full feature
development workflow (research → plan → TDD → review → ship).
The version string lives in VERSION (read at build time and exposed
via /health). When the installer clones the repo for a fresh install,
it reads VERSION to display the installed version. Always bump VERSION
in the same commit as the corresponding install.sh changes — drift
shows up as a mismatched footer and installer banner.
jabali-panel/
├── panel-api/ # Go HTTP server (Gin) + reconciler + agent RPC client
│ ├── cmd/server/ # main entry
│ ├── internal/ # api/, auth/, repository/, reconciler/, config/, ...
│ └── migrations/ # golang-migrate SQL (000xxx_*.up/.down.sql)
├── panel-agent/ # Go binary running as root; typed NDJSON RPC handlers
│ ├── cmd/jabali-agent/
│ └── internal/commands/
├── panel-ui/ # React SPA (AntD + TanStack Query)
│ ├── src/ # shells/, components/, theme/, pages/, ...
│ └── public/
├── agentwire/ # NDJSON RPC types shared by panel-api + panel-agent
├── internal/ # shared Go libs (cronvalidate, dbtuning, phpext, ...)
├── install/ # install.sh assets (nginx tmpl, stalwart plan,
│ # letsencrypt deploy hooks, bulwark env, ...)
├── docs/ # CONVENTIONS, BLUEPRINT, adr/, runbooks/, KNOWN_ISSUES
├── plans/ # per-milestone implementation blueprints
├── .gitea/workflows/ # CI (Go + vitest + E2E)
├── install.sh # single-supported install path (curl | sudo bash)
├── config.example.toml # reference config (copied to /etc/jabali-panel/)
├── Makefile # build / test / lint targets
└── go.mod # Go workspace root
AGPL-3.0 — see LICENSE.
Visiting mail.<your-domain> in a browser routes to webmail (Bulwark) via
the per-domain nginx vhost. The vhost installs an nginx sub_filter that
rewrites the panel hostname to the requested $host in Bulwark's
/api/config and Stalwart's /.well-known/jmap responses, so the SPA
stays same-origin on mail.<tenant> and Stalwart's JMAP Session URLs
never leak the panel hostname.
autodiscover / autoconfig paths are excluded so mail client
auto-discovery (Thunderbird, Outlook) keeps working.
See the docs/ directory for detailed guides:
- Conventions — repo-wide patterns (route families, SearchableTable, Drawer for create+edit, icon shim, list envelope, rate limits) + anti-patterns learnt the hard way
- Blueprint — full feature map + milestone roadmap
- ADRs — every load-bearing architectural decision (110+)
- Plans — per-milestone implementation blueprints
- Runbooks — operational guides for SSL, mail, M16 rollback, M22 SSO rework, M27 CrowdSec extensions, M30/M30.1 backups
- Known Issues — caveats + workarounds
- Contributing — feature development workflow
- Environment — full env-var reference