Deploy Django with uWSGI and Nginx

Run Django with uWSGI and Nginx: a strict uWSGI ini, socket permissions, systemd integration, graceful reloads, harakiri timeouts, and checks.

Problem statement

uWSGI is a mature, very configurable application server that many existing Django deployments still run. You may be inheriting one, or you may need features such as its native uwsgi protocol with Nginx, per-request harakiri timeouts, or fine-grained process management.

uWSGI's flexibility is also its main risk. It has hundreds of options, several defaults that surprise Django users, and configuration mistakes that fail silently:

  • a typo in an option name is ignored unless strict mode is on
  • the app fails to import but uWSGI keeps running and returns errors for every request
  • SIGTERM triggers a reload instead of shutdown unless configured otherwise
  • the socket is created with permissions Nginx cannot use

For new projects, Gunicorn is simpler, and the uWSGI project itself has been in maintenance mode for several years. This guide is for teams that choose or maintain uWSGI and want a safe, explicit configuration.

Django with uWSGI and NginxNginxuwsgi_passUnix socketuwsgi protocoluWSGI masterstrict iniWorkersDjango appStrict mode and need-app make configuration mistakes fail loudly at startup.

Quick answer

  1. Install uWSGI into the project virtualenv (pip install uwsgi), not from a distribution package with plugins you do not use.
  2. Write an ini file with strict, master, need-app, die-on-term, vacuum, a Unix socket with chmod-socket = 660, and a harakiri timeout.
  3. Run it as a systemd service with Type=notify and KillSignal=SIGQUIT, and reload gracefully with ExecReload=/bin/kill -HUP $MAINPID.
  4. Configure Nginx with include uwsgi_params; and uwsgi_pass unix:/run/myapp/uwsgi.sock;.
  5. Serve static files from Nginx.
  6. Verify the socket, the logs, and a request through Nginx.

Step-by-step solution

1. Install uWSGI in the virtualenv

uWSGI compiles a C extension, so install a compiler and Python headers first:

sudo apt install build-essential python3-dev
/srv/myapp/venv/bin/pip install uwsgi
/srv/myapp/venv/bin/uwsgi --version

Installing from pip ties uWSGI to the virtualenv's Python. Distribution packages use a plugin model where the Python plugin must match the interpreter, a common source of "no app loaded" errors.

2. Write the uWSGI configuration

/etc/myapp/uwsgi.ini:

[uwsgi]
strict = true
master = true
need-app = true
enable-threads = true
single-interpreter = true
die-on-term = true
vacuum = true

chdir = /srv/myapp/app
home = /srv/myapp/venv
module = config.wsgi:application
env = DJANGO_SETTINGS_MODULE=config.settings

uid = myapp
gid = www-data
socket = /run/myapp/uwsgi.sock
chmod-socket = 660

processes = 4
threads = 2
harakiri = 60
max-requests = 5000
max-requests-delta = 500
reload-on-rss = 512

disable-logging = false
log-4xx = true
log-5xx = true
uwsgi.ini options worth understandingstrict= trueneed-app= truedie-on-term= truesocket = /run/myapp/uwsgi.sockchmod-socket =660harakiri =60max-requests = 5000123451Typos in option names are errors, not silently ignored.2Exit if Django fails to import, instead of serving nothing.3SIGTERM stops uWSGI, as systemd expects.4Owner and www-data group only: Nginx can connect.5Kills a worker stuck on one request for 60 s.Without these, uWSGI defaults are surprisingly permissivedjango-deployment.com
max-requests-delta staggers recycling so workers do not all restart together.

What the important options do:

  • strict = true makes uWSGI refuse to start when the file contains an unknown option, so typos do not silently disable settings.
  • need-app = true exits if the WSGI module fails to import, instead of running a server that returns errors for every request.
  • die-on-term = true makes SIGTERM shut uWSGI down. Without it, older versions treat SIGTERM as a reload request.
  • vacuum = true removes the socket and pid files on exit.
  • single-interpreter = true disables multiple interpreter support, which Django does not use.
  • harakiri = 60 kills a worker stuck on one request for more than 60 seconds and logs the request.
  • max-requests with max-requests-delta recycles workers periodically at staggered counts to limit memory growth.
  • reload-on-rss (in megabytes) restarts a worker after a request leaves it above that resident memory size.

Secrets should not be written into this ini file. Load them from an environment file in systemd (step 3).

3. Run it with systemd

/etc/systemd/system/myapp.service:

[Unit]
Description=myapp uWSGI
After=network.target

[Service]
Type=notify
EnvironmentFile=/etc/myapp/env
RuntimeDirectory=myapp
RuntimeDirectoryMode=0755
ExecStart=/srv/myapp/venv/bin/uwsgi --ini /etc/myapp/uwsgi.ini
ExecReload=/bin/kill -HUP $MAINPID
KillSignal=SIGQUIT
NotifyAccess=all
Restart=on-failure

[Install]
WantedBy=multi-user.target
  • uWSGI supports systemd's notify protocol, so Type=notify marks the service as started only when uWSGI reports it is ready.
  • SIGHUP performs a graceful reload: workers finish current requests, and the app is reloaded.
  • SIGQUIT shuts uWSGI down quickly, which is what systemd should send on stop.
  • RuntimeDirectory=myapp creates /run/myapp at start. Because uWSGI starts as root and drops privileges with uid/gid, the directory can stay owned by root, and uWSGI creates the socket before dropping privileges.

Enable and start:

sudo systemctl daemon-reload
sudo systemctl enable --now myapp
sudo systemctl status myapp
journalctl -u myapp -n 50 --no-pager

4. Configure Nginx

upstream myapp {
    server unix:/run/myapp/uwsgi.sock;
}

server {
    listen 80;
    server_name www.example.com;

    client_max_body_size 20m;

    location /static/ {
        alias /srv/myapp/app/staticfiles/;
        expires 30d;
    }

    location /media/ {
        alias /srv/myapp/media/;
    }

    location / {
        include uwsgi_params;
        uwsgi_pass myapp;
        uwsgi_read_timeout 65s;
        uwsgi_param HTTP_X_FORWARDED_PROTO $scheme;
    }
}
the socket type decides the Nginx directiveSpeaksNginx usessocket =uwsgi binary protocol✓uwsgi_pass + uwsgi_paramshttp-socket =plain HTTP✓proxy_passhttp =HTTP via a router process!avoid behind NginxMixing them up gives 502 errors or garbled responsesdjango-deployment.com
This guide uses socket with uwsgi_pass over a Unix socket.

The uwsgi_params file shipped with Nginx maps request data such as REQUEST_METHOD, QUERY_STRING, and HTTP_HOST into the uwsgi protocol. Keep uwsgi_read_timeout slightly above harakiri, so uWSGI's timeout fires first and logs the slow request.

The uwsgi_param HTTP_X_FORWARDED_PROTO $scheme line passes the original scheme to Django, where it appears in request.META as HTTP_X_FORWARDED_PROTO. After enabling HTTPS, set:

SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")

Test and reload:

sudo nginx -t && sudo systemctl reload nginx

5. Socket permissions

The socket is owned by myapp:www-data with mode 660. Nginx on Debian and Ubuntu runs its workers as www-data, so it can connect through the group permission. Check:

ls -l /run/myapp/uwsgi.sock
sudo -u www-data test -w /run/myapp/uwsgi.sock && echo "nginx can write"

If Nginx logs connect() to unix:/run/myapp/uwsgi.sock failed (13: Permission denied), the group or mode is wrong, or a parent directory is not traversable.

6. Deploy new code

A release on this stack looks like:

cd /srv/myapp/app
sudo -u myapp git pull
sudo -u myapp /srv/myapp/venv/bin/pip install -r requirements.txt
sudo -u myapp bash -c 'set -a; . /etc/myapp/env; set +a; \
  /srv/myapp/venv/bin/python manage.py migrate --noinput && \
  /srv/myapp/venv/bin/python manage.py collectstatic --noinput'
sudo systemctl reload myapp

The reload replaces workers gracefully. If you changed the uWSGI ini itself (for example the number of processes), use systemctl restart.

7. Verify

curl -I http://127.0.0.1/ -H "Host: www.example.com"
journalctl -u myapp -f
sudo tail -f /var/log/nginx/error.log

Verification check:

  • the journal shows spawned uWSGI master process and the configured number of workers
  • there is no no python application found message
  • a request through Nginx returns the expected status
  • systemctl reload myapp completes without dropped requests in the Nginx error log

Rollback

Keep releases in versioned directories and point a current symlink at the active one, with chdir = /srv/myapp/current in the ini. Rolling back is switching the symlink to the previous release and running systemctl restart myapp, because a graceful reload may keep resolved paths from the old symlink target. Database migrations are not reverted automatically.

Explanation

Both Gunicorn and uWSGI run a master process that manages worker processes importing your Django WSGI application. uWSGI adds its own binary protocol, which Nginx speaks natively with uwsgi_pass, and many runtime controls. The ini above turns on the options that make failures visible (strict, need-app), makes process signals behave the way systemd expects (die-on-term, KillSignal=SIGQUIT), and bounds the damage of slow or leaking requests (harakiri, max-requests, reload-on-rss).

Edge cases / notes

uWSGI or Gunicorn for a new project?

Choose Gunicorn unless you need a specific uWSGI feature. It has fewer options, clear defaults, and active development. The Nginx and systemd patterns transfer almost unchanged.

Threads and connection handling

With threads = 2, each worker can serve two requests at once. Django's database connections are per thread, so the maximum database connections is roughly processes × threads.

Background threads in the app

enable-threads = true is required if your code or libraries start Python threads (for example some SDKs that flush telemetry in the background); without it, those threads do not run.

lazy-apps

By default uWSGI imports the app in the master and forks workers. If a library opens connections or sockets at import time, use lazy-apps = true so each worker imports the app itself.

Troubleshooting uWSGI

Reading the startup log

A healthy start in journalctl -u myapp includes, in order:

  1. *** Starting uWSGI ... with the version
  2. chdir() to /srv/myapp/app
  3. uwsgi socket 0 bound to UNIX address /run/myapp/uwsgi.sock
  4. WSGI app 0 (mountpoint='') ready
  5. spawned uWSGI master process and one spawned uWSGI worker line per process

If step 4 is missing and you see unable to load app 0 or no python application found, the module import failed. Scroll up for the Python traceback. With need-app = true, uWSGI then exits and systemd restarts it according to Restart=on-failure.

Common error messages

uWSGI messages, decodedMessageUsual causeno python application foundmodule or chdir wrong;need-app makes it fatalHARAKIRI ON WORKERrequest exceeded harakiri;slow view or queryPermission denied on the socketgid or chmod-socketdo not include Nginxinvalid request block sizeHTTP sent to a uwsgisocket, or big headersThe first lines of the uWSGI log explain most start-up failuresdjango-deployment.com
buffer-size raises the header limit when large cookies trigger the last error.
MessageCauseFix
unrecognized option or strict mode exittypo or unsupported option namecorrect the option; check uwsgi --help for the installed version
bind(): Permission deniedsocket directory not writableRuntimeDirectory= in the unit, or correct ownership
HARAKIRI ON WORKERa request exceeded harakiri secondsfind the slow view in the logged request line
invalid request block sizeclient sent HTTP to a socket (uwsgi protocol) portuse uwsgi_pass in Nginx, or http-socket for HTTP
!!! uWSGI process ... got Segmentation Fault !!!crash in a C extensiontry lazy-apps = true; check library compatibility with forking

The invalid request block size error is common when testing with curl directly against the uWSGI socket or port: socket speaks the binary uwsgi protocol, not HTTP. Test through Nginx instead, or temporarily add an http-socket = 127.0.0.1:8001 line for local HTTP testing.

Monitoring worker state

uWSGI can expose a stats server with per-worker request counts, memory, and status:

stats = /run/myapp/stats.sock
memory-report = true

Read it with uwsgitop (installed with pip install uwsgitop):

sudo -u myapp /srv/myapp/venv/bin/uwsgitop /run/myapp/stats.sock

Keep the stats socket on a Unix path or localhost only; it reveals internal details.

Migrating from uWSGI to Gunicorn

If you decide to move, the change is contained:

  1. Install Gunicorn in the same virtualenv.
  2. Replace the systemd ExecStart with a Gunicorn command binding to a Unix socket.
  3. Replace include uwsgi_params; uwsgi_pass ...; in Nginx with proxy_pass http://unix:/run/myapp/gunicorn.sock; and the standard proxy_set_header lines.
  4. Map harakiri to Gunicorn's --timeout, processes to --workers, threads to --threads, and max-requests to --max-requests with --max-requests-jitter.

Deploy the switch during a low-traffic window, because the socket path and protocol change at the same time.

Next steps

FAQ

What does "no python application found" mean in uWSGI?

uWSGI started but could not import your WSGI module. Check chdir, module, home, and the traceback above the message. With need-app = true, uWSGI exits instead of continuing without an app.

How do I reload Django code with uWSGI without downtime?

Send SIGHUP to the master process, for example with systemctl reload using ExecReload=/bin/kill -HUP $MAINPID. Workers finish in-flight requests and are replaced with the new code.

Should Nginx talk to uWSGI over HTTP or the uwsgi protocol?

Use the uwsgi protocol with uwsgi_pass and a Unix socket when Nginx and uWSGI are on the same host. It is efficient and avoids exposing an HTTP port.

What is harakiri in uWSGI?

A per-request timeout. If a worker spends longer than the harakiri value on one request, the master kills and replaces it and logs the offending request, which is useful for finding slow endpoints.

Keep going