Deploying Python on Vessl
Vessl detects Python apps via requirements.txt, pyproject.toml, or Pipfile and builds them with Nixpacks. This guide focuses on FastAPI and ASGI apps, but the same flow applies to Flask, aiohttp, and other Python web frameworks.
Prerequisites
- A server added and provisioned in Vessl
- A GitHub repository with your Python app
- At least one of:
requirements.txt,pyproject.toml, orPipfile
What Vessl auto-detects
When Vessl finds requirements.txt or pyproject.toml (without manage.py) it sets:
| Setting | Auto-filled value |
|---|---|
| Build engine | Nixpacks |
| Port | 8000 |
Nixpacks installs your dependencies and starts your app. For FastAPI apps it typically picks up uvicorn from your requirements and sets the start command accordingly. If it doesn't, set it manually (see below).
Step 1 — Set the start command
Nixpacks auto-detects a Procfile or a uvicorn/gunicorn entry in pyproject.toml. If not, set the Start command in the wizard or app Settings:
FastAPI with Uvicorn:
uvicorn main:app --host 0.0.0.0 --port ${PORT:-8000}
FastAPI with Gunicorn + Uvicorn workers (production):
gunicorn main:app --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:${PORT:-8000} --workers 2
Flask with Gunicorn:
gunicorn app:app --bind 0.0.0.0:${PORT:-8000}
Replace main:app with your module name and the FastAPI/Flask instance variable (e.g. src.main:app for a src/ layout).
The cleanest way to define the start command for Nixpacks is a Procfile at the repo root:
web: uvicorn main:app --host 0.0.0.0 --port ${PORT:-8000}
Nixpacks reads this automatically and you don't need to set anything in Vessl.
Step 2 — Create the app
- In your project, click New Application.
- Select your Python repository and branch.
- Vessl detects Python — port defaults to
8000. - Set the Start command if Nixpacks won't pick it up automatically.
- Click Deploy.
Step 3 — Python version
Nixpacks picks the Python version from (in order):
.python-versionfilepython_requiresinpyproject.tomlpythonfield inPipfile- Latest stable if none of the above
To pin a version, create .python-version:
3.12
Or set NIXPACKS_PYTHON_VERSION in the Variables tab (e.g. 3.12.3).
Step 4 — Add a database
From the Add-ons tab, add PostgreSQL, MySQL, or Redis. Vessl injects:
PostgreSQL:
DATABASE_URL=postgresql://<user>:<pass>@<host>:5432/<db>
DB_HOST=<internal hostname>
DB_PORT=5432
DB_DATABASE=<db name>
DB_USERNAME=<username>
DB_PASSWORD=<password>
FastAPI with SQLAlchemy:
import os
from sqlalchemy import create_engine
engine = create_engine(os.environ["DATABASE_URL"])
FastAPI with asyncpg:
import os
import asyncpg
conn = await asyncpg.connect(os.environ["DATABASE_URL"])
Redis:
import os
import redis
r = redis.from_url(os.environ["REDIS_URL"])
Step 5 — Database migrations
If you use Alembic for migrations, set the Post-deploy command to:
alembic upgrade head
For SQLModel or bare SQLAlchemy with create_all, you can run it from a startup script — but a post-deploy command is cleaner and only runs after a successful deploy.
Step 6 — Background workers
For Celery workers, add a daemon from the Daemons tab:
| Worker type | Command |
|---|---|
| Celery worker | celery -A main.celery worker --loglevel=info |
| Celery beat | celery -A main.celery beat --loglevel=info |
| ARQ / RQ worker | arq main.WorkerSettings |
Each daemon runs in its own container and shares the same environment variables (including REDIS_URL for the broker).
Step 7 — Monorepo / subdirectory
If your Python app lives in a subdirectory (e.g. backend/), set the Root Directory field in the wizard. All commands — install, build, start, post-deploy — run relative to that path.
Common issues
ModuleNotFoundError at startup
The package is missing from requirements.txt (or pyproject.toml dependencies). Add it, commit, and redeploy.
Uvicorn not found
Add uvicorn (or uvicorn[standard]) to your requirements.txt:
fastapi>=0.110.0
uvicorn[standard]>=0.29.0
App starts but returns 422 on the health check
FastAPI returns 422 for requests that fail validation. If your / route requires query parameters, set the Health check path to a dedicated /health endpoint:
@app.get("/health")
def health():
return {"status": "ok"}
DATABASE_URL uses postgres:// but SQLAlchemy 1.4+ requires postgresql://
Vessl injects postgresql:// — this should work. But if you're constructing the URL yourself or using an external DB, SQLAlchemy 1.4+ dropped support for the postgres:// scheme. Rename it to postgresql://.
uv / pdm dependency manager
Nixpacks supports uv (detects uv.lock) and pdm (detects pdm.lock). Make sure the lockfile is committed to the repo.