Using Django with Nginx, PostgreSQL, and Gunicorn on Debian 11

Updated on November 23, 2022
Using Django with Nginx, PostgreSQL, and Gunicorn on Debian 11 header image

Introduction

Django is a free and open-source Python web framework. This article provides a step-by-step guide to setting up a Django project on a Debian 11 server with Nginx, Gunicorn, and a free Let's Encrypt TLS certificate.

Prerequisites

  • Deploy a new Debain 11 server at Vultr
  • Create a sudo user. The example user is django_user
  • Update the Server
  • Point a domain name to the server. A domain name is required for Let's Encrypt SSL/TLS certificates. This guide uses the apex domain example.com and the fully-qualified domain name mydjango.example.com, and assumes the same IP address is assigned to both names.

1. Install Dependencies

  1. Log in to the server with a non-root sudo user via SSH.

     $ ssh django_user@mydjango.example.com
  2. Install UFW, Vim, and Nginx with the apt package manager.

     $ sudo apt -y install ufw vim nginx
  3. Allow all outbound traffic.

     $ sudo ufw default allow outgoing
  4. Block all inbound traffic except SSH (port 22), HTTP (port 80), and HTTPS (port 443).

     $ sudo ufw default deny incoming
     $ sudo ufw allow ssh
     $ sudo ufw allow http
     $ sudo ufw allow https
  5. Enable and reload UFW.

     $ sudo ufw enable
     $ sudo ufw reload

2. Install PostgresSQL

  1. Install PostgreSQL with the apt package manager, a postgres user is created during the installation.

     $ sudo apt install -y postgresql postgresql-contrib
  2. Switch to the postgres user.

     $ sudo su - postgres
  3. Log in to PostgresSQL.

     $ psql
  4. Create a PostgresSQL database role for the Django project.

     postgres=# CREATE ROLE dbuser WITH LOGIN;
  5. Set password for dbuser role.

     postgres=# \password dbuser
  6. Set dbuser encoding to UTF-8.

     postgres=# ALTER ROLE dbuser SET client_encoding TO 'utf8';
  7. Set dbuser default transaction isolation to read commit.

     postgres=# ALTER ROLE dbuser SET default_transaction_isolation TO 'read committed';
  8. Set dbuser timezone to UTC.

     postgres=# ALTER ROLE dbuser SET timezone TO 'UTC';
  9. Create the Django project database.

     postgres=# CREATE DATABASE appdb;
  10. Grant the dbuser all privileges to appdb.

    postgres=# GRANT ALL PRIVILEGES ON DATABASE appdb TO dbuser;
  11. Exit the postgres client.

    postgres=# \q
  12. Exit to sudo

    exit
  13. Restart the PostgresSQL server.

    $ sudo systemctl restart postgresql

3. Setup the Python Environment

A Python virtual environment should be used when deploying Python web apps.

  1. Install the Python venv.

     $ sudo apt -y install python3-venv
  2. Create app dir, where the virtual environment will be, and the app files. example app_dir. Change into the home directory.

     $ cd ~

    Create the directory.

     $ mkdir app_dir
  3. Change to app dir and create the virtual environment.

     $ cd app_dir
     $ python3 -m venv venv
  4. Activate the virtual environment.

     $ source venv/bin/activate
  5. Install Python PostgresSQL library psycopg2-binary instead of the psycopg2, as psycopg2 will need to be built during installation.

     $ pip install psycopg2-binary
  6. Install Django and Gunicorn.

     $ pip install django gunicorn

4. Upload or Create the Django Project

  1. The project source code needs to be uploaded to the app directory app_dir with git or scp, for this article, create a sample project called example_app.

     $ django-admin startproject example_app .
  2. Check the folder structure of the app_dir.

     $ ls
     example_app manage.py venv
  3. Create a folder to hold the static files of the Django project inside the app folder.

     $ mkdir static
  4. Open the settings.py in example_app with Vim to configure the project.

     $ vim example_app/settings.py
  5. Configure the allow host to the domain.

     ALLOWED_HOSTS = ['mydjango.example.com']
  6. Configure the DATABASES variable with the details created from section 2.

     DATABASES = {
         "default" : {
             "ENGINE" : "django.db.backends.postgresql",
             "NAME" : "appdb",
             "USER" : "dbuser",
    
             "PASSWORD" : "dbpassword",
    
             "HOST" : "127.0.0.1",              
             "PORT" : "5432",
    
             }
     }
  7. Set the static folder path. Look for STATIC_URL and underneath it add STATIC_ROOT.

     STATIC_ROOT = "/home/django_user/app_dir/static"
  8. Save and close the settings.py file.

  9. Create the project migrations.

     $ python manage.py makemigrations
  10. Run the migrations.

    $ python manage.py migrate
  11. Copy all the static files of the project to the static folder. Type yes when prompted.

    $ python manage.py collectstatic
  12. Create an admin user for the project.

    $ python manage.py createsuperuser
  13. Allow port 8000 on UFW.

    $ sudo ufw allow 8000
  14. Test the project to make sure it runs without errors.

    $ python manage.py runserver 0.0.0.0:8000
    
    Watching for file changes with StatReloader
    Performing system checks...
    
    System check identified no issues (0 silenced).
    November 16, 2022 - 21:19:26
    Django version 4.1.3, using settings 'example_app.settings'
    Starting development server at http://0.0.0.0:8000/
    Quit the server with CONTROL-C.

5. Configure Gunicorn

A Gunicorn service will be created to serve the project and start the project on server restarts.

  1. Test Gunicorn to see if it can serve the project without errors.

     $ gunicorn --bind 0.0.0.0:8000 example_app.wsgi
  2. Press Ctrl+C to stop it.

  3. Deactivate the virtual environment

     $ deactivate
  4. Edit the settings.py file with Vim.

     $ vim example_app/settings.py
  5. Disable debug. Look for the variable DEBUG and set it to False.

     DEBUG = False
  6. Save and close the settings.py file.

  7. Deny incoming traffic to port 8000 with UFW.

     $ sudo ufw deny 8000
  8. Create a django_app.service file inside /etc/systemd/system directory.

     $ sudo vim /etc/systemd/system/django_app.service
  9. Write the following into the file.

     [Unit]
    
     Description=Gunicorn for the Django project
     After=network.target
    
     [Service]
    
     User=django_user
     Group=www-data
    
    
     WorkingDirectory=/home/django_user/app_dir
     Environment="PATH=/home/django_user/app_dir/venv/bin"
     ExecStart=/home/django_user/app_dir/venv/bin/gunicorn --workers 2 --bind unix:django_app.sock example_app.wsgi
    
    
     [Install]
    
     WantedBy=multi-user.target

    * Under [Service], USER=django_user sets the user of the service, and Group=www-data sets the group of the service to the Nginx group.

    • WorkingDirectory=/home/django_user/app_dir sets the service working directory to the projects folder.
    • Environment="PATH=/home/django_user/app_dir/venv/bin" sets the virtualenv that the service should use.
    • ExecStart=/home/django_user/app_dir/venv/bin/gunicorn --workers 2 --bind unix:django_app.sock example_app.wsgi run gunicorn worker services for the project and binds them to a Unix socket for faster data exchange for the Nginx proxy server.
  10. Save and close the file.

  11. Enable the service to start at boot.

    $ sudo systemctl enable django_app
  12. Start the service.

    $ sudo systemctl start django_app

6. Configure Nginx

An Nginx proxy server is needed to serve Django apps in production and to handle SSL certificates.

  1. Configure a server block in Nginx by creating a file django_app in /etc/nginx/sites-available.

     $ sudo vim /etc/nginx/sites-available/django_app
  2. Write the following into the file.

     server {
         # listen to port 80 (HTTP)
         listen 80;
         # listen to the domain name
         server_name example.com mydjango.example.com;
    
         location /static/ {
             alias /home/django_user/app_dir/static/;
             expires max;
         }
    
         location / {
             include proxy_params;
    
             # pass the request to the project service sock
             proxy_pass http://unix:/home/django_user/app_dir/django_app.sock;
         }
     }
  3. Save and close the file.

  4. Create a link for django_app to Nginx's sites-enabled.

     $ sudo ln -s /etc/nginx/sites-available/django_app /etc/nginx/sites-enabled
  5. Check the Nginx configuration for any errors.

     $ sudo nginx -t
  6. Restart the Nginx server.

     $ sudo systemctl reload nginx

7. (Optional) Add HTTPS with Let's Encrypt

If you configured a domain in the beginning, then you can add an SSL/TLS certificate with Let's Encrypt to provide HTTPS support.

  1. Install Certbot with the Nginx plugin.

     $ sudo apt install certbot python3-certbot-nginx
  2. Configure Certbot with Nginx.

     $ sudo certbot --nginx -d example.com -d mydjango.example.com

    The first time running Certbot, an admin email for the certificates will be requested. Certbot will also configure Nginx and redirect all HTTP requests to HTTPS by configuring the server block.

     Saving debug log to /var/log/letsencrypt/letsencrypt.log
     Plugins selected: Authenticator nginx, Installer nginx
     Enter email address (used for urgent renewal and security notices)
      (Enter 'c' to cancel): me@example.com
    
     - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     Please read the Terms of Service at
     https://letsencrypt.org/documents/LE-SA-v1.3-September-21-2022.pdf. You must
     agree in order to register with the ACME server. Do you agree?
     - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     (Y)es/(N)o: y
    
     - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     Would you be willing, once your first certificate is successfully issued, to
     share your email address with the Electronic Frontier Foundation, a founding
     partner of the Let's Encrypt project and the non-profit organization that
     develops Certbot? We'd like to send you email about our work encrypting the web,
     EFF news, campaigns, and ways to support digital freedom.
     - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     (Y)es/(N)o: y
     Account registered.
     Requesting a certificate for example.com and mydjango.example.com
     Performing the following challenges:
     http-01 challenge for example.com
     http-01 challenge for mydjango.example.com
     Waiting for verification...
     Cleaning up challenges
     Deploying Certificate to VirtualHost /etc/nginx/sites-enabled/django_app
     Deploying Certificate to VirtualHost /etc/nginx/sites-enabled/django_app
     Redirecting all traffic on port 80 to ssl in /etc/nginx/sites-enabled/django_app
     Redirecting all traffic on port 80 to ssl in /etc/nginx/sites-enabled/django_app
    
     - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     Congratulations! You have successfully enabled https://example.com and
     https://mydjango.example.com
     - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     Subscribe to the EFF mailing list (email: me@example.com).
    
     IMPORTANT NOTES:
      - Congratulations! Your certificate and chain have been saved at:
        /etc/letsencrypt/live/example.com/fullchain.pem
        Your key file has been saved at:
        /etc/letsencrypt/live/example.com/privkey.pem
        Your certificate will expire on 2023-02-15. To obtain a new or
        tweaked version of this certificate in the future, simply run
        certbot again with the "certonly" option. To non-interactively
        renew *all* of your certificates, run "certbot renew"
      - If you like Certbot, please consider supporting our work by:
    
        Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
        Donating to EFF:                    https://eff.org/donate-le
  3. Restart Nginx.

     $ sudo systemctl reload nginx
  • Visit the domain of the server mydjango.example.com/admin in your browser.

You now have a working Django site on Debian 11.