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-devandpkg-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.12or3.12.4both 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 .venvof 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, orrequirements.txt,pyproject.tomlorPipfilenamesdjango,flask,fastapi,uvicornorgunicorn; package.jsonhas nostartscript 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 containswsgi.py(configif 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(orapp = Flask(inmain.py,app.py,server.py,app/main.py,src/main.py,wsgi.pyorapi.py, and uses it asmodule:variable(for exampleapp.main:app). If none is found,main:appis used. - A
Procfilewith aweb:line always provides the start command.
Click Analyze repository on the Deploy tab to check the result before deploying.
Deploy an app
- 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.
- Add your settings (database URL, secret key, allowed hosts) in Environment variables on the Deploy tab and click Save & redeploy.
- 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:
- Add
whitenoisetorequirements.txt. - Add
"whitenoise.middleware.WhiteNoiseMiddleware"right afterSecurityMiddlewareinMIDDLEWARE. - 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. |
Related
- Git deploy
- Node.js and Python apps
- Databases
- Packages and limits
- Django deployment checklist
- FastAPI deployment