# Python

A Python app deploys from its repository with no `Dockerfile`. Nimbus Land installs your dependencies and starts your app with a production server, as long as it can tell which one.

## Quick start

Python apps need a production server. In the console, create an app from your repository with the deploy wizard. From the CLI, run this in the project directory:

```sh
nimbus deploy
```

Before you do, check three things:

- **A dependency manifest** (`requirements.txt`, `pyproject.toml`, `Pipfile` or `setup.py`) is committed.
- **A production server is a dependency**: `gunicorn`, `uvicorn`, `hypercorn`, `waitress`, `daphne` or `granian`. Otherwise add a `Procfile` with a `web:` line. See [what every app needs](apps.md#what-every-app-needs).
- **The server listens on `PORT`.** Gunicorn's `--bind 0.0.0.0:$PORT` does.

FastAPI works with no `Procfile`. Django needs a few settings; see [Django](#django).

## Python version and uv

The Python version comes from `.python-version`. A `uv.lock` is installed with `uv sync --locked`.

## Start command

If there's no `Procfile` `web:` line and none of the servers above is among the dependencies, the deploy is blocked before anything builds:

```text
This app has no start command. Add gunicorn (or another server) to requirements.txt, or a Procfile with a web: line.
```

For a generic app, a `Procfile` like this works:

```text
web: gunicorn app:app --bind 0.0.0.0:$PORT
```

See [the deploy plan](apps.md#the-deploy-plan) for how verdicts like this appear.

## FastAPI

With no `Procfile`, Nimbus Land writes the start command: `fastapi run main.py` when `fastapi[standard]` or `fastapi-cli` is a dependency, otherwise `uvicorn main:app --host 0.0.0.0 --port $PORT`, naming the root file that creates `FastAPI()`. A `Procfile` wins, for example `web: gunicorn -k uvicorn.workers.UvicornWorker main:app`.

## Django

A stock `django-admin startproject` needs two additions.

**A dependency manifest.** `startproject` writes no `requirements.txt`, and the deploy is refused until you add one:

```text
Django==5.2.17
gunicorn
whitenoise
```

**Production settings.** Replace the hard-coded `SECRET_KEY`, `DEBUG` and `ALLOWED_HOSTS` in `settings.py` with values the platform fills:

```python
import os

SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", "django-insecure-change-me")
DEBUG = os.environ.get("DJANGO_DEBUG", "True") == "True"
ALLOWED_HOSTS = [h for h in os.environ.get("DJANGO_ALLOWED_HOSTS", "").split(",") if h]
# HTTPS ends at the platform's edge; trust its X-Forwarded-Proto header.
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
# Collected by the build, served by WhiteNoise.
STATIC_ROOT = BASE_DIR / "staticfiles"
```

Add `"whitenoise.middleware.WhiteNoiseMiddleware"` to `MIDDLEWARE` right after `SecurityMiddleware`, or the admin has no CSS.

### What Nimbus Land fills in

- **`DJANGO_SECRET_KEY`** is generated when absent and stored on the app, so redeploys reuse it.
- **`DJANGO_ALLOWED_HOSTS`** lists every hostname that reaches the app: the `nimbusapps.ca` address, and a verified custom domain with its `www` variant. It follows your domains as they change, unless you edit it. See [Custom domains](custom-domains.md).
- **`DJANGO_DEBUG=False`** is filled when absent.
- **`DJANGO_SETTINGS_MODULE`** comes from your `manage.py`.
- **Start command**: `gunicorn <project>.wsgi` when `gunicorn` is a dependency and there's no `Procfile`.
- **Static files**: the build runs `collectstatic`. Without `STATIC_ROOT` it is skipped (`Nimbus: STATIC_ROOT is not set; skipping collectstatic`), and nothing serves `/static/`.

### Database and migrations

Use a managed database. The stock SQLite file lives inside the app's image: it starts empty on every release, and one-off commands never see it. Add `psycopg[binary]` and `dj-database-url`, and set:

```python
DATABASES = {"default": dj_database_url.config(conn_max_age=600)}
```

The injected `DATABASE_URL` starts with `postgresql://`, so code that checks for `postgres://` should accept both.

The deploy wizard suggests `python manage.py migrate` as the release command for a Django app; you can edit or clear it under **Customize**. See [release command](apps.md#release-command). To run it once, use `nimbus run python manage.py migrate`. See [commands, workers and one-off runs](apps.md#commands-workers-and-one-off-runs).

## Database TLS

Attaching a managed Postgres database sets `DATABASE_URL` and `DATABASE_CA_CERT`. By default, psycopg and other libpq-based drivers connect in libpq's `prefer` mode, which doesn't verify the database's certificate, and that works as is.

To verify it, write `DATABASE_CA_CERT` to a file at start-up and connect with `sslmode=verify-full sslrootcert=<path>`.

## Troubleshooting

For problems that aren't specific to Python, such as an app that never answers, see [Apps troubleshooting](apps.md#troubleshooting).

### `This app has no start command`

The deploy was blocked because no server is a dependency and there's no `Procfile`. Add a server, or a `Procfile` with a `web:` line. See [Start command](#start-command).

### Django admin has no CSS, or `/static/` returns 404

`STATIC_ROOT` isn't set, so the build skipped `collectstatic`, or WhiteNoise isn't in `MIDDLEWARE`. Set both as shown under [Django](#django) and deploy again.

### Django: `DisallowedHost`

`ALLOWED_HOSTS` doesn't read `DJANGO_ALLOWED_HOSTS`. Use the settings shown under [Django](#django).

### `certificate verify failed`

A libpq-based driver set to verify the database's certificate doesn't know its authority. See [Database TLS](#database-tls).
