Overview
A long-running program owned by one tenant. Defined once, cluster-wide; each node starts or fences it by placement.
| process | container | |
|---|---|---|
| Runs | Command as the tenant's Linux user | OCI image on Podman (rootless userns) |
| Unit | ozapp-<name>.service | ozapp-t<uid>-<name>.service → podman run --rm |
| Address | 127.0.0.1:<port> | Podman bridge 10.88.0.0/16, per node |
| Ports | Node's namespace; listen_ports policy | Own namespace |
| Plan gate | none | feature 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"}}
| Field | Values | Default | Notes |
|---|---|---|---|
runtime | process · container | process | Fixed |
name | [a-z0-9][a-z0-9-]{1,40} | required | Unique per tenant |
domain | owned domain | none | App owns route, DNS, cert. Workers need none. |
port | 1–65535 | required (container) | Internal port; exported as PORT |
command | one line | required (process) | Container: args after the image |
env | NAME → value | empty | Stored with the app; no separate secret store |
enabled | bool | true | false stops on every node |
ha_mode | singleton · replicated | singleton | See placement |
nodes | node names | singleton: receiving node · replicated: all | Node allow-list |
working_dir | path in home | <home>/<domain> | Process only |
image | repo:tag | required | Short names → Docker Hub. Run by pinned digest. |
auto_update | bool | false | Follow the tag hourly; else pinned digest |
entrypoint | one program, ≤255 chars | image's | |
run_user | name or uid[:gid] | image's | |
mem_max_mb | MB | plan container_memory_mb | Memory cap; clamped to plan |
scratch_mb | MB | 256 | tmpfs /tmp and /scratch |
mount_app | bool | true | <home>/apps/<name> ro at /app |
mounts | ≤20 × {source,target,readonly} | Persistent folders from the home; ro optional | |
writable_root | bool | false | Read-only root by default |
browser | bool | false | Headless 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. - Update
PUT /apps/{name}replaces the whole definition. New image clears the pin. - DeleteUnit, container, route, DNS, ports. Home files kept.
- Database
DB_HOST=ozpanel-db, port 3306: the local node's MariaDB in every container. Same data after a move.
Placement
| Shape | ha_mode | nodes | Node stops | Use for |
|---|---|---|---|---|
| Pinned | singleton | one | Down until back or moved | Must stay on one machine |
| Interlocked | singleton | 2+ | Another candidate takes over | Writes files |
| Replicated | replicated | 1+ or empty (all) | Others keep serving | Stateless, 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
| Timing | Value |
|---|---|
| Lease renew | 10 s |
| Holder self-fence | 30 s without renewal |
| Earliest takeover | 35 s |
| Silent-but-up holder, takeover refused | 45 s (interlock_conflict) |
| Planned move, measured, incl. DNS and HTTPS | 17 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"}
| Method | Does | Host port |
|---|---|---|
http_vhost | Extra hostname to another container port | shared 443 |
caddy_sni | TLS passthrough by SNI | shared 443 |
caddy_l4 | Raw TCP/UDP, Caddy layer4 | exclusive |
caddy_l4_tls | TCP, TLS terminated with the managed cert | exclusive |
netavark_dnat | Podman forward; ranges allowed | exclusive |
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
| Path | Backed by | Persists |
|---|---|---|
/app | <home>/apps/<name>, ro | yes |
/data | <home>/apps/<name>/data | yes |
| Image volumes | …/volumes/<path>, seeded once | yes |
mounts | home paths; targets /, /app, /data, /tmp, /scratch, /proc, /sys, /dev refused | yes |
/tmp, /scratch, $HOME, /dev/shm | tmpfs | no |
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 / update
GET /apps/{name}/image·POST …/image {"apply":false}check ·{"apply":true}pin and roll. - RegistriesAnonymous only.
POST /podman/pruneremoves unused images.
Sandbox and limits
| Control | Value |
|---|---|
| Capabilities | --cap-drop=ALL, no-new-privileges |
| Identity | Rootless userns into the tenant's id range |
| Root filesystem | Read-only unless writable_root |
| Network | IPv4, shared bridge, no host network, no host ports without approval |
Tenant slice ozpanel-tenants-t<uid>.slice | processes → TasksMax · memory_mb → MemoryHigh (throttle) or MemoryMax (hard) · cpu_percent → CPUQuota. Shared by sites, cron, apps. |
| Disk I/O | io_mb_hour measured, not capped |
| Per container | container_memory_mb; kernel kills the container, not the node |
| Process apps | Node --max-old-space-size / Go GOMEMLIMIT at 80%, else LimitAS |
| Outbound policy | outbound_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.
- Logs
GET /apps/{name}/logs?lines=200; live SSE at/apps/{name}/logs/streamon the running node.
Routes for this area: API reference → route tables.