Deployment
#django
#uvicorn
#nginx

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:

  1. configure Django for production with DEBUG=False, correct hosts, static paths, and proxy SSL settings
  2. install your app into a virtualenv on the server
  3. run Uvicorn as a systemd service
  4. proxy requests from Nginx to Uvicorn over a Unix socket or local TCP port
  5. serve /static/ directly from Nginx
  6. add HTTPS with Let's Encrypt
  7. 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=uvicorn tells systemd to create /run/uvicorn automatically on boot.
  • RuntimeDirectoryMode=775 and UMask=007 help make socket access predictable for Nginx when both processes share the www-data group.
  • --uds binds Uvicorn to a Unix domain socket.
  • --workers 2 is a reasonable starting point for a small app. Tune based on CPU and workload.
  • WorkingDirectory should contain manage.py and 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 current symlink 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/uvicorn directory 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_ROOT matches the Nginx alias
  • collectstatic actually 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_HOSTS
  • CSRF_TRUSTED_ORIGINS
  • whether you need USE_X_FORWARDED_HOST = True
  • whether Nginx forwards Host and X-Forwarded-Host as expected

If you need more background or troubleshooting, these pages fit naturally with this setup:

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.

2026 · django-deployment.com - Django Deployment knowledge base