DocsPython apps

Python apps

Deploy Django, Flask and FastAPI apps with a virtual environment, gunicorn or uvicorn workers sized to the package, static files, migrations and logs.

ZoPanel runs Python web apps as a service of the hosting account, behind nginx. Each release gets its own virtual environment in .venv, the app server (gunicorn or uvicorn) is added when it is missing, and the number of workers follows the package's CPU and memory. This page covers what is specific to Python; the deployment pipeline itself is described in Git deploy.

Prerequisites

  • The Python toolchain on the server. The installer adds it by default; otherwise an administrator opens Runtimes, finds the Python card under Other runtimes and clicks Install. This installs python3, python3-venv, python3-pip, python3-dev, build-essential, libpq-dev and pkg-config.
  • A website of type Git deploy, or any website with its Deploy tab.
  • The account's package must include Git deploy & apps.

Python version

By default apps use the Python 3 that ships with the operating system:

System Python
Ubuntu 22.04 3.10
Ubuntu 24.04 3.12
Debian 12 3.11
Debian 13 3.13

To build apps with another Python, an administrator installs uv under Runtimes (the uv (Python versions) card). ZoPanel downloads the uv release and checks it against the SHA-256 checksum published with it. Then:

  • The app's Version selects the Python. Auto-detect takes it from .python-version (3.12 or 3.12.4 both mean 3.12); with Auto-detect off, pick it under Version (3.10 to 3.14 are listed).
  • During the build, uv installs that Python for the hosting account, as that account, in its home folder (about 100 MB per version, downloaded once and reused by later builds). uv checks each download against the checksum recorded in uv itself. The python3 -m venv .venv of the install command then creates the virtual environment with it.
  • The build log says which Python is used, for example "Using Python 3.13 (managed by uv; the system Python is 3.12)" or "Using the system Python 3.12".

Without uv, a .python-version that asks for another version is ignored, and the build log says so: "Python 3.13 was requested (.python-version or Version), but only the system Python 3.12 is available: building with 3.12…". Values uv cannot install (for example pypy3.10) are also ignored with a note. For an interpreter neither option provides, deploy with a Dockerfile (see Docker deploy).

Detection

A repository is treated as Python when it contains manage.py, requirements.txt, pyproject.toml or Pipfile, and none of the files checked earlier: package.json, composer.json, Gemfile, pom.xml/build.gradle or a *.csproj.

A Python web app that also has a package.json for its front-end tooling (Tailwind, Vite, esbuild) is still detected as Python when both are true:

  • the repository has manage.py, or requirements.txt, pyproject.toml or Pipfile names django, flask, fastapi, uvicorn or gunicorn;
  • package.json has no start script and no Node server framework (Next.js, Nuxt, NestJS, Express, Fastify, Koa, Hono, Remix, SvelteKit, Astro).

If package.json has a build script, the assets are built first: npm ci && npm run build (or npm install && npm run build without package-lock.json) is put in front of the build command, and Node.js must be installed under Runtimes. Otherwise the repository is detected as Node.js: turn Auto-detect off and set Runtime to python and the commands below yourself.

Install command

File found Install command
requirements.txt python3 -m venv .venv && .venv/bin/pip install --upgrade pip -q && .venv/bin/pip install -r requirements.txt
pyproject.toml python3 -m venv .venv && .venv/bin/pip install --upgrade pip -q && .venv/bin/pip install .
Pipfile python3 -m venv .venv && .venv/bin/pip install pipenv -q && .venv/bin/pipenv install --deploy --system

When the dependency files do not mention the app server the framework needs, && .venv/bin/pip install gunicorn (Django, Flask) or && .venv/bin/pip install uvicorn (FastAPI) is appended. pip's download cache is kept in the account, so later builds are faster.

Framework and start command

Detected when Framework Proposed start command
manage.py exists, or django is in the dependencies Django .venv/bin/gunicorn <project>.wsgi:application --bind 127.0.0.1:$PORT
fastapi in the dependencies FastAPI .venv/bin/uvicorn <module>:<app> --host 127.0.0.1 --port $PORT
flask in the dependencies Flask .venv/bin/gunicorn <module>:<app> --bind 127.0.0.1:$PORT
otherwise Python .venv/bin/python main.py (or app.py, server.py)
  • Django: <project> is the first folder that contains wsgi.py (config if none). The build command is .venv/bin/python manage.py collectstatic --noinput || true.
  • FastAPI and Flask: ZoPanel looks for a line such as app = FastAPI( or app = Flask( in main.py, app.py, server.py, app/main.py, src/main.py, wsgi.py or api.py, and uses it as module:variable (for example app.main:app). If none is found, main:app is used.
  • A Procfile with a web: line always provides the start command.

Click Analyze repository on the Deploy tab to check the result before deploying.

Deploy an app

  1. Open Websites → New website, choose Git deploy, enter the Repository URL, Branch and, if the app is in a subfolder, the Root directory. Click Create website.
  2. Add your settings (database URL, secret key, allowed hosts) in Environment variables on the Deploy tab and click Save & redeploy.
  3. To change a command, turn Auto-detect off in Build & run, edit it and click Save & deploy.

Runtime environment

The start command runs from the release folder with:

Variable Value
PORT The slot's port. Bind to 127.0.0.1:$PORT.
HOST 127.0.0.1
VIRTUAL_ENV, PATH .venv of the release, first in PATH, so gunicorn and python resolve to the virtual environment.
PYTHONUNBUFFERED 1, so print() and logging appear in the output at once.
WEB_CONCURRENCY Number of workers, unless you set it: 2 × CPUs the package allows + 1, at most one per 128 MB of the package's memory (minimum 2), maximum 32.

Both gunicorn and uvicorn use WEB_CONCURRENCY as their default worker count. A --workers option in the start command takes precedence.

Environment variables

Variables from the Environment variables card are available to the install and build commands and at runtime (up to 200, single-line values). Use Paste .env to add many at once. Read them with os.environ:

import os
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
DEBUG = os.environ.get("DJANGO_DEBUG", "") == "1"
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "").split(",")

Django behind nginx

nginx terminates SSL and forwards requests with X-Forwarded-Proto. Add to settings.py:

ALLOWED_HOSTS = ["example.com", "www.example.com"]
CSRF_TRUSTED_ORIGINS = ["https://example.com", "https://www.example.com"]
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")

Without CSRF_TRUSTED_ORIGINS, form posts over HTTPS fail with a 403 CSRF error.

Static files

On a proxied website every request, /static/ included, goes to the app. Serve static files from the app with WhiteNoise:

  1. Add whitenoise to requirements.txt.
  2. Add "whitenoise.middleware.WhiteNoiseMiddleware" right after SecurityMiddleware in MIDDLEWARE.
  3. Set STATIC_ROOT = BASE_DIR / "staticfiles".

The detected build command runs collectstatic on every deploy. It ends with || true, so a failure does not stop the deployment: check the build log in Tasks if styles are missing.

Uploaded media

Files written to the release folder are lost at the next deploy. Either add the media folder to Persistent paths (for example media), or store uploads in S3 with django-storages and ZoPanel's S3 storage. Django does not serve MEDIA_ROOT itself when DEBUG is off, so with a persistent folder you also need a view or WhiteNoise configuration that serves it.

Migrations

ZoPanel does not run migrations automatically. Add them to the Build command, which runs with your environment variables:

.venv/bin/python manage.py collectstatic --noinput && .venv/bin/python manage.py migrate --noinput

For Flask-Migrate use .venv/bin/flask db upgrade, for Alembic .venv/bin/alembic upgrade head.

Important: migrations run before the new release passes its health check, while the previous release still serves traffic, and a Rollback does not undo them. Keep migrations backward compatible (add columns first, remove them in a later release).

Zero-downtime releases, rollback and logs

Like every server app, a Python release starts in the idle slot and receives traffic only after the Health check path (default /) answers with a status below 500 within 90 seconds. A 400 or 404 passes; a 500 does not. The Releases card keeps the last 5 builds for Rollback.

  • Application output on the Deploy tab shows the last 400 lines of the app's output; turn on Live to follow it.
  • Build logs are in Tasks; nginx logs are on the Logs tab.

Each release has its own .venv, rebuilt on every deploy. Keep data in a database or a persistent path, never inside .venv or the release folder.

Troubleshooting

Message or symptom Cause and fix
the app did not respond on port … within 90s (it must listen on $PORT) The server binds another address or port. Use --bind 127.0.0.1:$PORT (gunicorn) or --host 127.0.0.1 --port $PORT (uvicorn).
the app exited during startup Read Application output. Typical causes: wrong module:app in the start command, a missing environment variable, an import error.
install failed: … with No module named venv or ensurepip is not available The Python toolchain is missing. An administrator installs Python under Runtimes.
The build log says "only the system Python … is available" The project asks for another Python in .python-version. Ask the administrator to install uv under Runtimes, or remove .python-version if the system Python is fine.
could not install Python 3.x with uv uv could not download that Python (no network, an unknown version, or the account's disk quota is full). Read the lines above it in the build log.
install failed: … while building mysqlclient The MySQL client headers are not installed. Use PyMySQL, or ask the administrator to run apt-get install -y default-libmysqlclient-dev pkg-config.
Failed to find attribute 'app' in 'main' The app object has another name or module. Fix the start command, for example .venv/bin/gunicorn myapp.wsgi:app --bind 127.0.0.1:$PORT.
Django returns Bad Request (400) for the domain Add the domain to ALLOWED_HOSTS.
Pages without CSS Static files are not served: set up WhiteNoise and check that collectstatic succeeded in the build log.
The app is killed under load Too many workers for the package's memory. Set WEB_CONCURRENCY lower in Environment variables.

← Node.js apps PHP applications →