Kingsfordweg 151, 1043GR Amsterdam, The Netherlands ● 24/7 support — support@novogara.com
Knowledge base

Deploying Python

The development server is not a production server, and every Python framework says so in its own documentation. Here is what to use instead.

The shape

nginx in front, a WSGI or ASGI server behind it, your application inside that, all under systemd. For Django and Flask that is gunicorn; for FastAPI or anything async, gunicorn with uvicorn workers, or uvicorn directly.

A unit that works

In /etc/systemd/system/myapp.service:

User=myapp
WorkingDirectory=/srv/myapp
ExecStart=/srv/myapp/venv/bin/gunicorn --workers 9 --bind unix:/run/myapp/gunicorn.sock myapp.wsgi:application
RuntimeDirectory=myapp
Restart=always

A unix socket rather than a TCP port: slightly faster, and impossible to reach from outside by accident. RuntimeDirectory= creates /run/myapp with the right ownership and cleans it up afterwards. See running your application as a service.

How many workers

The common starting point is 2 × cores + 1 for synchronous workers. For IO-bound applications — most web applications — async workers handle far more concurrency per process:

--worker-class uvicorn.workers.UvicornWorker

Measure rather than guess. Each worker is a full copy of your application in memory; on a 4 GB application, nine workers is 36 GB. Set --max-requests 1000 --max-requests-jitter 100 so workers recycle, which papers over small memory leaks without you having to find them first.

nginx in front

location / { proxy_pass http://unix:/run/myapp/gunicorn.sock; }

Serve static and media files directly from nginx, never through Python. Run collectstatic as part of your deploy and point a location at the output with a long cache lifetime. That one change removes a large share of the requests your application ever sees.

The virtualenv is part of the deployment

One venv per application, owned by its own user, built from a lockfile. Never pip install into the system Python on a server — distribution tooling depends on it and you will break something unrelated.

Build and test the venv before switching to it, so a failed dependency resolution does not take production with it.

Settings and secrets

  • DEBUG = False. A Django traceback page exposes your settings to the internet.
  • ALLOWED_HOSTS set to your real names.
  • Secrets from the environment, not the repository — see managing secrets. systemd's EnvironmentFile= with mode 600 is a reasonable minimum.
  • Run migrations as a separate step, not on start-up — otherwise nine workers race each other through them.

Reload without dropping requests

gunicorn reloads gracefully on HUP: new workers start, old ones finish what they are doing.

systemctl reload myapp with ExecReload=/bin/kill -s HUP $MAINPID in the unit.


Still stuck? Mail support@novogara.com — an engineer answers, at any hour. Back to the knowledge base