ozpanel sovereign hosting
Get ozpanel

Operator & API reference

Apps and containers

Process and container apps, every field, placement, interlock, ports, storage, images, limits.

Overview

A long-running program owned by one tenant. Defined once, cluster-wide; each node starts or fences it by placement.

processcontainer
RunsCommand as the tenant's Linux userOCI image on Podman (rootless userns)
Unitozapp-<name>.serviceozapp-t<uid>-<name>.service → podman run --rm
Address127.0.0.1:<port>Podman bridge 10.88.0.0/16, per node
PortsNode's namespace; listen_ports policyOwn namespace
Plan gatenonefeature container_apps, limit containers

Who: tenant (Apps screen) and operator. Runtime, name and domain fixed after creation.

Fields

POST /api/v1/apps
{"runtime":"container","name":"web","domain":"app.example.com",
 "image":"ghcr.io/example/app:1.4.2","port":8080,"env":{"NODE_ENV":"production"}}
FieldValuesDefaultNotes
runtimeprocess · containerprocessFixed
name[a-z0-9][a-z0-9-]{1,40}requiredUnique per tenant
domainowned domainnoneApp owns route, DNS, cert. Workers need none.
port1–65535required (container)Internal port; exported as PORT
commandone linerequired (process)Container: args after the image
envNAME → valueemptyStored with the app; no separate secret store
enabledbooltruefalse stops on every node
ha_modesingleton · replicatedsingletonSee placement
nodesnode namessingleton: receiving node · replicated: allNode allow-list
working_dirpath in home<home>/<domain>Process only
imagerepo:tagrequiredShort names → Docker Hub. Run by pinned digest.
auto_updateboolfalseFollow the tag hourly; else pinned digest
entrypointone program, ≤255 charsimage's
run_username or uid[:gid]image's
mem_max_mbMBplan container_memory_mbMemory cap; clamped to plan
scratch_mbMB256tmpfs /tmp and /scratch
mount_appbooltrue<home>/apps/<name> ro at /app
mounts≤20 × {source,target,readonly}Persistent folders from the home; ro optional
writable_rootboolfalseRead-only root by default
browserboolfalseHeadless browser: $HOME and /dev/shm 512 MB
  • FixedRestart on failure (5 s container, 3 s process; 5 in 300 s then stop) · start at boot after storage · stop 20 s then SIGKILL.
  • Not availablePer-app CPU limit · container health checks (use the site's health_uri) · restart policy choice · registry credentials · privileged, devices, added caps, image builds.
  • UpdatePUT /apps/{name} replaces the whole definition. New image clears the pin.
  • DeleteUnit, container, route, DNS, ports. Home files kept.
  • DatabaseDB_HOST=ozpanel-db, port 3306: the local node's MariaDB in every container. Same data after a move.

Placement

Shapeha_modenodesNode stopsUse for
PinnedsingletononeDown until back or movedMust stay on one machine
Interlockedsingleton2+Another candidate takes overWrites files
Replicatedreplicated1+ or empty (all)Others keep servingStateless, state in DB
POST /apps/web/placement {"nodes":["node1","node2"],"ha_mode":"singleton"}
POST /apps/web/run-on    {"node":"node2"}
  • MoveStop before start: freeze lease, old nodes confirm stop, then write. Unconfirmed: 409, nothing moves.
  • FenceNon-candidate: unit stopped, container and route removed.
  • ReplicatedWritable mounts refused.
  • No handbackMove back with run-on.

Interlock and failover

TimingValue
Lease renew10 s
Holder self-fence30 s without renewal
Earliest takeover35 s
Silent-but-up holder, takeover refused45 s (interlock_conflict)
Planned move, measured, incl. DNS and HTTPS17 s

Start chain: placement · no move in progress · lease held/free/stale · database in quorum · old holder confirmed stopped or unreachable everywhere · storage writable · ozpanel app-gate. No quorum: app down everywhere, recovers alone. Standbys preload the pinned image. Route and DNS follow the lease holder once running. Alert interlock_double_run.

Public ports

Tenant requests; operator approves and picks the method. Opens only on the node running the app.

POST /apps/pbx/ports {"proto":"udp","host_port":5060,"container_port":5060,"purpose":"SIP"}
POST /app-port-requests/{id}/approve {"method":"netavark_dnat"}
MethodDoesHost port
http_vhostExtra hostname to another container portshared 443
caddy_sniTLS passthrough by SNIshared 443
caddy_l4Raw TCP/UDP, Caddy layer4exclusive
caddy_l4_tlsTCP, TLS terminated with the managed certexclusive
netavark_dnatPodman forward; ranges allowedexclusive

Request: proto (tcp), host_port[_end], container_port[_end], purpose, suggested_method, vhost_domain. Approve: method, bind_addr (default public address, never all), host_port, vhost_domain, reason. Overlaps refused.

Container storage

PathBacked byPersists
/app<home>/apps/<name>, royes
/data<home>/apps/<name>/datayes
Image volumes…/volumes/<path>, seeded onceyes
mountshome paths; targets /, /app, /data, /tmp, /scratch, /proc, /sys, /dev refusedyes
/tmp, /scratch, $HOME, /dev/shmtmpfsno

Homes are on shared storage, every node. No storage: no start (ozpanel app-ready, 120 s), running container stopped, alert apps_held.

Images

  • One digestPinned cluster-wide at first start. No digest available: no start on another version.
  • StagedExported to the shared volume for standbys; no registry needed for takeover.
  • Check / updateGET /apps/{name}/image · POST …/image {"apply":false} check · {"apply":true} pin and roll.
  • RegistriesAnonymous only. POST /podman/prune removes unused images.

Sandbox and limits

ControlValue
Capabilities--cap-drop=ALL, no-new-privileges
IdentityRootless userns into the tenant's id range
Root filesystemRead-only unless writable_root
NetworkIPv4, shared bridge, no host network, no host ports without approval
Tenant slice ozpanel-tenants-t<uid>.sliceprocesses → TasksMax · memory_mb → MemoryHigh (throttle) or MemoryMax (hard) · cpu_percent → CPUQuota. Shared by sites, cron, apps.
Disk I/Oio_mb_hour measured, not capped
Per containercontainer_memory_mb; kernel kills the container, not the node
Process appsNode --max-old-space-size / Go GOMEMLIMIT at 80%, else LimitAS
Outbound policyoutbound_default allow · deny_ports 25, 465, 587 · deny_cidrs private, mesh, metadata · allow_* · listen_ports. Process apps only.

Recipes

Prepared app plus config and port requests. Tenant UI presets: waha, redis, whoami. Server-side: asterisk.

GET  /recipes
POST /tenants/{id}/recipes/{key}/instantiate {"name","variant","domain","params"}

Operating

  • Actionsstart · stop (transient) · restart · enable · disable · update image · inspect · events · live logs · shell in a new tab · relink · move.
  • StatusPer node: running · stopped · failed · fenced · stray · unknown (2 min silent; never counted down).
  • Eventsstarted · stopped · fenced · restart_loop · stray · ip_changed · image_changed · held/released · fence_stuck · interlock_conflict · probe_failed. Ack per event.
  • LogsGET /apps/{name}/logs?lines=200; live SSE at /apps/{name}/logs/stream on the running node.

Routes for this area: API reference → route tables.

Previous: Web hosting and Caddy Next: Mail