Docker Apps
Last updated
Curated marketplace of ~45 self-hosted apps. Every install is a docker compose stack, rendered from a versioned catalog entry, hardened for tenants, and wired into the panel’s backup and reconciler.
Catalog
/jabali-admin/docker-apps/catalog (admin) or /jabali-panel/docker-apps/catalog (tenant, when enabled).
Files & sync
Notes & docs
Media
Analytics
Business
Communication & community
Development & automation
Passwords & identity
AI & search
Feeds
The full slug list lives in install/docker-apps/ — one directory per app with app.yaml + compose.yml.tmpl + icon.svg.
Install pipeline
- Catalog lookup — validate slug, load the catalog entry (image digest pin, ports, env, tenant caps, volume owner).
- Port allocation — auto-assign host ports subject to the
(bind_interface, host_port, protocol)uniqueness constraint. Loopback by default. - Env materialization — merge catalog defaults with any operator overrides. Fields marked
secret: trueare generated (password32/password64) at install time. - Domain wiring — for loopback-bound apps with
reverse_proxy: truethe panel creates a domain withmanaged_by='docker_app'and installs an nginx vhost with SSL. - Compose render — the agent runs
text/templateovercompose.yml.tmplwith the render params and writes/var/lib/jabali/docker-apps/<instance_slug>/compose.yml. docker compose up --wait— the agent starts the stack and waits on the healthcheck. On failure the install row moves tofailedwithlast_errorpopulated; on success it moves torunning.
CPU limits declared in the catalog (e.g. "2.0") are clamped to the host’s CPU count so Docker doesn’t reject the compose file on a single-vCPU box.
Isolation & hardening
Tenant installs get a fixed hardening pass on top of the admin-neutral compose:
security_opt: [no-new-privileges:true]cap_drop: [ALL]+ a minimalcap_addfromtenant_capsin the catalog (baselineCHOWN,SETUID,SETGIDfor su-exec/gosu images).cgroup_parent: jabali-user-<username>.slice— pins the app under the tenant’s M18 slice so CPU/mem accounting rolls up per user.pids_limitper service.- Docker daemon must run with
--userns-remapenabled. The agent runs adocker.tenant_healthheartbeat every reconcile tick; if remap is removed at runtime the agent deletes/etc/jabali/docker-tenant-enabledand the tenant routes start failing closed.
The tenantForbiddenCaps blocklist keeps SYS_ADMIN, SYS_PTRACE, NET_ADMIN, and friends out reach even if a catalog entry asks for them.
Updates
Every ~6 hours the panel pulls the image digest for each install into available_digest. When it differs from image_sha, the panel shows “update available”.
Manual update — POST /admin/docker-apps/:id/update:
- Restic snapshot tagged
reason:pre-update. - Status →
updating, agent runsdocker_app.update. - On failure the panel restores from the pre-update snapshot (
rolling_back→failed). - On success the row records the new
image_shaand returns torunning.
Auto-update lives behind update_mode = auto per install (staged for the auto-update timer).
Backups
The panel snapshots /var/lib/jabali/docker-apps/<instance_slug>/ — volumes and rendered config — through restic. Snapshots are tagged docker-app, panel-managed, slug:<slug>, and reason:manual|pre-update.
POST /docker-apps/:id/backup— create a snapshot on demand.GET /docker-apps/:id/backups— list snapshots for the install.
Per-install backup_destination_id points at an entry in the M30 backup destinations table. The JABALI_RESTIC_REPO and JABALI_RESTIC_PASSWORD env vars on jabali-agent.service act as the fallback repository.
Admin controls
/jabali-admin/docker-apps:
| Setting | Where | Effect |
|---|---|---|
docker_marketplace_enabled | server settings | Master switch. When off, all /admin/docker-apps and /docker-apps routes return 503. |
docker_apps_for_users_enabled | server settings | Enables the tenant catalog. Off by default. |
docker_tenant_apps | server settings | CSV allowlist of tenant-visible slugs. Empty = every tenant_installable catalog entry. |
hosting_packages.docker_app_slugs | per package | Per-package allowlist. Empty = fall back to the server allowlist above. |
hosting_packages.max_docker_apps | per package | Count entitlement. Reconciler stops installs beyond the cap and never restarts them until raised. |
Per-install, admins can override CPU/memory/PID limits, override env vars, regenerate secrets, stream logs, exec into containers, and rebuild the stack. None of that is exposed to tenants (ADR-0117 Decision 8).
Tenant flow
/jabali-panel/docker-apps when both docker_apps_for_users_enabled and /etc/jabali/docker-tenant-enabled are true.
- Catalog is filtered to
tenant_installable: trueentries that survive the package + server allowlist cascade. - Only loopback-bound apps show up —
default_bind: publicdisqualifies at list time. - Users can install, start/stop/restart, delete (
?keep_volumes=1optional), view logs, and read env vars with secrets redacted. - No
exec, no raw compose editing, no port rebinding.
CLI
jabali docker-app catalog
jabali docker-app install --slug immich --name photos
jabali docker-app list
jabali docker-app status <id>
jabali docker-app start|stop|restart <id>
jabali docker-app update <id>
jabali docker-app backups <id>
jabali docker-app parity # audit db vs docker reality
jabali docker-app engine status # daemon + userns-remap + storage driver
Tenant-scoped mirror commands live under jabali docker-app tenant ….
Caveats
- Stuck watchdog — install/update rows sit in
installingorupdatingfor at most 15 minutes; the reconciler flips them tofailedafter that. - Volume ownership — images that expect a non-root UID (Gitea 1000:1000, NextCloud 33:33, Linkwarden 1000:1000) declare
volume_ownerin the catalog so the agent pre-chowns the bind mounts. - SMTP secrets — operator-supplied SMTP passwords containing
$,#,:,"are JSON-marshalled and$$-doubled during render to survive compose interpolation. - Stops don’t reclaim disk — bind-mount volumes persist across stop; only delete frees space.
- Catalog is loaded at boot — edits to
install/docker-apps/need a panel-api restart to take effect. - Multi-install — the
instance_slug(e.g.nextcloud-prod,nextcloud-staging) lets you install the same catalog entry twice with distinct paths and container names.
See also: Applications (PHP apps), Python Apps, Backups, Hosting Packages.