ozpanel sovereign hosting
Get ozpanel

Operator & API reference

Web hosting and Caddy

Site modes, every per-site option, global Caddy options, certificates, PHP, logs, checks.

How it works

  • CaddyBundled in the release, fixed module list. Every node serves every site.
  • RecordsTable caddy_vhosts, replicated. Each node renders /etc/caddy/sites-enabled/<domain>.caddy.
  • ChangeValidate → save → render if different → reload. Caddy rejects: old file restored, API 409 with Caddy's error.
  • Reload gateHash of every loaded file and cert. No change, no reload. Graceful.
  • Hand editsOverwritten. No raw-directive field. GET /caddy/raw shows generated files.
  • WhoSame options form for tenant and operator. Operator alone: global options, app mode, port approvals.
Tenant · Websites. — ozpanel screenshot
Tenant · Websites.

Site modes

ModeServesWho
phpOwn FPM pool + static filestenant, operator
staticDocument root, optional SPA fallbacktenant, operator
proxy (default)One or more upstreamstenant, operator
appStatic build + path routes to backends, one hostnameAPI, operator
FieldValuesDefaultNotes
domainhostnamerequiredRegistered domain in caller's scope
upstreamhost:port, space-separatedSeveral = load-balanced
rootblank, relative, absolute<home>/<domain>/public_htmlMust stay inside the home
tls_modeauto · internal · offautoACME · Caddy CA · HTTP only
optsobjectPUT replaces the whole object
confirm_internalboolfalseSuper-admin: allow a protected proxy target

New site defaults: compression on · log_level requests · forward_real_ip on for proxy · spa_fallback on for app. Every response: X-Served-By: <node>.

Per-site options

Fields of opts. Tenant and operator. An option whose module is missing is stored, not rendered.

Names, redirects, paths

OptionValuesDefaultNotes
aliaseshostnameswww on provisioned sitesOne certificate covers all
redirect_wwwbooloffwww ↔ apex, permanent, either direction
redirects[]from, to, code permanent|temporarypermanent301 / 302
rewrites[]from, toInternal; URL unchanged
deny_paths[]paths or bare names404; .env matched anywhere
index_filesnamesCaddy default
browsebooloffDirectory listing
spa_fallbackboolon for apptry_files {path} /index.html
app_routes[]path, upstreamApp mode, e.g. /api/* → 127.0.0.1:3000
error_pagefile in rootAll errors, original status
error_messagetextWhen no error page
max_body_size25MB, 1GBunsetUpload size; PHP limits apply separately

Compression, cache, headers

OptionValuesDefaultNotes
compressionboolonzstd, gzip
brotlibooloffbr, zstd, gzip
cache / cache_ttlbool / durationoffEdge cache (Souin cache-handler)
cache_rules[]extensions, max_ageCache-Control: public, max-age; 0 = no-store
security_headersbooloffHSTS 1 year, nosniff, SAMEORIGIN, strict-origin-when-cross-origin
headers[]op set|add|remove, name, valueUnknown op = set

Access control

OptionValuesDefaultNotes
auth_areas[]path, users[] name+passwordBasic auth, bcrypt, write-only. / = whole site.
ip_mode / ip_listallow|deny / IPs, CIDRs, private_rangesoff403
geo / geo_mode / geo_countriesbool / block|allow / ISO codesoff / blockMaxMind database in global options
wafbooloffCoraza + OWASP CRS
mtls / mtls_modebool / request, require, verify_if_given, require_and_verifyoff / require_and_verifyClient certs from the site's panel CA

Rate limits

OptionValuesDefault
rate_limitbool, master switchoff
rate_ip / rate_ip_requests / rate_ip_windowper client IPon if no scope / required / 1m
rate_site / rate_site_requests / rate_site_windowwhole siteoff / required / 1m

Counters shared cluster-wide (distributed).

Proxy

OptionValuesDefault
forward_real_ipbool; X-Real-IP (X-Forwarded-For always)on
trusted_proxiesprivate_ranges or CIDRs
streamingbool; flush -1 for SSEoff
keepalive / dial_timeout / response_timeoutdurationsCaddy
lb_policyround_robin, least_conn, ip_hash, random, firstCaddy
lb_try_durationduration
fail_duration / max_failspassive health
health_uri / _interval / _timeout / _statusactive health; status 200 or 2xxCaddy

WebSockets: no option needed.

Logging

log_levelRecords
offNothing
requests (default)Time, client, method, URI, status, size, timing
headersPlus headers, unredacted (cookies, Authorization)
bodiesPlus bodies, time-limited capture
captureDefaultMax
request / responsetrue / falseOne must be on
methodsPOST PUT PATCH DELETEPlus GET HEAD OPTIONS
max_kb641024
untilnow + 24 hnow + 7 days
retention_hours24168

Log /var/log/caddy/<domain>.log, 50 MiB × 5. Captures hourly .jsonl. Search across nodes:

GET /caddy/vhosts/example.com/logs?kind=requests&status=4xx&path=/login&ip=203.0.113.10&since=…&limit=200

Client certificates (mTLS)

  1. POST /caddy/vhosts/{domain}/mtls/ca: org, validity_days (3650), force.
  2. POST …/mtls/clients: common_name, email, validity_days (365). PEM + PKCS#12 returned once; key not stored.
  3. Set mtls on, pick mtls_mode.
  4. Revoke: POST …/mtls/clients/{serial}/revoke. Leaf allowlist; effective at once.

Global Caddy options

Operator only. GET/PUT /caddy/settings or Caddy → Settings. Validated with caddy validate, backed up, reloaded, restored on failure, then rendered by every node.

SettingValuesDefault
emailACME contacthostmaster address
acme_caLet's Encrypt, LE staging, ZeroSSLLet's Encrypt
local_certsbool; all sites internal CAoff
http_port / https_port1–6553580 / 443
log_levelDEBUG, INFO, WARN, ERRORINFO
grace_perioddurationunset
storageLocal disk, Redis (Sentinel); read-only hereLocal disk
storage_rootpath/mnt/cluster/caddy on shared volume
geo_db_path / geo_accountpath / MaxMind ID/var/lib/caddy/GeoLite2-Country.mmdb
geo_api_keycluster secret (Cluster → Secrets)unset
http3bool; needs UDP 443off
idle_timeout / read_header_timeout / write_timeoutdurationsCaddy
metricsbool, per host; bandwidth needs iton

Modules

Redis storage · ratelimit · cache-handler (Souin) · Coraza · Brotli · geo-ip · layer4 · ozcapture. CrowdSec bouncer and replace-response compiled in, unused. No ACME DNS-provider module. GET /caddy/plugins.

Operator · Caddy settings. — ozpanel screenshot
Operator · Caddy settings.

Certificates

  • autoACME http-01; port 80 reachable. Held on an internal cert until DNS points at the cluster; recheck every 2 min (POST /caddy/tls/recheck, GET /caddy/tls/audit).
  • SharedRedis via Sentinel (one copy, one issuance per name, lock) or /mnt/cluster/caddy. Node-local disk: single node only.
  • Switch to RedisPOST /caddy/storage/redis/switch: copy, byte compare, record, roll. No re-issue.
  • Other services/var/lib/ozpanel/certs/<name>/fullchain.pem, privkey.pem, info.json for Postfix, Dovecot, panel.
  • InventoryGET /certs, POST /certs/sync, POST /certs/import (records a PEM; not served as a site cert). Health fails on expired public certs; warns at 14 days.
  • mail.<domain>Platform site per mail domain for the mail cert; redirects browsers.
  • FailoverStorage follows Sentinel; local mirror if Redis is down at start; SNI probe restarts a stuck Caddy (5 min cooldown).
  • Not availableCustom certificate per site · wildcard · DNS-01.

Domains and pages

Parent isKindCounts against
Same account's domainsubdomain, parent's zonesubdomains
Another account'srefused
Not on the platformapex, own zonedomains
  • Auto wwwRedirect block added when the zone publishes www at the same addresses.
  • Welcome pageWritten once into an empty root. GET/PUT/DELETE /caddy/welcome: brand_name, brand_url, headline, body, template. POST …/preview, …/rerender (signed pages only; dry run unless apply).
  • SuspendedEvery site answers 503, no-store. Files untouched.
  • DeleteSite: record, file, pool, client CA. Domain: plus logs, cert records.
Tenant · Domain. — ozpanel screenshot
Tenant · Domain.

Proxy target rules

TargetResult
Public host, private 10/8, 172.16/12, 192.168/16Allowed
Loopback on the account's own app portAllowed
Other loopback, mesh 100.64/10, 169.254/16, cluster nodes, unresolvableRefused (super-admin: 409, retry with confirm_internal)

PHP

One PHP version installed (PHP-FPM 8.4). Per-site PHP version: not available. GET /phpfpm/versions.

  • PoolOne per site, account's user, own FPM master in the account's cgroup slice. ondemand, idle 30 s, 500 requests, max_children = plan php_workers (10, max 200).
  • Lockedopen_basedir home · per-account temp and session dirs · disable_functions exec, passthru, shell_exec, system, proc_*, popen, pcntl_exec, pcntl_fork, dl.
SettingDefaultCeilingCapped by plan
memory_limit256 MB2048php_memory_mb
upload_max_filesize, post_max_size64 MB1024php_upload_mb
max_execution_time30 s300php_max_execution
max_input_time60 s300
max_input_vars100010000
session.gc_maxlifetime1440 s86400
date.timezone, display_errorsUTC, Off

Tenant and operator: GET/PUT /phpfpm/pools/{domain}/php (requested · ceiling · enforced). Capped settings are php_admin_value. Operator defaults: GET/PUT /phpfpm/settings (memory, times, input vars, upload, OPcache).

Checks

POST /caddy/vhosts/{domain}/verify: "is this site working?" Verdict ok · warn · critical.

CheckFails when
dnsNot resolving here
caddyNot in running config (reload rejected)
upstreamNothing listening (502)
rootDocument root missing
tls / tls_flagNo handshake, wrong name, expired, internal on auto; hold disagrees with DNS
access logNot writable by Caddy

Also: GET /caddy/verify, /caddy/health, /caddy/upstreams, /caddy/live, /caddy/config, /caddy/metrics. Alerts: tls_selfsigned_served, tls_unknown.

Bandwidth

Month to date, all nodes: Caddy per-host metrics + nftables counters on published ports + Postfix bytes. Not counted: SFTP, DNS, IMAP/POP3, panel. bandwidth_gb_month reported, not capped. GET /stats/bandwidth/coverage, /tenants/{id}/usage.

Routes for this area: API reference → route tables.

Previous: The cluster Next: Apps and containers