How-to
Python applications
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 |
scalws-controller (default) | You run uvicorn or gunicorn yourself (for example as a systemd unit). ScalWS proxies to it. | proxy |
scalws-controller refuses python runtimes, also python with upstream:
(ADR-0036).
Prerequisites
- ScalWS installed, with
include: sites.d/*.yamlin/etc/scalws/scalws.yaml. - The project outside
/home, for example/srv/api, with a virtualenv that contains the server:uvicornfor ASGI,gunicornfor WSGI.
python3 -m venv /srv/api/.venv
/srv/api/.venv/bin/pip install -r /srv/api/requirements.txt uvicornFastAPI with uvicorn (scalwsd.service)
# /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: trueScalWS runs /srv/api/.venv/bin/uvicorn main:app --uds <socket> --workers 2 --no-access-log in /srv/api.
python.mode:asgiorwsgi(required).python.app:module:callable.python.virtualenv: the server binary comes from<virtualenv>/bin;VIRTUAL_ENVandPATHare set.python.workers: uvicorn’s own worker count ismin. Withprocess_pool: trueScalWS runsmintomaxsingle-worker servers itself instead, which it can scale.profile: fastapisets the readiness check to/docsand bypasses the cache for/docsand/openapi.json. If your application disables/docs, setpython.health_pathto 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)
# /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
appneedspython.servergunicorn, the default forwsgi. - Route paths are not stripped: with
STATIC_ROOT = "/srv/mysite/public/static"the route’srootis/srv/mysite/public. Runmanage.py collectstaticafter each deploy. profile: djangodenies/manage.pyand/db.sqlite3and bypasses the cache for/adminand thesessionidandcsrftokencookies.- 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:
# /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.targetFor Django the ExecStart= line would be
/srv/mysite/.venv/bin/gunicorn mysite.wsgi:application --bind 127.0.0.1:8000 --workers 4.
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# /etc/scalws/sites.d/api.example.com.yaml
domain: api.example.com
proxy: http://127.0.0.1:8000The 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
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
modemissing:python:as a map needsmode: asgiormode: wsgi.- No worker becomes ready (503): the server is not installed in the virtualenv, the
module:callableis wrong, orhealth_pathanswers 5xx. The server’s output is in the journal with targetscalws::app. managed python runtimes are not supported with several instances: use the proxy setup under scalws-controller, or switch toscalwsd.service.- Static files 404 under
/static: the routerootmust be the directory that containsstatic/, notstatic/itself. - Django answers 400 Bad Request: the domain is missing from
ALLOWED_HOSTS. - A custom command (extra server options): use
python.command, for examplecommand: [/srv/api/.venv/bin/uvicorn, "main:app", --proxy-headers]together withmode; ScalWS appends the bind arguments.
See also: applications, FastAPI and Django examples.