One-click Docker apps: 54+ self-hosted stacks on Debian 13
Jabali Panel now installs 54+ self-hosted apps as hardened Docker Compose stacks — Nextcloud, Immich, Vaultwarden, Grafana, Gitea, WordPress, Odoo, and dozens more — with automatic SSL, backups, tenant isolation, and reconciler-driven updates.
The one thing Jabali Panel didn’t have a good answer for was: “I want to run Nextcloud + Immich + Vaultwarden on my server, and I don’t want to write compose files by hand.” As of this release, it does. Jabali Panel ships a Docker Apps marketplace — 54+ curated self-hosted apps, installed as Docker Compose stacks from a versioned catalog, hardened for multi-tenant use, and wired into the panel’s backup and reconciler pipeline.

Not a “docker exec into things” panel
The panel host itself doesn’t run in Docker — Jabali Panel needs systemd cgroup v2 and systemd-machined for its per-user isolation model, and neither works reliably in unprivileged containers. Docker is used for the apps, not for the panel.
That distinction matters because it changes what “one-click install” is actually doing. Every app is a docker compose stack rendered from a catalog entry (install/docker-apps/<slug>/compose.yml.tmpl) with pinned image digests, port allocation subject to a (bind_interface, host_port, protocol) uniqueness constraint, secret generation at install time (fields marked secret: true become password32 / password64), and volume owner pre-chowning for images that expect a non-root UID (Gitea 1000:1000, Nextcloud 33:33, Linkwarden 1000:1000).
The install pipeline runs in this order:
- Catalog lookup — validate slug, load image digest pin, ports, env, tenant caps.
- Port allocation — auto-assign host ports, loopback by default.
- Env materialization — merge catalog defaults with operator overrides; generate secrets.
- Domain wiring — for loopback-bound apps with
reverse_proxy: true, create a domain withmanaged_by='docker_app'and install an nginx vhost with Let’s Encrypt SSL. - Compose render —
text/templateovercompose.yml.tmpl→/var/lib/jabali/docker-apps/<instance_slug>/compose.yml. docker compose up --wait— start the stack and wait on the healthcheck.
Fail at any step, the install row moves to failed with last_error populated; the panel doesn’t leave zombie containers or half-configured domains.
The 54+ apps
The catalog groups apps by what people actually search for:
Files & sync — Nextcloud, ownCloud, Immich, Paperless-ngx, PrivateBin, Enclosed, Linkwarden
Notes & docs — DokuWiki, Joplin Server, Memos, DailyTxt, Mealie
Media — PeerTube, MediaCMS, RomM
Analytics & observability — Plausible, Matomo, Grafana
Business — Odoo, ERPNext, Bigcapital, InvoiceShelf, Snipe-IT, OpenEMR, ONLYOFFICE Docs, FOSSBilling
Communication & community — Rocket.Chat, phpBB, Flarum, Apache Answer
Development & automation — Gitea, Forgejo, n8n, Uptime Kuma
Passwords & identity — Vaultwarden, ZITADEL
AI & search — Open WebUI, SearXNG
Feeds — FreshRSS
The catalog also carries the PHP marketplace apps (WordPress, Drupal, Joomla, MediaWiki, Moodle, PrestaShop, OpenCart, ITFlow) for people who want them under the same install flow. And of course Jabali Sounder for fleet management.
Every catalog entry has a tooltip describing what services the stack runs (e.g. “Immich — four services: web server, ML inference, PostgreSQL with pgvecto.rs, Redis”) so you know what disk and CPU the install will actually consume before you click it.

Isolation, or: how tenants don’t own your box
Admin installs get a neutral compose. Tenant installs get a fixed hardening pass on top:
security_opt: [no-new-privileges:true]cap_drop: [ALL]+ a minimalcap_addfrom the catalog’stenant_caps(baselineCHOWN,SETUID,SETGIDfor su-exec / gosu images)cgroup_parent: jabali-user-<username>.slice— pins the app under the tenant’s per-user systemd slice so CPU and memory accounting rolls up per tenant, exactly like their PHP-FPM poolpids_limitper service
The Docker daemon must run with --userns-remap enabled. The agent runs a docker.tenant_health heartbeat every reconcile tick; if remap gets removed at runtime the agent deletes /etc/jabali/docker-tenant-enabled and tenant Docker routes start failing closed rather than exposing an unmapped container.
A tenantForbiddenCaps blocklist keeps SYS_ADMIN, SYS_PTRACE, NET_ADMIN, and friends unreachable even if a catalog entry asks for them — a catalog PR can’t accidentally hand a tenant kernel-adjacent power.
Updates that don’t require a maintenance window
Every ~6 hours the panel pulls the image digest for each install into available_digest. When it differs from the running image_sha, the install shows “update available” in the admin UI.
Manual update (POST /admin/docker-apps/:id/update) runs:
- Restic snapshot tagged
reason:pre-update. - Status →
updating, agent runsdocker_app.update. - On failure, 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 per-install update_mode = auto for people who want the auto-update timer to handle it.
Backups per install, not per host
/var/lib/jabali/docker-apps/<instance_slug>/ — volumes and rendered compose — is snapshotted through restic. Snapshots are tagged docker-app, panel-managed, slug:<slug>, and reason:manual|pre-update.
POST /docker-apps/:id/backup— snapshot on demandGET /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 are the fallback repository. Every update is preceded by a snapshot, so an update that breaks the app rolls back automatically instead of leaving you at docker compose down at 2am.
Admin knobs
/jabali-admin/docker-apps exposes:
| Setting | Where | Effect |
|---|---|---|
docker_marketplace_enabled | server settings | Master switch. Off → all /admin/docker-apps and /docker-apps routes 503. |
docker_apps_for_users_enabled | server settings | Turns on the tenant catalog. Off by default. |
docker_tenant_apps | server settings | CSV allowlist of tenant-visible slugs. Empty = every tenant_installable entry. |
hosting_packages.docker_app_slugs | per package | Per-package allowlist; falls back to the server allowlist. |
hosting_packages.max_docker_apps | per package | Count entitlement; reconciler stops installs beyond the cap. |
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.
CLI
Anything the UI does, the CLI does:
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 ….
What it doesn’t do
- No Docker for the panel itself. The panel host is Debian 13 on bare metal or KVM. Docker inside the panel process would break the systemd-machined per-user isolation the rest of the panel depends on.
- No arbitrary compose file uploads. Every install goes through a versioned catalog entry — you don’t paste YAML into a form. This is what lets tenant installs be safe by default and updates roll back to a known-good digest.
- No “run Kubernetes on the panel”. Different problem, different tool.
Get it
The Docker Apps marketplace lands in the current Jabali Panel release. It’s off by default — flip docker_marketplace_enabled in server settings to expose it in the admin UI, and docker_apps_for_users_enabled when you’re ready to hand tenants the catalog too.
The full app list, per-app service breakdown, and catalog schema lives in the Docker Apps documentation.
Frequently Asked Questions
- Does Jabali Panel now run apps in Docker?
- Yes. Jabali Panel ships a Docker Apps marketplace: 54+ curated self-hosted apps installed as Docker Compose stacks from a versioned catalog. Each install is a docker compose stack, rendered from a catalog entry, hardened for tenants, and wired into the panel's backup and reconciler pipeline. The panel host itself still runs on bare metal or a KVM/QEMU VM — Docker is used for the apps, not for the panel.
- How many apps does the Docker marketplace include?
- 54+ apps at launch across Files & sync (Nextcloud, ownCloud, Immich, Paperless-ngx), Notes & docs (DokuWiki, Joplin, Memos, Mealie), Media (PeerTube, MediaCMS, RomM), Analytics (Plausible, Matomo, Grafana), Business (Odoo, ERPNext, Bigcapital, Snipe-IT, OpenEMR, ONLYOFFICE, FOSSBilling), Communication (Rocket.Chat, phpBB, Flarum, Apache Answer), Development (Gitea, Forgejo, n8n, Uptime Kuma), Identity (Vaultwarden, ZITADEL, Enclosed, PrivateBin), AI & search (Open WebUI, SearXNG), Feeds (FreshRSS), and more.
- How are Docker apps isolated from each other?
- Tenant installs run with security_opt no-new-privileges, cap_drop ALL plus a minimal cap_add allowlist, pids_limit per service, and cgroup_parent pinned to the tenant's per-user systemd slice so CPU and memory accounting rolls up per tenant. The Docker daemon must run with --userns-remap enabled; if it stops running the agent fails Docker routes closed. A forbidden-caps blocklist keeps SYS_ADMIN, SYS_PTRACE, NET_ADMIN, and friends unreachable even if a catalog entry asks for them.
- Does the panel handle SSL and reverse proxy for Docker apps?
- Yes. Loopback-bound apps that declare reverse_proxy: true get a domain provisioned with managed_by='docker_app', an nginx vhost, and a Let's Encrypt certificate issued via the panel's existing certbot pipeline. No hand-editing of nginx configs.
- Are Docker app installs backed up?
- Yes. Every install directory under /var/lib/jabali/docker-apps/<instance_slug>/ — volumes plus rendered compose — is snapshotted through restic. Snapshots are tagged docker-app, panel-managed, slug:<slug>, and reason:manual|pre-update. Per-install backup destinations point at a M30 backup destinations entry; the jabali-agent.service env vars are the fallback repository. Updates are always preceded by a pre-update snapshot so failed updates roll back automatically.
- Can tenants install Docker apps or is it admin-only?
- Both. Master switch docker_marketplace_enabled turns the feature on. A second switch docker_apps_for_users_enabled exposes a filtered catalog to tenants at /jabali-panel/docker-apps. Server-level and per-hosting-package allowlists control which slugs are visible; per-package max_docker_apps caps the count. Tenants only see loopback-bound entries with tenant_installable: true — public-bound apps stay admin-only.
- How are Docker app updates handled?
- Every ~6 hours the panel pulls the image digest for each install and stores it as available_digest. When it differs from the running image_sha the panel surfaces an update available. Manual update triggers a restic pre-update snapshot, runs docker_app.update through the agent, and on failure restores from the snapshot automatically. Per-install update_mode = auto enables unattended updates through the auto-update timer.
- Which operating system is required?
- Debian 13 (Trixie) on bare metal or a KVM/QEMU VM. The panel installer detects and rejects earlier Debian releases and Ubuntu. Docker itself is installed from the Debian native package and runs with --userns-remap enabled by the panel's docker.tenant_health heartbeat.