Tenants
A tenant is a real Linux user, ozt<uid>, same uid on every node. uids from 100000, never reused.
| Asset | Where |
|---|---|
| Home | /home/ozt<uid> on shared storage |
| PHP | Own FPM pool and master, as the tenant |
| Cron | /etc/cron.d/ozpanel-ozt<uid>, in the tenant slice |
| Databases | ozt<uid>_<name> |
| Processes | ozpanel-tenants-t<uid>.slice |
| Files, SFTP | Always as the tenant, never root |
POST /tenants {"name":"Example Ltd","plan_id":2,"contact_email":"admin@example.com","domains":["example.com"]}
| Field | Notes |
|---|---|
name, plan_id | Required; no plan = refused |
contact_email, contact_name | Contact; default first login |
domains | Initial domains, reported per domain |
login_email, login_password, no_login | First domain_admin login; password shown once (Argon2id) |
region_policy, cluster_mode | any; local · all · region · nodes |
Lifecycle
| Call | Effect |
|---|---|
PUT /tenants/{id}, /plan, /limits, /features, /clustering | Partial update, plan, overrides |
POST /tenants/{id}/suspend · /unsuspend | Sites 503, mail refused, logins and DB logins locked, cron stopped. Backups continue. No data touched. |
GET /tenants/{id}/teardown · DELETE /tenants/{id} | Everything, every node, incl. snapshots. Irreversible. |
POST /tenants/{id}/impersonate | Operator acts as the tenant; banner, attributed, no chaining. POST /auth/stop-impersonating. |
Billing hooks
Idempotent, keyed by domain: POST /provision/activate {domain,plan?,contact_email?,tenant?} · /suspend · /unsuspend · /cancel {delete} · GET /provision/status/{domain}. Domain provisioning: POST /domains/{d}/provision (DNS → web → mail). Templates: /templates.
Plans and limits
POST /plans {"name":"Starter","limits":{"domains":1,"mailboxes":5,"disk_mb":5120},
"features":{"shell_access":true},"template_id":1,"visible":true}
Effective value = plan, overridden per tenant; source shown. Missing or 0 = unlimited. Plan edits apply at once. Plan in use cannot be deleted.
| Feature | On |
|---|---|
shell_access | Chrooted SFTP (never a shell) |
container_apps | Container apps allowed |
external_forwarding | Mail forwards outside the platform allowed |
| Limit | Unit | Enforced |
|---|---|---|
domains, subdomains, mailboxes, mail_aliases, databases, db_users, cron_jobs, terminals, containers | count | Refused (409) |
disk_mb | MB | Hard JuiceFS quota, rounded up to GiB; uploads refused |
mail_quota_mb | MB | Mailbox allocation refused |
php_memory_mb, php_upload_mb, php_max_execution, php_workers | MB, s, count | PHP pool |
processes, memory_mb, cpu_percent | count, MB, % | Tenant slice (cgroup v2) |
io_mb_hour | MB/h | Measured |
email_per_hour, email_per_day | messages | Mail gateway, cluster-wide |
bandwidth_gb_month | GB | Measured |
container_memory_mb | MB | Per container |
api_req_min | /min | Not enforced |
Resource control
processes → TasksMax (fork refused) · memory_mb → MemoryHigh (throttle, default) or MemoryMax (hard) · cpu_percent → CPUQuota. Mode off · throttle · hard: PUT /resources/tenants/{id}. Coverage: GET /resources. Size plans for failover: all apps may land on one node.
Usage
ozpanel usage-collect every 15 min. Unmeasured ≠ 0. Over limit: reported, never auto-suspended. GET /stats/overview, /tenants/{id}/usage?days=30, /tenants/{id}/processes, /quota/verify.
Consoles and logins
| Role | Console | Scope |
|---|---|---|
super_admin | /admin/ | Platform |
domain_admin | /account/ | One tenant |
domain_user | webmail.<domain> / /ozpanel/ | One mailbox |
Tenant screens: Overview · Websites · Domain · DNS · Apps · Mail · Mail domain · Mail routing · Mailbox · Files · Databases & tasks · Backups · Notifications · Plan · Security. No reseller role. Out of scope answers "not found".
Logins: GET/POST /tenants/{id}/users {email,role,password?,mailbox?} · PUT …/{uid} {role,mailbox,status,unlock} · …/password · …/invite.
Files, SFTP, cron, terminal
Routes for this area: API reference → route tables.
Symlink escapes refused. Operator adds ?tenant=<id>.
- SFTPWith
shell_access: groupozsftp, chroot/home,internal-sftp, no forwarding.POST /tenants/{id}/sftp/password.sftp ozt100001@panel.example.com. - Cron
POST /tenants/{id}/cron {"schedule":"*/10 * * * *","command":"…"};activeon update. Runs on the primary only, in the slice, as the tenant. - Terminalttyd behind panel auth. Operator: host, container, tenant user. Tenant: own containers. 15 min default, 1 h max, 3 per user, 10 per node, plan
terminals.
Shared storage
JuiceFS on Garage. Every tenant home and /mnt/cluster on every node.
| Part | Detail |
|---|---|
| Data | Garage (S3, on the cluster's nodes), three copies on three nodes |
| Metadata | PostgreSQL (synchronous); Redis selectable |
| Quota | Hard per-tenant directory quota from disk_mb |
| Capacity | Real, replication-aware: GET /juicefs/capacity |
| Fence | ozpanel juicefs-mount-gate: mount only on a proven single metadata writer; seal after 3 bad passes |
| Integrity | POST /juicefs/fsck |
| Metadata backup | Hourly dump (48, then daily ×14), daily restore drill |
| Keys | POST /juicefs/s3-key/rotate; Garage S3 keys /garage/s3-keys |
Operator console: Garage/JuiceFS enable, fsck, metadata engine switch (POST /storage/juicefs/engine), Garage data dirs (/garage/data-dirs). Per tenant: GET /tenants/{id}/storage. Mount over a non-empty home: refused.
GlusterFS is retired cluster-wide. Switchable back by an administrator only: GET /storage/glusterfs/retirement, POST|DELETE /storage/glusterfs/retire, POST /storage/switch.
Backups
Snapshot per tenant: plain files, verifiable without ozpanel (tar, zstd, gzip, sha256).
<target>/<system_user>/<timestamp>/ manifest.json written last; parts, sizes, sha256, missing home.tar.zst whole home db-<name>.sql.gz one per MySQL / PostgreSQL database config.json domains, DNS, mailboxes (hashes), aliases, spam, vhosts, PHP, cron
Target kind | Destination | Off-site |
|---|---|---|
local | Path; default /var/backups/ozpanel | no |
sftp | user@host:/path, port, password; host key pinned; panel key pair | yes |
POST /backups/targets {"name":"offsite","kind":"sftp","destination":"backup@backup.example.com:/srv/backups","port":22}
POST /backups/schedules {"target_id":1,"cron":"17 3 * * *","keep":7,"what":"files,databases,config"}
POST /backups/run {"target_id":1,"files":true,"databases":true,"config":true}
POST /backups/snapshots/{id}/verify
POST /backups/snapshots/{id}/restore {"files":true,"databases":true,"config":true,"dry_run":true}
- WhoTenant self-service (targets, schedules, snapshots, restore) and operator (
?tenant=). - SchedulesSystem cron,
ozpanel backup-run --schedule N, on the primary.keep7; newest complete never removed. 6 h per run. - Verify firstRestore refuses a mismatched or partial snapshot. Dry run lists every line.
- RestoreFiles, dumps, settings in order; passwords verbatim; paths remapped to a re-created account. Not included: panel logins, certificates.
- Platform
/mnt/clusterarchived daily ×14 per node;POST /backups/platform/restore. - Fleet
GET /backups/status: last success, failures, tenants with none. - Not availableBackup encryption · S3 targets · PostgreSQL PITR.







