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_HOSTSset 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