Python Apps
Last updated
Native hosting for Python web apps. Each app is a per-user systemd unit running Gunicorn (WSGI) or Uvicorn (ASGI) on a loopback port, reverse-proxied by nginx at a mount point on the owner’s domain.
Framework catalog
Seven frameworks shipped, each with a scaffolded project template, pinned pip requirements, and the right server (gunicorn for WSGI, uvicorn for ASGI). Ordered by popularity.
Runtime
Interpreters supported when installed on the host: Python 3.8 – 3.14. The panel probes python<version> binaries via the agent (app.python.versions) and offers only versions that are actually installed. The probe is fail-open: if the agent is offline the version gate is skipped.
Runtime prerequisites installed once by install_python_apps_runtime (agent verb app.python.install_runtime):
python3 python3-venv python3-dev build-essential libffi-dev libssl-dev
server_settings.python_apps_enabled gates the entire feature. Users can’t create apps until an admin toggles it on.
App server types
| Type | Server | Bind |
|---|---|---|
wsgi (default) | gunicorn --workers 3 --bind 127.0.0.1:<port> | loopback |
asgi | uvicorn --host 127.0.0.1 --port <port> (--root-path <base_uri> if not /) | loopback |
The loopback port is auto-allocated from the [20000, 29999] pool. Uniqueness is enforced by uniq_python_apps_port. When the pool is exhausted the API returns 404.
Creating an app
POST /python-apps:
| Field | Required | Notes |
|---|---|---|
domain_id | yes | Domain the app mounts under. Must be owned by the requesting user. |
name | yes | Display name. |
python_version | yes | e.g. 3.11. Rejected if the interpreter isn’t on the host. |
app_root | yes | Absolute path under the owning Linux user’s home; scope-validated by the agent. |
app_type | no | wsgi (default) or asgi. |
entrypoint | yes | module:callable — e.g. myapp.wsgi:application. |
base_uri | no | Default /. Validated against /[A-Za-z0-9._/-]*. |
env | no | Initial environment map. |
start_command | no | Overrides the derived Gunicorn/Uvicorn command. |
cpu_limit / memory_limit / pids_limit | no | Passed through to the systemd unit. |
Venv & dependencies
- Venv lives at
<app_root>/venv/, created as the owning user withpython<version> -m venv. requirements.txtinapp_rootis installed automatically withpip install -r requirements.txt(8 min timeout per invocation,PIP_DISABLE_PIP_VERSION_CHECK=1).- Gunicorn or Uvicorn is always installed into the venv even if not in requirements.
Systemd unit
Written to /etc/systemd/system/jabali-app-<id>.service:
- User — the owning tenant, not root.
- Slice — nested under
jabali-user-<username>.sliceso package cgroup limits apply. - WorkingDirectory —
app_root. - Restart —
on-failure, 3 s delay. - Sandbox —
NoNewPrivileges=true,PrivateTmp=true. - EnvironmentFile —
/etc/jabali/python-apps/<id>.env(mode0640, owned by the app user). WSGI apps under a non-rootbase_urigetSCRIPT_NAMEset here.
Nginx wiring
The API attaches a proxy_pass rule to the domain’s NginxRules array with:
- Path — the app’s
base_uri. - Target —
http://127.0.0.1:<loopback_port>. - Websocket — enabled.
The domain reconciler renders the vhost; the Python reconciler doesn’t touch nginx directly.
Package quota
hosting_packages.max_python_apps (unsigned int, default 0).
0— Python apps aren’t included in the package. Create returnspython_apps_not_in_package.>0— the create endpoint counts existing apps and rejects withquota_exceededwhen the cap is hit.- Admins bypass the check.
Lifecycle
| Action | REST | Agent verb |
|---|---|---|
| List | GET /python-apps | — |
| Create | POST /python-apps | app.python.apply (via reconciler) |
| Get | GET /python-apps/:id | — |
| Delete | DELETE /python-apps/:id | app.python.remove |
| Start / stop / restart / status | POST /python-apps/:id/control | app.python.control |
| Tail logs | GET /python-apps/:id/logs?lines=200 (max 1000) | app.python.logs (journalctl -u jabali-app-<id>.service) |
| Set env | PUT /python-apps/:id/env | app.python.apply (rerun) |
| List installed interpreters | GET /python-apps/versions | app.python.versions |
Delete stops and removes the unit and env file. The venv is intentionally preserved.
Reconciler
reconcile_python_apps calls app.python.apply for every eligible row on each cycle. It converges venv, dependencies, unit file, and unit state, then writes status and last_error back to the row. Status values: pending → running | stopped | failed.
CLI
jabali python-app create
jabali python-app list
jabali python-app control <id> start|stop|restart|status
jabali python-app logs <id>
jabali python-app env get|set <id>
jabali python-app delete <id>
Caveats
- No health checks beyond systemd
Restart=on-failure. - No backup of the app tree by this feature — snapshot via Backups.
- Loopback bind only; no direct port publishing.
See also: Applications (PHP apps), Docker Apps, Hosting Packages.