BlogGuides

Deploy Django with Uvicorn and Nginx: A Beginner's Guide

A beginner's guide to deploying Django with Uvicorn and Nginx on an Ubuntu VPS: SSH keys, Supervisor, a firewall and free HTTPS with Certbot, step by step.

On this page

To deploy Django with Uvicorn and Nginx, you run your project's ASGI app under Uvicorn, keep it alive with Supervisor, and put Nginx in front as a reverse proxy that also serves your static files. Certbot then adds a free Let's Encrypt certificate, and UFW closes every port you do not need. This beginner's guide walks through each command on an Ubuntu VPS, from the first SSH login to HTTPS.

In this guide:

1. Introduction

This guide will walk you through the process of deploying a Django application with ASGI (Uvicorn) to a Virtual Private Server (VPS). We'll also cover setting up a Redis scheduler, configuring Nginx as a reverse proxy, securing your server with a firewall, and enabling HTTPS. This guide is designed for beginners, so we'll explain each step in detail.

2. Prerequisites

Before we begin, make sure you have:

  • A VPS running Ubuntu. This guide was first written on Ubuntu 22.04 and updated where commands differ on newer LTS releases.

  • A domain name (e.g., tymr.online) pointed to your VPS's IP address

  • Your Django project code (including requirements.txt)

  • Basic familiarity with command-line interfaces

3. Setting Up Your VPS

First, we need to access our VPS. We'll use SSH (Secure Shell) for this.

Shell
ssh root@your_server_ip

Replace your_server_ip with your VPS's IP address. You'll be prompted for a password, which you should have received from your VPS provider or in most VPS you can directly login into server after pasting the local desktop's ssh public key into the server during the setup process. for example, in digital ocean you have this feature here while creating a droplet ( VPS ) 馃憞

DigitalOcean's Create Droplet page with the SSH key option selected, so you can log in to the new server without a password

Once logged in, update your system:

Shell
sudo apt update
sudo apt upgrade -y

These commands update the list of available packages and upgrade them to their latest versions.

4. Securing Your Server

Creating a New User

It's best practice to avoid using the root user. Let's create a new user:

Shell
adduser username

Replace username with your desired username. You'll be prompted to set a password and provide some optional information.

Grant this user sudo privileges:

Shell
usermod -aG sudo username

This command adds the user to the sudo group, allowing them to run commands with superuser privileges.

Setting Up SSH Key Authentication

SSH keys are more secure than passwords. On your local machine, generate an SSH key pair:

Shell
ssh-keygen -t ed25519

This creates a public/private key pair. The public key can be freely shared, while the private key must be kept secret. Ed25519 keys are short and fast and are the usual choice today; the older ssh-keygen -t rsa -b 4096 also works.

Copy the public key to your server:

Shell
ssh-copy-id username@your_server_ip

Now, let's disable password authentication for SSH. On the server:

Shell
sudo nano /etc/ssh/sshd_config

Find and modify these lines:

Code
PasswordAuthentication no
PermitRootLogin no

On many cloud images a file in /etc/ssh/sshd_config.d/ (often 50-cloud-init.conf) sets PasswordAuthentication yes, and the first value SSH reads wins. Check with grep -r PasswordAuthentication /etc/ssh/sshd_config.d/ and change it there too.

Save the file (Ctrl+X, then Y, then Enter) and restart the SSH service (on Ubuntu the service is called ssh):

Shell
sudo systemctl restart ssh

Now login as non root user you just created. Keep your current session open until a new login works, so a typo cannot lock you out.

5. Installing Required Software

Let's install the necessary software:

Shell
sudo apt install build-essential python3-dev python3-venv python3-pip nginx redis

This installs Python with its virtual environment module, pip (Python package manager), Nginx (web server) and Redis. The first version of this guide installed python3.11 by name; newer Ubuntu LTS releases ship a newer Python as python3, so use that and check that your Django version supports it.

6. Configuring Your Django Project

Create a directory for your project:

Shell
mkdir ~/myproject
cd ~/myproject

Upload your project files to this directory. You can use SCP, SFTP, or Git for this.

Create a virtual environment:

Shell
python3 -m venv venv
source venv/bin/activate

Install your project dependencies:

Shell
pip install -r requirements.txt

Also, install Uvicorn:

Shell
pip install uvicorn

Before going further, set DEBUG = False, your domain in ALLOWED_HOSTS, and CSRF_TRUSTED_ORIGINS = ["https://tymr.online"] in your settings, then run python manage.py check --deploy. The Django deployment checklist explains every warning it prints.

Understanding and Setting Up User Permissions for our Django project

When deploying a Django application, proper file permissions and ownership are crucial for security and functionality. Here's a detailed guide tailored for beginners:

Understanding Users and Groups

In our setup, we'll work with three main entities:

  1. Your user account (e.g., 'normaluser')

  2. The web server user (usually 'www-data' for Nginx on Ubuntu)

  3. The root user

Best Practices for Django Project Files

  1. Project Directory Ownership: Your user should own the project directory and most files.

    Shell
     sudo chown -R normaluser:normaluser /home/normaluser/myproject
    
  2. Project Directory Permissions: Set 755 (drwxr-xr-x) for directories and 644 (-rw-r--r--) for files.

    Shell
     find /home/normaluser/myproject -type d -exec chmod 755 {} \;
     find /home/normaluser/myproject -type f -exec chmod 644 {} \;
    
  3. Sensitive Files: For files like settings.py, use more restrictive permissions.

    Shell
     chmod 600 /home/normaluser/myproject/myproject/settings.py
    
  4. Static and Media Directories: The web server needs read access to these.

    Shell
     sudo chown -R normaluser:www-data /home/normaluser/myproject/static
     sudo chown -R normaluser:www-data /home/normaluser/myproject/media
     sudo chmod -R 750 /home/normaluser/myproject/static
     sudo chmod -R 750 /home/normaluser/myproject/media
    
  5. Log Directory: Create a logs directory within your project and set appropriate permissions.

    Shell
     mkdir -p /home/normaluser/myproject/logs
     sudo chown -R normaluser:www-data /home/normaluser/myproject/logs
     sudo chmod -R 770 /home/normaluser/myproject/logs
    
  6. SQLite Database (if used): If you're using SQLite, secure the database file.

    Shell
     sudo chown normaluser:www-data /home/normaluser/myproject/db.sqlite3
     sudo chmod 660 /home/normaluser/myproject/db.sqlite3
    
  7. Virtual Environment: Keep your virtual environment secure.

    Shell
     chmod -R 750 /home/normaluser/myproject/venv
    

Explanation of Permissions

  • 755 for directories: Owner can read/write/execute, others can read/execute.

  • 644 for regular files: Owner can read/write, others can read.

  • 600 for sensitive files: Only owner can read/write.

  • 750 for static/media: Owner can read/write/execute, group (www-data) can read/execute.

  • 770 for logs: Owner and group can read/write/execute.

  • 660 for SQLite: Owner and group can read/write.

Security Best Practices

  1. Principle of Least Privilege: Give only the permissions necessary for each file and directory.

  2. Use a Non-Root User: Always run your Django application as a non-root user.

  3. Separate Media Uploads: If possible, store user-uploaded files outside of the project directory.

  4. Regular Audits: Periodically check and update file permissions.

  5. Use Environment Variables: Store sensitive information like secret keys in environment variables, not in files.

  6. Secure the Secret Key: Ensure Django's SECRET_KEY is not in version control and has restricted access.

  7. Backup Permissions: When backing up, preserve file permissions.

Applying Changes

After setting permissions, restart your web server and Django application:

Shell
sudo systemctl restart nginx
sudo supervisorctl restart all

Verifying Permissions

You can verify permissions using the ls -l command. For example:

Shell
ls -l /home/normaluser/myproject

This will show you the permissions for files and directories in your project root.

Remember, these settings are a starting point. You may need to adjust based on your specific setup and security requirements. Always test thoroughly after changing permissions to ensure your application functions correctly.

7. Nginx Configuration

Before we set up our Nginx configuration, let's remove the default configuration:

Shell
sudo rm /etc/nginx/sites-enabled/default

Now, let's create our configuration file:

Shell
sudo nano /etc/nginx/sites-available/myproject

Add the following content:

Nginx
server {
    # replace your domain names with placeholder tymr.online
    server_name tymr.online www.tymr.online;  # Your domain name(s)

    location = /favicon.ico {
        access_log off;
        log_not_found off;
    }  # Efficiently handle favicon requests

    location /static/ {
        root /home/username/myproject;  # Path to your static files
    }  # Serve static files directly

    location / {
        proxy_pass http://unix:/home/username/myproject/myproject.sock;  # Pass requests to Uvicorn
        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-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }  # Proxy settings for Uvicorn
}

Let's break this down:

  • server_name: Specifies which domain names this server block should respond to.

  • location = /favicon.ico: Efficiently handles favicon requests.

  • location /static/: Configures Nginx to serve static files directly, without passing the request to Django.

  • location /: The main block that handles all other requests.

    • proxy_pass: Forwards requests to our Uvicorn server.

    • The proxy_set_header directives: Pass necessary headers to the Django application.

After creating this file, enable it:

Shell
sudo ln -s /etc/nginx/sites-available/myproject /etc/nginx/sites-enabled

This creates a symbolic link, effectively enabling our configuration.

Test the Nginx configuration:

Shell
sudo nginx -t

If it's okay, restart Nginx:

Shell
sudo systemctl restart nginx

8. Setting Up SSL with Let's Encrypt

We'll use Snap to install Certbot, which automates the process of obtaining and renewing Let's Encrypt certificates. Before you start, the domain's A record must point at the server; the guide to adding an A record for your domain shows what that record looks like.

  1. Remove any old Certbot installations:

    Shell
     sudo apt remove certbot
    
  2. Install Certbot via Snap:

    Shell
     sudo snap install --classic certbot
    

    The --classic flag allows Certbot to access system resources outside of the snap's confined environment.

  3. Create a symbolic link for easy access:

    Shell
     sudo ln -s /snap/bin/certbot /usr/local/bin/certbot
    
  4. Obtain and install the certificate:

    Shell
     sudo certbot --nginx -d tymr.online -d www.tymr.online
    

    This command will:

    • Obtain a certificate for your domains

    • Automatically configure Nginx to use the new certificate

    • Set up automatic renewal

  5. Test that renewal works:

    Shell
     sudo certbot renew --dry-run
    

Follow the prompts during the Certbot execution. It will ask for your email and agreement to terms of service.

The steps follow Certbot's official instructions for Nginx, which are the place to check if a command changes.

9. Configuring Supervisor

Supervisor is a process control system that allows you to monitor and control a number of processes on UNIX-like operating systems. We use Supervisor because:

  1. It automatically restarts processes if they crash.

  2. It starts processes on system boot.

  3. It provides an easy way to start, stop, and monitor processes.

  4. It logs stdout and stderr of the processes it manages.

Here's how to set it up:

  1. Install Supervisor:

    Shell
     sudo apt install supervisor
    
  2. Create a configuration file:

    Shell
     sudo nano /etc/supervisor/conf.d/myproject.conf
    
  3. Add the following content:

INI
[program:myproject]
command=/home/username/myproject/venv/bin/uvicorn myproject.asgi:application --uds /home/username/myproject/myproject.sock
directory=/home/username/myproject
user=username
autostart=true
autorestart=true
stderr_logfile=/var/log/myproject.err.log
stdout_logfile=/var/log/myproject.out.log

[program:celery]
command=/home/username/myproject/venv/bin/celery -A myproject worker -l info
directory=/home/username/myproject
user=username
autostart=true
autorestart=true
stderr_logfile=/var/log/celery.err.log
stdout_logfile=/var/log/celery.out.log

[program:redis_scheduler]
command=/home/username/myproject/venv/bin/python manage.py redis_scheduler
directory=/home/username/myproject
user=username
autostart=true
autorestart=true
stderr_logfile=/var/log/redis_scheduler.err.log
stdout_logfile=/var/log/redis_scheduler.out.log

Uvicorn's option for a Unix socket is --uds (the first version of this guide said --unix-socket, which Uvicorn does not accept). Django's guide to running Uvicorn also shows how to run several worker processes.

This configuration sets up three processes:

  • Your Django application running with Uvicorn

  • A Celery worker for background tasks

  • Your custom Redis scheduler

  1. Reload Supervisor to apply changes:

    Shell
     sudo supervisorctl reread
     sudo supervisorctl update
    

Supervisor is different from simple systemd services because it provides more fine-grained control and is specifically designed for managing application processes, making it ideal for complex setups like Django projects with multiple components.

10. Firewall Configuration

UFW (Uncomplicated Firewall) is a user-friendly interface for managing iptables, the default firewall management tool for Ubuntu.

Allow SSH first, then the web ports, and only then enable the firewall, so you never cut off your own SSH session:

Shell
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable

These commands allow SSH connections and both HTTP and HTTPS traffic for Nginx.

Check the status:

Shell
sudo ufw status

Final List of UFW Commands:

Shell
sudo ufw allow OpenSSH # You need to allow SSH so you can access your VPS remotely.
sudo ufw allow 'Nginx HTTP'
sudo ufw allow 'Nginx HTTPS'
sudo ufw allow 8000  # Optional, for development server
sudo ufw enable
sudo ufw status

Open port 8000 only while you test with the development server, and close it again with sudo ufw delete allow 8000: in production every request should go through Nginx.

11. Deploying Your Django Application

Collect static files:

Shell
python manage.py collectstatic

Apply migrations:

Shell
python manage.py migrate

12. Running the Redis Scheduler

The Redis scheduler should now be managed by Supervisor. You can check its status with:

Shell
sudo supervisorctl status

13. Custom Scripts for Process Management

To simplify starting and stopping all processes, you can create custom scripts. Here are examples:

  1. Create a start script:

    Shell
     nano ~/start_all.sh
    

    Add the following content:

    Shell
     #!/bin/bash
     sudo supervisorctl start all
     sudo systemctl start nginx
    
  2. Create a stop script:

    Shell
     nano ~/stop_all.sh
    

    Add the following content:

    Shell
     #!/bin/bash
     sudo supervisorctl stop all
     sudo systemctl stop nginx
    
  3. Make these scripts executable:

    Shell
     chmod +x ~/start_all.sh ~/stop_all.sh
    

Now you can start all processes with ./start_all.sh and stop them with ./stop_all.sh.

Remember to run these scripts with sudo if your user doesn't have the necessary permissions.

14. Monitoring and Maintenance

Regularly update your system:

Shell
sudo apt update
sudo apt upgrade

Monitor your logs:

Shell
tail -f /var/log/myproject.err.log
tail -f /var/log/myproject.out.log
tail -f /var/log/redis_scheduler.err.log
tail -f /var/log/redis_scheduler.out.log

Bots will try your SSH port and your Django admin login within days. Protecting the Django admin login with Fail2Ban shows how to ban the repeat offenders.

15. Troubleshooting

  • If your site isn't accessible, check Nginx status: sudo systemctl status nginx

  • If Uvicorn isn't running, check Supervisor: sudo supervisorctl status

  • Check Nginx error logs: sudo tail -f /var/log/nginx/error.log

  • Ensure your firewall isn't blocking connections: sudo ufw status

  • 502 Bad Gateway: Nginx cannot reach Uvicorn's socket. Check that the program is running in sudo supervisorctl status and that the socket path in the Nginx config matches the one after --uds.

  • 403 Forbidden on static files, or a 502 with "Permission denied" in the Nginx log: since Ubuntu 21.04, home directories are created with mode 750, so the www-data user cannot enter /home/username. Give it access with sudo chmod 755 /home/username, or keep the project outside your home directory.

  • Bad Request (400): the domain is missing from ALLOWED_HOSTS.

  • CSRF verification failed on forms over HTTPS: add https:// plus your domain to CSRF_TRUSTED_ORIGINS.

What this setup does not cover yet

Your site is live, but three jobs are still on you:

  • Backups. Nothing saves the database yet. Schedule a daily dump, copy it off the server, and try a restore once.

  • Updates without downtime. Restarting Uvicorn after a git pull drops the requests in flight, and a broken commit takes the site down.

  • A safe place to test. A staging copy with its own database, so a migration never runs first on real data.

If your project has a Vue.js frontend, the Django and Vue.js deployment guide covers serving both from one VPS. And if you would rather skip the Nginx and Supervisor setup entirely, deploying Django without Docker using gunicorn, systemd and Caddy shows a shorter stack and how ox automates it.

16. Conclusion

You've now deployed your Django application with Uvicorn, set up Nginx as a reverse proxy, configured SSL, and set up a Redis scheduler. Your application should be accessible via HTTPS at your domain.

Remember to keep your system and applications updated, monitor your logs, and maintain good security practices.

Key takeaways

  • Uvicorn runs Django's ASGI app; Nginx sits in front as the reverse proxy and serves static files.
  • Supervisor keeps Uvicorn, Celery and the scheduler running and restarts them if they crash.
  • Lock down SSH with keys, allow SSH in UFW before enabling it, and check the cloud-init SSH override.
  • Certbot gives free HTTPS and renews it; test renewal with certbot renew --dry-run.
  • Most first-deploy errors are a socket path, home directory permissions, or ALLOWED_HOSTS.

Frequently asked questions

Should I use Uvicorn or Gunicorn to run Django?

Use Uvicorn when your project relies on ASGI features such as async views or websockets, as this guide does. For a classic synchronous Django app, Gunicorn is the most common choice. Both sit behind Nginx the same way, and Gunicorn can also run Uvicorn workers if you want both process management and ASGI.

Do I need Supervisor if Ubuntu already has systemd?

No, systemd can also start your app at boot and restart it after a crash. This guide uses Supervisor because it groups the Django app, the Celery worker and the scheduler in one file with simple start, stop and status commands. Either tool works; pick one and manage every process with it.

Does Certbot renew the Let's Encrypt certificate automatically?

Yes. The snap version of Certbot installs a timer that renews certificates before they expire, and the Nginx plugin reloads Nginx after each renewal. Run sudo certbot renew --dry-run once after setup to prove renewal works. If it fails, check that port 80 is open and the domain still points at the server.

Why does Nginx show 502 Bad Gateway for my Django app?

Nginx could not get an answer from Uvicorn. Usually Uvicorn is not running, the socket path in proxy_pass differs from the one Uvicorn listens on, or Nginx cannot enter your home directory. Check sudo supervisorctl status, then /var/log/nginx/error.log, which names the exact path and error.

Next step

Once HTTPS works, set up a daily database backup before anything else, then add Fail2Ban for the admin login. If you want those jobs handled for you on the same kind of server, read how ox deploys Django without Docker.

Updated : answer-first summary, current SSH, Python and Certbot commands, the Uvicorn --uds fix, a safer firewall order, common errors, and a FAQ.

First published on blog.sorv.dev.

Keep reading

Deploy to a server you own

ox sets up systemd, Caddy and PostgreSQL on your Ubuntu server and deploys on every push. Free during the beta.

Sign up with GitHub Read the docs