Django with Nginx & Gunicorn on Debian 13 (Linode)
This setup uses Nginx as the web server and Gunicorn as the application server to deploy a Django Project. It uses PostgreSQL for the data models, and either PostgreSQL (database cache) or Redis for caching. It assumes that the local environment uses a similar setup.
NOTE
This setup uses a Public Subnet, so the Django server can be accessed over the internet.
NOTE
You can follow the steps in this guide as written, but replace the following placeholders with your own names:
<DJANGO Server IP Address>: Your Linode's Public IP Addressnon_root: Your non-root usernameyour-name: Your name for git commitsyour-email-id: Your email ID for git commits<remote-URL>: Your GitHub repository's SSH clone URLmy-project: Your cloned project directorymy_project: Your Django project package (the folder containingwsgi.py)api.example.com: Your domain nameinfo@example.com: Your admin email address
You should also update the IP addresses and VPC CIDR blocks, to match your VPC settings.
Requests flow through the server like this:
Internet → Nginx (:443, TLS, static files) → /run/gunicorn.sock → Gunicorn (Django, as non_root)(Optional) Create and set up new Django Project
If you don't already have a Django project created, you can use this guide to create and configure one.
Prepare the Project (Local Machine)
Make these changes locally, then commit and push them before setting up the server.
Add Gunicorn as a regular (not development) dependency, so it's installed on the server with uv sync --no-dev:
uv add gunicornNginx terminates HTTPS and forwards requests to Gunicorn over plain HTTP, so Django can't tell on its own that the original request was secure. In settings.py, add this line to the if not is_test: block:
if not is_test:
# Trust Nginx's X-Forwarded-Proto header to detect HTTPS requests
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
...WARNING
Without this line, Django treats every request as http. SECURE_SSL_REDIRECT then redirects every request to https forever, and CSRF checks fail. It's only safe because Nginx always overwrites this header (see the Nginx configuration below), so clients can't fake it.
Setup Linode (Debian) for Django
Launch a Linode
| Parameter | Value |
|---|---|
| Region | in-maa (Chennai) |
| OS | Debian (Debian 13 as of 22-Feb-2026) |
| Plan | Nanode 1 GB (Shared CPU) |
| Label | Give your preferred label (Label can't have spaces) |
| Root Password | Create a Strong Password and store it somewhere safe |
| SSH Keys | You can add an existing SSH key or add this later when you deploy a new server |
| Disk Encryption | Enable |
| VPC | Select the VPC your other servers use (or create one if this is your first server) |
| Subnet | Select a public subnet since Django Server should be accessed via internet |
| Auto-assign a VPC IPv4 | Enable |
| Allow public IPv4 access | Enable |
| Network Interface Type | Linode Interfaces |
| VPC Interface Firewall | Create and assign a Firewall (that allows all outbound and no inbound - configured later in this guide) |
| Backups | Disable (The backups are useful only for databases) |
Upgrade Packages
TIP
Use the LISH Console to connect to the Linode server. The firewall blocks all inbound traffic until you configure it, so you can't SSH in from your local machine yet.
Upgrade the packages on the server:
sudo apt update && sudo apt upgrade -ySet Timezone
Install all locales first to disable locale warnings:
sudo apt install locales-allAll new Linode servers are set to UTC time by default. To change it to IST, use:
timedatectl set-timezone 'Asia/Kolkata'Confirm the date by running the date command in the terminal.
Configure Firewall
Add the following inbound rules to the Django Firewall:
| Rule Purpose | Label | Protocol | Ports | IP / Netmask | Action |
|---|---|---|---|---|---|
| Allow ICMP (ping) traffic from other servers | Choose a label | ICMP | Leave blank | VPC subnet IP range (Ex: 10.0.0.0/16) | Accept |
| Allow HTTP traffic from the internet | Choose a label | TCP | HTTP (80) | All IPv4, All IPv6 | Accept |
| Allow HTTPS traffic from the internet | Choose a label | TCP | HTTPS (443) | All IPv4, All IPv6 | Accept |
| Allow SSH connections from admin systems | Choose a label | TCP | SSH (22) | Admin system's IP address (use /32) | Accept |
NOTE
Port 80 must stay open even after HTTPS is set up: Certbot uses it to renew certificates, and Nginx uses it to redirect http to https.
TIP
If your admin IP address changes (common with home internet connections), SSH will stop connecting. Log in through the LISH Console instead, and update the SSH rule with your new IP address.
Allow Outgoing Email
New Linode accounts block outgoing traffic on the SMTP ports (25, 465, and 587). Django sends error reports to ADMINS and any other emails (such as password resets) over SMTP, so without this step every email fails.
Open a support ticket in the Linode Cloud Manager asking to lift the SMTP restrictions for this Linode. Once support confirms, check that the port your email provider uses (465 in the project settings) is reachable:
timeout 5 bash -c '</dev/tcp/smtpout.secureserver.net/465' && echo "SMTP port open"Disable Root Login
IMPORTANT
The LISH Console does not use SSH, so PermitRootLogin no and PasswordAuthentication no do not apply to it. Anyone with access to your Linode account can reach a root login prompt through LISH using the root password. Your Linode account credentials and 2FA are therefore the real security perimeter for this server - enable 2FA and store the root password in a password manager.
First, create a limited user account:
adduser non_root
# You'll be prompted to provide passwordAdd the new user to the sudo group for administrative privileges:
adduser non_root sudoExit the session and SSH back into the server as your new user (from local Mac machine):
exit
ssh non_root@<DJANGO Server IP Address>Create an SSH directory and add the public key of your local Mac machine to the authorized keys file:
mkdir ~/.ssh && chmod 700 ~/.ssh && vi ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keysDisable Root login and Password Authentication:
sudo vi /etc/ssh/sshd_config
# Set `PermitRootLogin` to `no`
# Set `PasswordAuthentication` to `no`
# Set `AddressFamily` to `inet` (to disable IPv6 connections)Validate the configuration before restarting, and keep your current session open in case something is wrong:
sudo sshd -tConfirm the settings sshd will actually use. Files in /etc/ssh/sshd_config.d/ are included at the top of sshd_config, and the first value read wins, so a drop-in file can silently override your edits:
sudo sshd -T | grep -Ei 'permitrootlogin|passwordauthentication|addressfamily'
# Expected: permitrootlogin no, passwordauthentication no, addressfamily inetFinally, restart the SSH service to apply the changes:
sudo systemctl restart sshTIP
On Debian 13, SSH is socket-activated and the unit is named ssh. If you changed the listening address or port, also restart ssh.socket.
Create SSH Key for the VPC
The PostgreSQL (and Redis) server has no public IP, so you'll administer it over SSH from this server. Create a key pair for it:
ssh-keygen -t ed25519 -C "django-server"Note down this server's public key and its private IP address. The PostgreSQL guide asks for both - the key goes into the database server's authorized_keys, and the IP goes into its firewall rules and pg_hba.conf:
cat ~/.ssh/id_ed25519.pub
# The private IP is the 10.x.x.x address (also shown in the Linode dashboard)
ip -4 -brief addrSetup Database Server
At this point, if you don't have a Postgres Database server ready for production use, you'll need to set one up. You can follow this guide to configure it. In that guide, this Django server is the application server (the "other Linode").
IMPORTANT
When the PostgreSQL guide asks you to install ca.crt as the user your application runs as, run it as non_root on this server. Gunicorn runs Django as non_root, so PostgreSQL clients will find the certificate at /home/non_root/.postgresql/root.crt.
If the PostgreSQL server already exists, give this new server access to it instead:
- Firewall - In the PostgreSQL server's firewall, add this server's private IP (use /32) to the SSH (
22) and PostgreSQL (5432) rules. If it's in a different subnet, add that subnet to the ICMP rule too. pg_hba.conf- Add ahostssl <your-database-name> <your-db-username> <this server's private IP>/32 scram-sha-256line, then runsudo systemctl reload postgresql.- SSH key - Add this server's public key (from the step above) to
~/.ssh/authorized_keysofnon_rooton the PostgreSQL server, using the LISH Console or an existing application server. - CA certificate - Restore
ca.crtfrom your offline storage to this server, then install it withinstall -D -m 644 ca.crt ~/.postgresql/root.crt && rm ca.crt.
(Optional) Redis for Caching
By default, the project uses PostgreSQL as the Django cache (DatabaseCache). If you'd rather use Redis, install it on this server with this guide once the rest of this guide is complete. Redis listens on localhost only, so it needs no firewall rule, TLS certificate, or access from the PostgreSQL server. The guide also covers the Django settings, the .env values, and a connection check.
Setup Django Project
uv
First, install the uv package manager:
curl -LsSf https://astral.sh/uv/install.sh | shLoad uv in every shell:
# Added at the top of ~/.bashrc - Debian's ~/.bashrc stops early for non-interactive shells
# (such as `ssh server 'command'`), so lines at the end would be skipped there
sed -i '1i . "$HOME/.local/bin/env"' ~/.bashrc
source ~/.bashrc
uv --versionNOTE
Unlike mod_wsgi, Gunicorn runs from the project's virtual environment, so the project isn't tied to Debian's system Python. uv installs whichever Python version your project's .python-version and requires-python ask for.
Clone the GitHub project
Create a separate key pair for GitHub:
ssh-keygen -t ed25519 -C "django-server-github" -f ~/.ssh/github_deployTell SSH to use this key for GitHub:
cat >> ~/.ssh/config <<EOF
Host github.com
IdentityFile ~/.ssh/github_deploy
IdentitiesOnly yes
EOF
chmod 600 ~/.ssh/configGrab the generated public key and add it as a Deploy key in your GitHub repository's settings (Settings → Deploy keys → Add deploy key), leaving "Allow write access" unchecked:
cat ~/.ssh/github_deploy.pubTIP
A deploy key only grants read access to a single repository. Don't add this key to your GitHub account settings - an account key gives this server access to every repository you own, so a compromised server would expose all of them.
Install git to manage the GitHub repositories locally:
sudo apt -y install gitConfirm git installation:
git --versionConfigure your git credentials locally:
git config --global user.name "your-name"
git config --global user.email "your-email-id"Clone the GitHub repository in your home directory on the Linux Server:
cd ~ && git clone <remote-URL>Setup Project on the Server
Navigate into your cloned project directory (e.g., cd ~/my-project). Create an .env file with variables that match your local setup but with updated production values:
- Set
DJ_ENVtoPROD. - Set
POSTGRES_HOSTto the PostgreSQL server's private IP (it must match the certificate's SAN exactly). - Set
POSTGRES_SSLMODEtoverify-full, so Django checks the database server's certificate against~/.postgresql/root.crt. - Add the AWS S3 and CloudFront values from this guide. In production, uploaded files (such as profile pictures) are stored in S3, so uploads fail without them.
- If you use Redis for caching, add the Redis values from the Redis guide.
Generate a new secure SECRET_KEY using the following command and add it to your .env file:
python3 -c "import secrets; print(secrets.token_urlsafe(64))"Only non_root needs to read the .env file, so lock it down:
chmod 600 .envCreate the virtual environment and install dependencies. --locked installs exactly what's in uv.lock (and fails if it's missing or out of date - so make sure uv.lock is committed to the repository), and --no-dev skips development-only packages:
uv sync --locked --no-dev
# Confirm Gunicorn is installed
.venv/bin/gunicorn --versionCreate the log directory. Django runs as non_root (both under Gunicorn and for management commands), so it needs to own the directory to create debug.log:
sudo install -d -m 750 -o non_root -g non_root /logsGunicorn workers and management commands write to the same log file from different processes, so rotating it from inside Django is unsafe. In production, the project logs with WatchedFileHandler, and logrotate rotates the file instead:
sudo tee /etc/logrotate.d/django > /dev/null <<'EOF'
/logs/debug.log {
su non_root non_root
create 0640 non_root non_root
weekly
rotate 8
compress
delaycompress
missingok
notifempty
}
EOF
# Check the configuration (dry run)
sudo logrotate -d /etc/logrotate.d/djangoCollect the static files. Keep passing --no-dev to uv run - without it, uv run re-syncs the environment and installs the development packages again:
uv run --no-dev manage.py collectstaticCreate the Django Cache table (skip this if you use Redis for caching):
uv run --no-dev manage.py createcachetableCheck the project for any configuration errors before deploying:
uv run --no-dev manage.py check
uv run --no-dev manage.py check --deployConfirm there are no model changes without a migration (migrations should be created locally and committed, never on the server), then run the migrations:
uv run --no-dev manage.py makemigrations --check --dry-run # Should print "No changes detected"
uv run --no-dev manage.py migrateCreate a Super User, if your application needs one (for example, for admin-only API endpoints):
uv run --no-dev manage.py createsuperuserNOTE
The project's urls.py only enables the Django admin site when DEBUG is on, so there's no admin UI in production. The superuser logs in through your API's JWT endpoints like any other user.
Run Django with Gunicorn
Gunicorn runs as a systemd service. systemd owns the Unix socket (/run/gunicorn.sock) and hands it to Gunicorn, so:
- Only Nginx (
www-data) can connect to the socket, while Django itself runs asnon_root. - While Gunicorn restarts (for example, on a deploy), new requests wait in the socket instead of failing.
Create the socket unit:
sudo tee /etc/systemd/system/gunicorn.socket > /dev/null <<'EOF'
[Unit]
Description=Gunicorn socket for Django
[Socket]
ListenStream=/run/gunicorn.sock
# Gunicorn inherits the socket from systemd, so only Nginx needs access to it
SocketUser=www-data
SocketGroup=www-data
SocketMode=0660
[Install]
WantedBy=sockets.target
EOFCreate the service unit. --workers 3 suits a 1 GB Nanode (a common starting point is 2 × CPU cores + 1; each worker uses roughly 50–100 MB):
sudo tee /etc/systemd/system/gunicorn.service > /dev/null <<'EOF'
[Unit]
Description=Gunicorn (WSGI) for Django
Requires=gunicorn.socket
After=network.target
[Service]
Type=notify
NotifyAccess=main
User=non_root
Group=non_root
WorkingDirectory=/home/non_root/my-project
ExecStart=/home/non_root/my-project/.venv/bin/gunicorn --workers 3 my_project.wsgi:application
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=mixed
TimeoutStopSec=5
PrivateTmp=true
Restart=on-failure
[Install]
WantedBy=multi-user.target
EOFStart Gunicorn now and at every boot:
sudo systemctl daemon-reload
sudo systemctl enable --now gunicorn.socket gunicorn.service
sudo systemctl status gunicornConfirm Django answers on the socket. The Host and X-Forwarded-Proto headers stand in for Nginx, which isn't set up yet:
sudo curl -sI --unix-socket /run/gunicorn.sock \
-H "Host: api.example.com" -H "X-Forwarded-Proto: https" http://localhost/
# Any response from Django (such as 404 for `/` in an API-only project) means it works.
# A 400 means the Host isn't in ALLOWED_HOSTS; a 301 means SECURE_PROXY_SSL_HEADER is missing.TIP
Gunicorn's own output (startup errors, worker crashes) goes to the journal: sudo journalctl -u gunicorn -n 50. Django's application logs still go to /logs/debug.log.
Install Nginx
Install Nginx and Certbot (Debian's certbot package also installs a systemd timer that renews certificates automatically):
sudo apt -y install nginx certbotYou should now see the Nginx default page at http://<DJANGO Server IP Address>.
Grant Access to Static Files
Nginx (running as www-data) serves the static files directly, so it needs to read staticfiles, but nothing else in the project.
Debian 13 creates home directories with mode 700. Let the www-data group pass through (but not list) your home directory and the project directory:
sudo chgrp www-data /home/non_root /home/non_root/my-project
chmod 710 /home/non_root /home/non_root/my-projectRemove access for everyone else from the project, then give the www-data group read access to the static files only:
chmod -R o= /home/non_root/my-project
sudo chown -R non_root:www-data /home/non_root/my-project/staticfiles
chmod -R u=rwX,g=rX,o= /home/non_root/my-project/staticfiles
# New files from `collectstatic` inherit the www-data group
find /home/non_root/my-project/staticfiles -type d -exec chmod g+s {} +Files created later (by git pull, uv sync, etc.) would still be readable by others under Debian's default umask of 022. Set a stricter umask for non_root, so new files are readable only by you and the group:
sed -i '1i umask 027' ~/.bashrc
source ~/.bashrcGet the SSL Certificate
At this point, you must point your domain to the current IP address using Route53 or a similar DNS service. Edit the A records for both domains.
api.example.com
www.api.example.comGet the certificate using the webroot method: Certbot places a file in /var/www/html (which the Nginx default site serves on port 80), and Let's Encrypt fetches it to confirm you own the domain. The --deploy-hook is saved, so Nginx reloads after every renewal to pick up the new certificate:
sudo certbot certonly --webroot -w /var/www/html \
-d api.example.com -d www.api.example.com \
--deploy-hook "systemctl reload nginx"
# Details to be filled as follows:
# * Email: `info@example.com`
# * Terms of Service: `Y`
# * Share Email Address: `N`Confirm that automatic renewal works:
sudo certbot renew --dry-runNOTE
Unlike the Apache guide, Certbot here only gets the certificate (certonly) and doesn't edit the web server configuration. You write the complete Nginx configuration yourself in the next step, and it keeps serving /.well-known/acme-challenge/ from /var/www/html so renewals keep working.
Configure Nginx
Create the site configuration:
sudo vi /etc/nginx/sites-available/my-projectAdd the following configuration:
# Reject requests for any other host (such as the Linode IP address) - they never reach Django
server {
listen 80 default_server;
listen [::]:80 default_server;
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
ssl_reject_handshake on;
return 444;
}
# HTTP: serve Certbot's renewal challenges, and redirect everything else to HTTPS
server {
listen 80;
listen [::]:80;
server_name api.example.com www.api.example.com;
location /.well-known/acme-challenge/ {
root /var/www/html;
}
location / {
return 301 https://api.example.com$request_uri;
}
}
# HTTPS: redirect www to non-www
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name www.api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
return 301 https://api.example.com$request_uri;
}
# HTTPS: the Django site
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
# Nginx's default limit is 1 MB, which is too small for most uploads (such as profile pictures)
client_max_body_size 10M;
location /static/ {
alias /home/non_root/my-project/staticfiles/;
}
location / {
proxy_pass http://unix:/run/gunicorn.sock;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Always overwritten here, so clients can't fake it (see SECURE_PROXY_SSL_HEADER)
proxy_set_header X-Forwarded-Proto $scheme;
}
}WARNING
Never use root pointing at the project directory, or an alias for anything other than staticfiles. Nginx would then serve raw files - including .env, .git, and settings.py. Here, only /static/ is served from disk, and everything else goes to Django.
Replace the default site with yours, check the configuration for errors, and reload Nginx:
sudo ln -s /etc/nginx/sites-available/my-project /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginxNow test the secure endpoint at https://api.example.com/. The www subdomain should correctly redirect to non-www, and all http traffic should force an upgrade to https.
Confirm that project files are not reachable, and that requests using the IP address are rejected (run from your local machine):
# Should return Django's 404 - never the file contents
curl -sI https://api.example.com/.env
# Should fail with "Empty reply from server" (HTTP) and a TLS handshake error (HTTPS)
curl -I http://<DJANGO Server IP Address>/
curl -kI https://<DJANGO Server IP Address>/TIP
Nginx logs to /var/log/nginx/access.log and /var/log/nginx/error.log. A 502 Bad Gateway means Nginx couldn't reach Gunicorn - check sudo systemctl status gunicorn and sudo journalctl -u gunicorn.
Deploy Updates
To deploy new code, run these commands on the server:
cd ~/my-project
git pull
uv sync --locked --no-dev
uv run --no-dev manage.py migrate
uv run --no-dev manage.py collectstatic --noinput
# Restart Django - requests wait in the socket meanwhile, so none are dropped
sudo systemctl restart gunicorn
# If you run the RQ worker from the Redis guide, restart it too
sudo systemctl restart rqworkerNOTE
Restarting Gunicorn fully restarts the Django processes, so changes to .env and upgraded packages are picked up too. Only reload Nginx (sudo nginx -t && sudo systemctl reload nginx) if you changed its configuration.
Keep the Server Patched
This server is exposed to the internet, so install security updates automatically:
sudo apt install -y unattended-upgrades needrestart
sudo dpkg-reconfigure -plow unattended-upgrades # Choose "Yes"unattended-upgrades restarts most services itself, but a new kernel only takes effect after a reboot. Check periodically, and reboot if it reports an outdated kernel:
sudo needrestart -kYou should now have a working Django server, securely connected to your database within the VPC.
(Optional) Add Uvicorn for WebSockets (ASGI)
Gunicorn (WSGI) can't hold WebSocket connections, which features like a chat app need. You don't need to switch the whole site to ASGI: keep regular requests on Gunicorn, and run a second, ASGI service - Gunicorn managing Uvicorn workers - for WebSocket paths (/ws/) only.
/ws/... → /run/asgi.sock → Gunicorn + Uvicorn workers (my_project.asgi)
Everything else → /run/gunicorn.sock → Gunicorn (my_project.wsgi)In the Project (Local Machine)
Add Django Channels, its Redis channel layer, and the Uvicorn worker. The channel layer passes messages between chat users connected to different workers, so it needs the Redis server (and the REDIS_URL defined in its Redis settings):
uv add channels channels-redis uvicorn-worker 'uvicorn[standard]'Add "channels" to INSTALLED_APPS, and the channel layer to settings.py (database 4, so it doesn't mix with the caches and the django-rq queues):
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [f"{REDIS_URL}/4"],
},
},
}NOTE
Channel layer messages expire on their own (after 60 seconds by default), so they can't fill Redis under noeviction.
Route WebSockets in my_project/asgi.py. Your consumers and websocket_urlpatterns (with paths starting with ws/) come from the Channels tutorial:
import os
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.security.websocket import AllowedHostsOriginValidator
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "my_project.settings")
django_asgi_app = get_asgi_application()
# Imported after Django is set up, since consumers use the models
from chat.routing import websocket_urlpatterns # noqa: E402
application = ProtocolTypeRouter(
{
"http": django_asgi_app,
# Only accepts connections from origins in ALLOWED_HOSTS
"websocket": AllowedHostsOriginValidator(
AuthMiddlewareStack(URLRouter(websocket_urlpatterns))
),
}
)Commit and push the changes, then deploy them on the server.
On the Server
Create the ASGI socket and service by copying the Gunicorn units and changing the socket path, application, and worker class:
sed 's#/run/gunicorn.sock#/run/asgi.sock#; s#Gunicorn socket#ASGI socket#' \
/etc/systemd/system/gunicorn.socket | sudo tee /etc/systemd/system/asgi.socket > /dev/null
sed 's#gunicorn.socket#asgi.socket#; s#Gunicorn (WSGI)#Gunicorn + Uvicorn (ASGI)#; s#--workers 3 my_project.wsgi:application#--workers 2 --worker-class uvicorn_worker.UvicornWorker my_project.asgi:application#' \
/etc/systemd/system/gunicorn.service | sudo tee /etc/systemd/system/asgi.service > /dev/null
# Check the changed lines
grep -E 'ListenStream|Requires|ExecStart' /etc/systemd/system/asgi.*
sudo systemctl daemon-reload
sudo systemctl enable --now asgi.socket asgi.serviceIn /etc/nginx/sites-available/my-project, add this location to the Django site's server block (next to location /). WebSockets need HTTP/1.1 and the Upgrade headers, and a long read timeout so idle chats aren't disconnected after Nginx's default 60 seconds:
location /ws/ {
proxy_pass http://unix:/run/asgi.sock;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 1h;
}Check and reload Nginx:
sudo nginx -t && sudo systemctl reload nginxFrom now on, restart both services on every deploy:
sudo systemctl restart gunicorn asgiNOTE
Restarting asgi disconnects open WebSockets. Your frontend should reconnect automatically when a connection closes.
