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

  1. Catalog lookup — validate slug, load the catalog entry (image digest pin, ports, env, tenant caps, volume owner).
  2. Port allocation — auto-assign host ports subject to the (bind_interface, host_port, protocol) uniqueness constraint. Loopback by default.
  3. Env materialization — merge catalog defaults with any operator overrides. Fields marked secret: true are generated (password32 / password64) at install time.
  4. Domain wiring — for loopback-bound apps with reverse_proxy: true the panel creates a domain with managed_by='docker_app' and installs an nginx vhost with SSL.
  5. Compose render — the agent runs text/template over compose.yml.tmpl with the render params and writes /var/lib/jabali/docker-apps/<instance_slug>/compose.yml.
  6. docker compose up --wait — the agent starts the stack and waits on the healthcheck. On failure the install row moves to failed with last_error populated; on success it moves to running.

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 minimal cap_add from tenant_caps in the catalog (baseline CHOWN, SETUID, SETGID for 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_limit per service.
  • Docker daemon must run with --userns-remap enabled. The agent runs a docker.tenant_health heartbeat every reconcile tick; if remap is removed at runtime the agent deletes /etc/jabali/docker-tenant-enabled and 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 updatePOST /admin/docker-apps/:id/update:

  1. Restic snapshot tagged reason:pre-update.
  2. Status → updating, agent runs docker_app.update.
  3. On failure the panel restores from the pre-update snapshot (rolling_backfailed).
  4. On success the row records the new image_sha and returns to running.

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:

SettingWhereEffect
docker_marketplace_enabledserver settingsMaster switch. When off, all /admin/docker-apps and /docker-apps routes return 503.
docker_apps_for_users_enabledserver settingsEnables the tenant catalog. Off by default.
docker_tenant_appsserver settingsCSV allowlist of tenant-visible slugs. Empty = every tenant_installable catalog entry.
hosting_packages.docker_app_slugsper packagePer-package allowlist. Empty = fall back to the server allowlist above.
hosting_packages.max_docker_appsper packageCount 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: true entries that survive the package + server allowlist cascade.
  • Only loopback-bound apps show up — default_bind: public disqualifies at list time.
  • Users can install, start/stop/restart, delete (?keep_volumes=1 optional), 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 installing or updating for at most 15 minutes; the reconciler flips them to failed after that.
  • Volume ownership — images that expect a non-root UID (Gitea 1000:1000, NextCloud 33:33, Linkwarden 1000:1000) declare volume_owner in 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.