# Python applications

URL: https://scalws.com/docs/howto/python/
Source: `docs/howto/python.md` in the ScalWS repository

Run a FastAPI (ASGI) application with uvicorn or a Django (WSGI) application with
gunicorn behind ScalWS.

## Which way?

| Server unit | How the application runs | Site file key |
|---|---|---|
| `scalwsd.service` | ScalWS builds the uvicorn or gunicorn command line, starts and supervises it. | [`python`](https://scalws.com/docs/reference/site-file/#python) |
| `scalws-controller` (default) | You run uvicorn or gunicorn yourself (for example as a systemd unit). ScalWS proxies to it. | [`proxy`](https://scalws.com/docs/reference/site-file/#proxy) |

scalws-controller refuses `python` runtimes, also `python` with `upstream:`
([ADR-0036](https://scalws.com/docs/adr/0036-multi-instance-state/)).

## Prerequisites

* ScalWS installed, with `include: sites.d/*.yaml` in `/etc/scalws/scalws.yaml`.
* The project outside `/home`, for example `/srv/api`, with a virtualenv that contains
  the server: `uvicorn` for ASGI, `gunicorn` for WSGI.

```sh
python3 -m venv /srv/api/.venv
/srv/api/.venv/bin/pip install -r /srv/api/requirements.txt uvicorn
```

## FastAPI with uvicorn (scalwsd.service)

```yaml
# /etc/scalws/sites.d/api.example.com.yaml
domain: api.example.com
root: /srv/api
profile: fastapi
python:
  mode: asgi
  app: "main:app"
  virtualenv: /srv/api/.venv
  workers: {min: 2, max: 8}
https_redirect: true
```

ScalWS runs `/srv/api/.venv/bin/uvicorn main:app --uds <socket> --workers 2
--no-access-log` in `/srv/api`.

* [`python.mode`](https://scalws.com/docs/reference/site-file/#python.mode): `asgi` or `wsgi` (required).
* [`python.app`](https://scalws.com/docs/reference/site-file/#python.app): `module:callable`.
* [`python.virtualenv`](https://scalws.com/docs/reference/site-file/#python.virtualenv): the server binary
  comes from `<virtualenv>/bin`; `VIRTUAL_ENV` and `PATH` are set.
* [`python.workers`](https://scalws.com/docs/reference/site-file/#python.workers): uvicorn's own worker
  count is `min`. With [`process_pool: true`](https://scalws.com/docs/reference/site-file/#python.process_pool)
  ScalWS runs `min` to `max` single-worker servers itself instead, which it can scale.
* `profile: fastapi` sets the readiness check to `/docs` and bypasses the cache for `/docs`
  and `/openapi.json`. If your application disables `/docs`, set
  [`python.health_path`](https://scalws.com/docs/reference/site-file/#python.health_path) to a path it
  serves.

The short form `python: "main:app"` (also `scalwsctl site add --python main:app`) runs
uvicorn from the system path with its defaults.

## Django with gunicorn (scalwsd.service)

```yaml
# /etc/scalws/sites.d/example.org.yaml
domain: example.org
www: redirect
root: /srv/mysite
profile: django
python:
  mode: wsgi
  app: "mysite.wsgi:application"
  virtualenv: /srv/mysite/.venv
  process_pool: true
  workers: {min: 2, max: 8, autoscale: auto, target_concurrency: 1}
routes:
  - path: /static
    runtime: {type: static, root: /srv/mysite/public}
```

* WSGI with `app` needs [`python.server`](https://scalws.com/docs/reference/site-file/#python.server)
  `gunicorn`, the default for `wsgi`.
* Route paths are not stripped: with `STATIC_ROOT = "/srv/mysite/public/static"` the
  route's `root` is `/srv/mysite/public`. Run `manage.py collectstatic` after each deploy.
* `profile: django` denies `/manage.py` and `/db.sqlite3` and bypasses the cache for
  `/admin` and the `sessionid` and `csrftoken` cookies.
* Add the domains to Django's `ALLOWED_HOSTS`.

The process gets `FORWARDED_ALLOW_IPS=*` and `PYTHONUNBUFFERED=1`. It runs inside the
unit's sandbox, where `/srv` is read-only; give write access (media uploads, a SQLite
file) with a `ReadWritePaths=` drop-in for `scalwsd.service`.

## Under scalws-controller: proxy to your own service

Run the server on loopback with systemd, for example for FastAPI:

```ini
# /etc/systemd/system/api-example-com.service
[Unit]
Description=api.example.com
After=network.target

[Service]
User=api
WorkingDirectory=/srv/api
ExecStart=/srv/api/.venv/bin/uvicorn main:app --host 127.0.0.1 --port 8000 --workers 4 --proxy-headers --forwarded-allow-ips 127.0.0.1
Restart=always

[Install]
WantedBy=multi-user.target
```

For Django the `ExecStart=` line would be
`/srv/mysite/.venv/bin/gunicorn mysite.wsgi:application --bind 127.0.0.1:8000 --workers 4`.

```sh
sudo systemctl daemon-reload
sudo systemctl enable --now api-example-com
sudo scalwsctl site add api.example.com --proxy http://127.0.0.1:8000
```

```yaml
# /etc/scalws/sites.d/api.example.com.yaml
domain: api.example.com
proxy: http://127.0.0.1:8000
```

The proxy replaces `X-Forwarded-For` and `X-Forwarded-Proto` with the values it saw, so the
application can trust them from `127.0.0.1` (uvicorn `--forwarded-allow-ips`, Django
`SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")`). Serve Django's static
files with a `static` route as above.

## Check it works

```sh
curl -sI -H 'Host: api.example.com' http://127.0.0.1/docs
sudo scalwsctl site list
scalwsctl apps                       # under scalwsd.service
journalctl -u scalwsd -o cat | jq -c 'select(.target == "scalws::app")'
```

## Common problems

* **`mode` missing**: `python:` as a map needs `mode: asgi` or `mode: wsgi`.
* **No worker becomes ready (503)**: the server is not installed in the virtualenv, the
  `module:callable` is wrong, or `health_path` answers 5xx. The server's output is in the
  journal with target `scalws::app`.
* **`managed python runtimes are not supported with several instances`**: use the proxy
  setup under scalws-controller, or switch to `scalwsd.service`.
* **Static files 404 under `/static`**: the route `root` must be the directory that
  contains `static/`, not `static/` itself.
* **Django answers 400 Bad Request**: the domain is missing from `ALLOWED_HOSTS`.
* **A custom command** (extra server options): use
  [`python.command`](https://scalws.com/docs/reference/site-file/#python.command), for example
  `command: [/srv/api/.venv/bin/uvicorn, "main:app", --proxy-headers]` together with
  `mode`; ScalWS appends the bind arguments.

See also: [applications](https://scalws.com/docs/guide/applications/#python),
[FastAPI](https://scalws.com/docs/guide/applications/#fastapi-asgi) and
[Django](https://scalws.com/docs/guide/applications/#django-wsgi) examples.
