Configuring Debian, Nginx, and Gunicorn for a Django Project



Good day to all.

There was a task to raise the Debian server on Nginx for projects Django 3.x. Break a bunch of information on the Internet, I managed to do this by combining recommendations from several different sites. If you are interested in reading how to set up your first server for a Django project, then welcome.

I’ll tell you a little about yourself so that you understand who I am. I am not a developer, not a programmer, not a system administrator, or even have an IT education. I am a computer science teacher. But at work I have to explain to students some points that are very far from the school course in computer science and one of them is the development of projects on Django.

Basic settings


To begin with, we already have a server with Debian 10 installed, there should be no problems installing Debian. I had a clean installation, without any additional settings, so I went to my server via root and started installing some of the main components for me.

apt-get update
apt-get install -y sudo htop git curl wget unzip zip gcc build-essential make
apt-get install -y tree redis-server nginx  libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev llvm libncurses5-dev libncursesw5-dev xz-utils tk-dev libffi-dev liblzma-dev python3-dev python-pil ython3-pil 
apt-get install -y python3-lxml libxslt-dev python-libxml2 python-libxslt1 python-dev gnumeric libpq-dev libxml2-dev libxslt1-dev libjpeg-dev libfreetype6-dev libcurl4-openssl-dev supervisor libgdbm-dev libnss3-dev ufw

You can put all this into one installation, but I got an error, and in this order everything went well.

Next, create a new user and add him to the sudo group so that he can start processes on behalf of the superuser.

adduser username 
usermod -aG sudo username

where username is the username that you will use in the future.
Install the latest version of Python from source. At the time of writing, this was 3.8.2 .

cd /home/username
curl -O https://www.python.org/ftp/python/3.8.2/Python-3.8.2.tar.xz
tar -xf Python-3.8.2.tar.xz
cd Python-3.8.2

Explanation of Actions
cd — , change directory;
curl — URL. ;
tar — .

After downloading the latest version of Python and moving to the source directory, we run:

./configure --enable-optimizations
make -j 2

This will allow us to prepare everything necessary for installing Python. Here, the number 2, this is the number of processor cores. You can find out with the nproc command .
We start the installation.
make altinstall

You can verify that Python has installed by using the command:

python3.8 -V

It will output a version of Python .
After that, we update pip and install the most commonly used packages for Python .
python3.8 -m pip install -U pip
python3.8 -m pip install -U setuptools
python3.8 -m pip install pillow
python3.8 -m pip install virtualenv

Django setup


We restart the system and go under the user you created. Let's go to the / var / www directory and download our project into it, which is uploaded to GitHub .

cd /var/www
sudo git clone LINK_TO_PROJECT

Configure the rights for normal operation with the / var / www directory
sudo chown -R www-data:www-data /var/www
sudo usermod -aG www-data username
sudo chmod go-rwx /var/www
sudo chmod go+x /var/www
sudo chgrp -R www-data /var/www
sudo chmod -R go-rwx /var/www
sudo chmod -R g+rwx /var/www

It may seem that we do certain actions and then cancel them, but the point is that we first remove all permissions for everyone and then assign them to a specific user or group.

We go to our project, create a virtual environment and run it:

cd djangoprojectname/
virtualenv env
source ./env/bin/activate

If everything is activated, there will be a line of the form: (env) username @ server.

We will deliver / update the main packages that we need for our project.

pip install -U pip
pip install -U setuptools
pip install -U pillow

We put Django, Gunicorn and an adapter for PostgreSQL.

pip install django gunicorn psycopg2-binary

You can also put the packages necessary for your project, I use summernote for text:

pip install django-summernote

We set up a firewall, so we need to create an exception for it on port 8000 (we use it by default, if we plan to use another, then specify it):

sudo ufw allow 8000

The required step is to check the server’s health:

python3.8 manage.py runserver 0.0.0.0:8000

And go to your website, DOMAIN_NAME: 8000 , to make sure everything works! But, at the moment, we have not yet configured very much, these are only basic settings, we need to configure the normal operation of the server, connect the statics, etc.
If your site does not start, then you need to delve into the settings (possibly access rights) and see which package has not been installed, everything is very individual.

To complete by pressing the keys: the CTRL + the C .

Then we move on only if your project has started, I highly recommend not moving on if something doesn’t work out. It is better to fix the problem at the initial stage than to tear the server down to the root (I demolished 3 times, and then I started to write this instruction, fixing every action).

Checking the work of Gunicorn:

gunicorn --bind 0.0.0.0:8000 MAINAPPNAME.wsgi

MAINAPPNAME is the name of the main application that contains settings.py .

If the server is working, then move on.

To complete by pressing the keys: the C TRL + the C .

Setting settings.py


When deploying to a production server, you need to disable debug from the project and change several settings, I did this as follows in settings.py of my project.

DEBUG = False
if DEBUG:
    ALLOWED_HOSTS = ['*']
else:
    ALLOWED_HOSTS = ['HOST IP', 'DOMAIN NAIM', 'localhost']
...
STATIC_URL = '/static/'
if DEBUG:
    STATIC_DIR = os.path.join(BASE_DIR, 'static')
    STATICFILES_DIRS = [
        STATIC_DIR,
        '/var/www/static/',
    ]
else:
    STATIC_ROOT = os.path.join(BASE_DIR, 'static/')
    STATICFILES_FINDERS = (
        'django.contrib.staticfiles.finders.FileSystemFinder',
        'django.contrib.staticfiles.finders.AppDirectoriesFinder',
    )
MEDIA_URL = '/media/'

In urls.py, I changed the settings for statics and media.

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
    urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

I do not know how much this is the right decision, and perhaps there are more correct changes to these files. But in this case it is enough for me to change the value of DEBUG in order to switch between development and publication.

Run the command:

python3.8 manage.py collectstatic

To collect statics and deactivate the virtual environment:

deactivate

Configure Gunicorn


Let's open for customization

sudo nano /etc/systemd/system/gunicorn.socket

Let's write a few settings in the file:

[Unit]
Description=gunicorn socket

[Socket]
ListenStream=/run/gunicorn.sock

[Install]
WantedBy=sockets.target

We created the [Unit] section to describe the socket, in the [Socket] section we determined the location of the socket and in the [Install] section we need to install the socket at the right time.

Open the systemd utility file to configure the service:

sudo nano /etc/systemd/system/gunicorn.service

[Unit]
Description=gunicorn daemon
Requires=gunicorn.socket
After=network.target

[Service]
User=username
Group=www-data
WorkingDirectory=/var/www/djangoprojectname
ExecStart=/var/www/djangoprojectname/env/bin/gunicorn \
          --access-logfile - \
          --workers 5 \
          --bind unix:/run/gunicorn.sock \
          myproject.wsgi:application

[Install]
WantedBy=multi-user.target

Do not forget to indicate your user, your project name and your virtual environment.

As explained to me, workers are calculated as the number of processor cores * 2 + 1 .
Now we launch and activate the Gunicorn socket.

sudo systemctl start gunicorn.socket
sudo systemctl enable gunicorn.socket

And be sure to test that everything works!

sudo systemctl status gunicorn.socket

Should display information about the running service.

Press the Q key to exit (required in the English layout).

file /run/gunicorn.sock

The last command should display a message about the file.

If the first sudo systemctl status gunicorn.socket command returns an error, or if the second file /run/gunicorn.sock command reports that the gunicorn.sock file is not in the directory, then the Gunicorn socket could not be created. You need to check the Gunicorn socket logs with the following command:

sudo journalctl -u gunicorn.socket

See what the mistake is and fix it.

Let's move on to testing socket activation.

sudo systemctl status gunicorn

Most likely you will have a record:

Active: inactive (dead)

So far, everything is going well.

Establish a connection to the socket through curl.

curl --unix-socket /run/gunicorn.sock localhost


May throw Bad Request (400) error.

Try instead of localhost to specify the IP of your server , if everything is fine, then most likely this is due to the nginx settings that we have not done yet. Just move on.

And again, request status output:

sudo systemctl status gunicorn

Active: active (running)

If the record is this + there is still a lot of information, but not about errors, then everything is fine.

Otherwise, we look at errors in the log

sudo journalctl -u gunicorn

And restart the gunicorn processes

sudo systemctl daemon-reload
sudo systemctl restart gunicorn

NGINX setup


We will create a new server block for our site and configure it so that when accessing our site in the address bar, the server understands what and where to get it from.

sudo nano /etc/nginx/sites-available/djangoprojectname

server {
    listen 80;
    server_name server_domain_or_IP;

    location = /favicon.ico { access_log off; log_not_found off; }
    location /static/ {
        alias /var/www/djangoprojectname/static/;
    }

    location /media/ {
        alias /var/www/djangoprojectname/media/;
    }

    location / {
        include proxy_params;
        proxy_pass http://unix:/run/gunicorn.sock;
    }
}

In the sever_name section, you can specify multiple addresses separated by spaces.

Test for syntax errors:

sudo nginx -t

If there are no errors, then restart the server and give our firewall the necessary rights:

sudo systemctl restart nginx
sudo ufw allow 'Nginx Full'

You can also remove access to port 8000

sudo ufw delete allow 8000

At this point, everything should work, but it didn't work fine for me until I configured https. Unfortunately, I can’t explain what this is connected with, but the statics were not loaded, although the media files were loaded normally.

HTTPS setup


We will use Cerbot to configure

sudo apt-get install certbot python-certbot-nginx
sudo certbot –nginx

And follow the prompts on the screen. If there are errors, then eliminate and retry. For example, an error may occur if you specify a domain name that is not related to this server.
To automatically renew the certificate, enter the command.

sudo certbot renew --dry-run 

Now we’re testing the site and everything should work!

If the statics still didn’t appear, then try to open some css file indicating the full address before it, if you get a 403 error, then everything is fine, but the access rights problem needs to be experimented with them, try resetting all rights settings for www-data to directory / var / www . If the error is 404, then you need to look towards the location settings in the NGINX settings .

PostgreSQL setup


Set up PostgreSQL and import data from SQLite. If you do not need it, then skip this step, or take only the PostgreSQL settings.

Create a database and user.

sudo -u postgres psql

CREATE DATABASE dbforproject;
CREATE USER projectdbuser WITH PASSWORD 'password';

We will follow the recommendations for the Django project.

We set the default encoding to UTF-8, set the default transaction isolation scheme to “read committed”, to block reading from unconfirmed transactions. Set the time zone to GMT (UTC).

ALTER ROLE projectdbuser SET client_encoding TO 'utf8';
ALTER ROLE projectdbuser SET default_transaction_isolation TO 'read committed';
ALTER ROLE projectdbuser SET timezone TO 'UTC';

After that, you need to provide the user we created with access to administer the new database:

GRANT ALL PRIVILEGES ON DATABASE dbforproject TO projectdbuser;

After executing all the commands, you can close the PostgreSQL dialog using the command:

\q

Now PostgreSQL configuration is complete, now we will configure Django to work correctly with PostreSQL.

First, copy the old data from SQLite (if necessary). To do this, go to your project and launch the virtual environment:

cd /var/www/djangoprojectname
source ./env/bin/activate
cd mainappname/
python3.8 manage.py dumpdata > datadump.json

Change the settings for connecting to the database:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': 'dbforproject',
        'USER': 'projectdbuser',
        'PASSWORD': 'password',
        'HOST': 'localhost',
        'PORT': '',
    }
}

Run the migration for the database:

python3.8 manage.py migrate --run-syncdb

After this shell:

python3 manage.py shell

And write in it:

>>> from django.contrib.contenttypes.models import ContentType
>>> ContentType.objects.all().delete()
>>> quit()

And load the previously uploaded data from SQLite:

python3.8 manage.py loaddata datadump.json

That's all, it all started and works for me, but in case of errors it’s very individual, so I recommend not to skip the steps with the performance testing so that you can understand at what stage the error occurred.

When writing, I used:

  1. www.digitalocean.com/community/tutorials/how-to-set-up-django-with-postgres-nginx-and-gunicorn-on-ubuntu-18-04-ru
  2. vexxhost.com/resources/tutorials/how-to-deploy-django-on-nginx-gunicorn-with-postgres
  3. pythonworld.ru/web/django-ubuntu1604.html

All Articles