Deploy Django ASGI with Uvicorn and Nginx
If you want to deploy Django ASGI with Uvicorn and Nginx, python manage.py runserver is not a production option.
Problem statement
If you want to deploy Django ASGI with Uvicorn and Nginx, python manage.py runserver is not a production option. It does not provide process supervision, safe restarts, TLS termination, or reliable reverse proxy behavior.
For a real Django ASGI deployment, you need a few pieces working together:
- an ASGI application entrypoint such as
project.asgi:application - a production ASGI server such as Uvicorn
- a reverse proxy such as Nginx
- static file handling outside Django
- environment-based secrets and production settings
- a restart and rollback path that does not leave the app half-deployed
This guide shows a practical single-server setup: Django ASGI running under Uvicorn + systemd, with Nginx in front for proxying, static files, and TLS.
Quick answer
The standard pattern to run Django with Uvicorn and Nginx in production is:
- configure Django for production with
DEBUG=False, correct hosts, static paths, and proxy SSL settings - install your app into a virtualenv on the server
- run Uvicorn as a
systemdservice - proxy requests from Nginx to Uvicorn over a Unix socket or local TCP port
- serve
/static/directly from Nginx - add HTTPS with Let's Encrypt
- verify health before sending traffic
This article uses a manual single-server setup first. That keeps the moving parts visible and makes later automation easier.
Step-by-step solution
Step 1 — Prepare Django for production
Confirm the ASGI entrypoint
Make sure your project has a working asgi.py, usually:
# project/asgi.py
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings")
application = get_asgi_application()
Test that the import works from your project root:
python -c "from project.asgi import application; print(application)"
If you are only serving normal HTTP requests, plain Django ASGI is enough. If you use WebSockets or Channels, the ASGI app and Nginx proxy config need additional handling.
Set production settings
Your production settings should at minimum include:
DEBUG = False
ALLOWED_HOSTS = ["example.com", "www.example.com"]
CSRF_TRUSTED_ORIGINS = [
"https://example.com",
"https://www.example.com",
]
STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
# Enable after HTTPS and proxy headers are confirmed working
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = False
If Django should trust the host forwarded by Nginx, add:
USE_X_FORWARDED_HOST = True
Only use USE_X_FORWARDED_HOST if you intend Django to rely on the reverse proxy host header. In many simple setups, Host forwarding from Nginx is already enough.
Load secrets and database settings from environment variables, not from the repo:
import os
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": os.environ["DB_NAME"],
"USER": os.environ["DB_USER"],
"PASSWORD": os.environ["DB_PASSWORD"],
"HOST": os.environ["DB_HOST"],
"PORT": os.environ.get("DB_PORT", "5432"),
}
}
Run Django’s deployment checks before going further:
python manage.py check --deploy
Collect static files and run migrations
After dependencies are installed and environment variables are available:
python manage.py migrate
python manage.py collectstatic --noinput
Verification:
ls -lah staticfiles/
Rollback note: migrations are often the riskiest part of a deployment. If a migration is backward-incompatible, do not assume you can safely roll back only the code.
Step 2 — Create the application environment on the server
This example uses /srv/project with a simple layout.
sudo mkdir -p /srv/project
sudo chown $USER:$USER /srv/project
cd /srv/project
python3 -m venv .venv
source .venv/bin/activate
Copy or clone your application code into /srv/project/app, then install dependencies:
mkdir -p /srv/project/app
cd /srv/project/app
pip install --upgrade pip
pip install -r requirements.txt
Make sure uvicorn is installed:
pip show uvicorn
For this stack, Gunicorn is not required. Use it only if you intentionally want Gunicorn managing Uvicorn workers.
Store secrets safely
Create an environment file owned by root and readable by the app service user:
sudo mkdir -p /etc/project
sudo nano /etc/project/project.env
Example:
DJANGO_SETTINGS_MODULE=project.settings
DJANGO_SECRET_KEY=replace-me
DB_NAME=projectdb
DB_USER=projectuser
DB_PASSWORD=replace-me
DB_HOST=127.0.0.1
DB_PORT=5432
Set permissions:
sudo chown root:www-data /etc/project/project.env
sudo chmod 640 /etc/project/project.env
Do not put secrets in the Git repo or directly in the systemd unit.
Step 3 — Run Uvicorn with systemd
Choose bind method: Unix socket vs TCP port
For local Nginx-to-Uvicorn proxying on one server, a Unix socket is usually a good default. It avoids exposing an app port beyond localhost and fits this architecture well.
Create the systemd service
Create /etc/systemd/system/project-uvicorn.service:
[Unit]
Description=Uvicorn service for Django project
After=network.target
[Service]
User=deploy
Group=www-data
WorkingDirectory=/srv/project/app
EnvironmentFile=/etc/project/project.env
RuntimeDirectory=uvicorn
RuntimeDirectoryMode=775
UMask=007
ExecStart=/srv/project/.venv/bin/uvicorn project.asgi:application --workers 2 --uds /run/uvicorn/project.sock
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
Replace deploy with your actual deployment user.
Notes:
RuntimeDirectory=uvicorntellssystemdto create/run/uvicornautomatically on boot.RuntimeDirectoryMode=775andUMask=007help make socket access predictable for Nginx when both processes share thewww-datagroup.--udsbinds Uvicorn to a Unix domain socket.--workers 2is a reasonable starting point for a small app. Tune based on CPU and workload.WorkingDirectoryshould containmanage.pyand your Django package.
Enable and start it:
sudo systemctl daemon-reload
sudo systemctl enable --now project-uvicorn
sudo systemctl status project-uvicorn
Check logs:
journalctl -u project-uvicorn -n 100 --no-pager
Verification:
sudo ls -lah /run/uvicorn/project.sock
If you prefer TCP instead, use:
ExecStart=/srv/project/.venv/bin/uvicorn project.asgi:application --workers 2 --host 127.0.0.1 --port 8000
Step 4 — Configure Nginx as the reverse proxy
Create the Nginx server block
Create /etc/nginx/sites-available/project:
server {
listen 80;
server_name example.com www.example.com;
client_max_body_size 10M;
location /static/ {
alias /srv/project/app/staticfiles/;
}
location / {
proxy_pass http://unix:/run/uvicorn/project.sock;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 60;
proxy_connect_timeout 60;
proxy_redirect off;
}
location ~ /\. {
deny all;
}
}
If you use a TCP upstream instead:
proxy_pass http://127.0.0.1:8000;
Serve static files directly with Nginx
The alias path must match Django’s STATIC_ROOT. A common failure is pointing Nginx at the wrong directory or forgetting the trailing slash.
If you also serve media uploads:
location /media/ {
alias /srv/project/app/media/;
}
Make sure Nginx can read those directories. For example, if your app user is deploy and Nginx uses the www-data group:
sudo chown -R deploy:www-data /srv/project/app/staticfiles /srv/project/app/media
sudo chmod -R u=rwX,g=rX,o= /srv/project/app/staticfiles /srv/project/app/media
If your host uses a different Nginx user or group, adjust those commands. Be more careful with /media/ than /static/: uploaded files are user-controlled content and should not be treated like trusted application assets.
Enable the site and test Nginx config
sudo ln -s /etc/nginx/sites-available/project /etc/nginx/sites-enabled/project
sudo nginx -t
sudo systemctl reload nginx
Verification:
curl -I http://example.com
curl -I http://example.com/static/admin/css/base.css
If Nginx returns 502 Bad Gateway, inspect both:
journalctl -u project-uvicorn -n 100 --no-pager
sudo tail -n 100 /var/log/nginx/error.log
Step 5 — Add TLS and secure the edge
Install a certificate with Certbot using the Nginx plugin or your preferred ACME workflow:
sudo certbot --nginx -d example.com -d www.example.com
After issuance, verify redirect behavior:
curl -I http://example.com
curl -I https://example.com
Django must receive the original HTTPS information through:
proxy_set_header X-Forwarded-Proto $scheme;
And Django must trust that header:
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
Before considering the deployment complete, make sure one redirect layer is active:
- either Nginx redirects HTTP to HTTPS
- or Django uses
SECURE_SSL_REDIRECT = True
Do not leave both HTTP and HTTPS serving the same application unintentionally.
Basic hardening in this layout includes:
- keep Uvicorn off the public edge
- deny hidden files
- cap request body size with
client_max_body_size - enable secure cookies in Django
- enable HSTS only after HTTPS works correctly end to end
You can also set server_tokens off; in the main Nginx config if you want to reduce version exposure.
Step 6 — Verify the deployment
Check the service:
sudo systemctl status project-uvicorn
If using a Unix socket, test the upstream directly:
curl --unix-socket /run/uvicorn/project.sock http://localhost/
Test the public path through Nginx:
curl -I https://example.com
Verify static files come from Nginx:
curl -I https://example.com/static/admin/css/base.css
If your app has a health endpoint such as /health/, test that too:
curl -I https://example.com/health/
Confirm security-sensitive behavior:
- HTTP redirects to HTTPS
- requests to unknown hosts are not accepted by Django
- admin login works over HTTPS
- CSRF-protected forms submit correctly
Run this after deployment as an extra Django settings check:
cd /srv/project/app
source /srv/project/.venv/bin/activate
set -a
. /etc/project/project.env
set +a
python manage.py check --deploy
This set -a pattern assumes your env file is shell-compatible. If you use a different env format, use a dedicated loader instead of trying to parse it with export $(...).
Step 7 — Safe release and rollback workflow
A simple release flow on one server looks like this:
cd /srv/project/app
git pull
source /srv/project/.venv/bin/activate
pip install -r requirements.txt
set -a
. /etc/project/project.env
set +a
python -c "from project.asgi import application; print(application)"
python manage.py migrate
python manage.py collectstatic --noinput
sudo systemctl restart project-uvicorn
sudo systemctl status project-uvicorn
Reload Nginx only if its config changed:
sudo nginx -t && sudo systemctl reload nginx
Verification after restart:
curl -I https://example.com
For safer deployments, keep the previous release available instead of editing the live code directory in place. A common pattern is release directories plus a current symlink, so you can switch back quickly if the new code fails before or after restart.
Rollback guidance:
- keep the previous code version available
- revert to the previous commit or switch the
currentsymlink back - restart Uvicorn
- verify health before considering the rollback complete
Database rollback is separate and riskier than code rollback. If a deployment includes destructive schema changes, plan the migration strategy in advance and consider a database backup or snapshot before applying risky migrations.
When to script this
Once you have done this process a few times, the repetitive parts are good automation candidates: virtualenv setup, environment file placement, migrate + collectstatic, ASGI import validation, service restart, Nginx config test, and post-deploy health checks. A reusable script or template becomes useful when you are repeating the same Django ASGI pattern across multiple apps or servers.
Explanation
Why Uvicorn is used for Django ASGI
Uvicorn is a production ASGI server. It runs Django’s ASGI application directly and is a good fit when you want ASGI support, including async request handling and long-lived connections.
Why Nginx stays in front
Nginx handles the edge concerns Uvicorn should not manage alone:
- TLS termination
- reverse proxying
- static file serving
- request buffering and timeouts
- basic request filtering
This keeps the app server focused on the Django application.
When to use a different pattern
Use a different deployment pattern if:
- you want Gunicorn managing process workers with Uvicorn workers
- you use Django Channels and need a more explicit WebSocket architecture
- you deploy in containers and want the process model handled by an orchestrator instead of
systemd
Edge cases / notes
If the app uses WebSockets
Nginx needs upgrade headers for WebSockets. Add these in the relevant proxied location:
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
Then verify end-to-end behavior with a real WebSocket client, not only normal HTTP requests.
If using a Unix socket
Most 502 errors come from one of these:
- Uvicorn never started, so the socket was never created
- the socket file exists but Nginx cannot access it
- the
/run/uvicorndirectory was not created with the expected ownership and mode
Using RuntimeDirectory=, RuntimeDirectoryMode=, and a shared group between the service and Nginx makes this more reliable.
If static files return 404
Check these three items first:
STATIC_ROOTmatches the Nginxaliascollectstaticactually ran successfully- file permissions allow Nginx to read the files
If Django thinks requests are HTTP
That usually means one of these is missing or wrong:
- Nginx does not send
X-Forwarded-Proto - Django does not set
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
If host or CSRF errors appear behind Nginx
Check:
ALLOWED_HOSTSCSRF_TRUSTED_ORIGINS- whether you need
USE_X_FORWARDED_HOST = True - whether Nginx forwards
HostandX-Forwarded-Hostas expected
Internal links
If you need more background or troubleshooting, these pages fit naturally with this setup:
- read What Is ASGI in Django and When to Use It in Production for the ASGI vs WSGI decision
- compare Deploy Django with Gunicorn and Nginx on Ubuntu if you want the WSGI pattern instead
- follow Django Deployment Checklist for Production before going live
- check Deploy Django with Docker Compose in Production if you want a container-based process model
FAQ
Can I deploy Django ASGI with Uvicorn and Nginx without Docker?
Yes. This guide is a non-Docker deployment. systemd plus Nginx is a normal production pattern on Linux servers.
Should I use a Unix socket or a local TCP port for Uvicorn?
For a single server with local Nginx, a Unix socket is usually a good default. A local TCP port is simpler to inspect with common tools and may be easier in some environments. Either is valid if permissions and proxy settings are correct.
Do I need Gunicorn if I already use Uvicorn for Django ASGI?
No. Uvicorn can run Django ASGI directly. Gunicorn is optional if you specifically want Gunicorn’s worker supervision model.
How do I handle WebSockets with Django, Uvicorn, and Nginx?
Keep Django on ASGI, make sure your app routing supports WebSockets if needed, and add Nginx upgrade headers so the connection can be upgraded properly. Then test with a real WebSocket client, not only with normal HTTP requests.
What is the safest way to restart the app during releases?
Apply code and dependencies, validate the ASGI import before restart, run migrations carefully, collect static files, then restart only the Uvicorn service. Reload Nginx only if its configuration changed. For safer rollbacks, keep the previous release available so you can switch back quickly.