Setting up Ubuntu 20.04 / PostgreSQL 13 / Django 3.1

This guide starts after the first boot of an Ubuntu 20.04 LTS Server system. I assume you're already familiar with Django (this is not a Hello World Django tutorial).

Groundwork

Run the following commands as root, or prepend them with sudo if that's more your speed.

# Install SSH and enable SSH
apt install ssh -y
 
# Update the system
apt update
apt upgrade -y
reboot
 
# Install Python, Git, and Nginx (Postgres is more complicated). NOTE: openldap-devel is optional if you don't need it for your project.
apt install python3 python3-dev git nginx gcc  -y
 
# Enable the official Postgres repo
sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
apt update
 
# Install the latest version of Postgres (13.1 at time of writing)
apt install postgresql postgresql-server-dev-13 -y
 
# Enable and run Postgres and Nginx
systemctl enable postgresql nginx --now
 
# OPTIONAL packages I use for my own project (for LDAP authentication, and PostGIS for geographic support)
apt install libldap-dev libsasl2-dev postgresql-13-postgis-2.5 -y

Now some Django-specific stuff

# Make a user.  Your Django app will run as this user.
adduser --system --group --home=/opt/django django
# Install virtualenv
apt install python3-virtualenv -y
# You can use sudo to switch to the django user now if you're inclined.
sudo -u django bash
# Create new virtual environment for your Django app
virtualenv /opt/django/venv
# Activate your virtual environment
source /opt/django/venv/bin/activate

Django / Gunicorn / Nginx Proof-of-concept

At this point, we take a break to set up a starter Django project to (re)familiarize ourselves with the Nginx/Gunicorn/Django stack. We can trash the starter project when we're done.

# Install Django and gunicorn
pip install Django gunicorn
# change to django directory
cd /opt/django
# Create a test app
django-admin startproject djangotest
# change to test project directory
cd djangotest
# Run migrations
./manage.py migrate
# Run Django's built-in server (figure out what your external IP address is with ``ip addr``)
 ./manage.py runserver 0.0.0.0:8000

Now go to http://192.168.10.205:8000 (change the IP address obviously). You should get a DisallowedHost at / error. This is normal at this point, but it proves that Django is working.

Edit the djangotest/settings.py file to add the IP address to the ALLOWED_HOSTS list.

Try running the Django test project through gunicorn:

gunicorn djangotest.wsgi -b 192.168.10.205

You should be able to get to the Django test project in your browser. This time it's gunicorn serving up the site instead of Django's built-in server.

Now we'll add Nginx into the mix. First, make sure Nginx is accessible by going to http://192.168.10.205 (default port of 80). You should see the default Nginx Redhat splash page.

Create a file called /etc/nginx/conf.d/test.conf and add the following to it:

server {
    listen 80;
    server_name 192.168.10.205;

    location / {
        proxy_set_header Host $http_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;
        # This is the path to the UNIX socket that gunicorn listens to requests on
        proxy_pass http://unix:/opt/django/run/socket;
    }
}

Now we restart Nginx and also run gunicorn so we can test our new configuration:

# Restart nginx to apply the config
systemctl restart nginx
# Create a folder for gunicorn's socket
mkdir /opt/django/run
# Make sure owner is django user
chown django /opt/django/run
# Give "group" and "others" read/execute permission on the /opt/django folder so that nginx can access the socket file
chmod go+rx /opt/django /opt/django/run
# Change to the test folder
cd /opt/django/djangotest
gunicorn  --workers 3 --bind unix:/opt/django/run/socket djangotest.wsgi

Now visit the website and you should see the default Django page. After we confirm this all works, we can return to getting our real Django site working.

The Real Site

Here are some assumptions I'm making:

  • We're going to use the host/md5 method to authenticate the user
  • The Django database user will be called djangouser
  • The Django database user will be called djangodb

Postgres

First, let's address Postgres' configuration:

# Switch to the postgres user.
su - postgres
# Create the database user for your Django site.  When prompted about making the new user a superuser, I recommend answering yes.  You can remove superuser permissions later.
createuser --interactive --pwprompt
# Create the database, assigning the owner to our djangodb user
createdb -O djangouser djangodb

Edit the /etc/postgresql/13/main/pg_hba.conf file. Underneath the local all all peer line add host djangodb djangouser 127.0.0.1/32 md5. Comment out the two lines that start with host all. In the end your file should look something like this:

# TYPE  DATABASE        USER            ADDRESS                 METHOD

# "local" is for Unix domain socket connections only
local   all             all                                     peer
host    djangodb        djangouser   127.0.0.1/32           md5
# IPv4 local connections:
#host    all             all             127.0.0.1/32            ident
# IPv6 local connections:
#host    all             all             ::1/128                 ident
# Allow replication connections from localhost, by a user with the
# replication privilege.
local   replication     all                                     peer
host    replication     all             127.0.0.1/32            ident
host    replication     all             ::1/128                 ident

Reload Postgres to apply the changes, and try to connect with your Django database user account.

# Reload Postgres
systemctl reload postgresql@13-main.service
# Try to log in
psql -U djangouser -h 127.0.0.1 djangodb

Django

Assumptions:

  • Your Django project is called myproject. It will live in /opt/django/myproject.
  • You have unique Django settings modules for production, development, etc. located in a module called main.
  • Your production settings module is found in main.settings_for_prod_centos8 (so the full path will be /opt/django/myproject/main/settings_for_prod_centos8.py).

Time to get our real Django app running.

# Change to Django directory
cd /opt/django
# Clone your project's repo
git clone https://github/yourname/myproject
# We need pg_config so that we can build PostgreSQL support for Django (the pscyopg2 module)
export PATH=/lib/postgresql/13/bin:$PATH
# Install your requirements
pip install -r myproject/requirements.txt
# Change into your project directory
cd myproject

I assume your projects uses separate files for customizing the settings between development, testing, and production. Update your myproject/settings_for_prod.py and customize it with your database settings. The DATABASES section will look like this:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'djangoudb',
        'USER': 'djangouser',
        'PASSWORD': 'whatever_you_set_earlier',
        'HOST': '127.0.0.1',
        'PORT': 5432,
    }
}

Now we can try running Django's built-in server with your real project.

# Create the static folder
mkdir /opt/django/static
# Create the media folder
mkdir /opt/django/media
# Set DJANGO_SETTINGS_MODULE to your project'
export DJANGO_SETTINGS_MODULE=main.settings_for_prod_centos8
# Run the migrations with the ''--plan'' parameter to see what Django will do to the database.
./manage.py migrate --plan
# If you saw no errors, run the migrations for real
./manage.py migrate
# Run collectstatic
./manage.py collectstatic
# Test your app with the Django's runserver command
./manage.py runserver 0.0.0.0:8000

If you can access your app, congratulations! You did it! But you're not finished yet. Let's get Nginx working with our Django app.

Nginx

# Set the owner of the directory to django
chown django:django /opt/django/run
# Make a copy of our Nginx test.conf file
cp /etc/nginx/conf.d/test.conf /etc/nginx/conf.d/django.conf
# Rename the test.conf file so Nginx doesn't load it (alternatively, delete it)
mv /etc/nginx/conf.d/test.conf /etc/nginx/conf.d/test.unused

Update the /etc/nginx/conf.d/django.conf file to include the /static/ files config:

server {
    listen 80;
    server_name 192.168.10.205;
 
    location /static/ {
        root /opt/django;
    }
 
    location / {
        proxy_set_header Host $http_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_pass http://unix:/opt/django/run/socket;
    }
}

Try running Nginx and Django with this configuration:

# Change to the folder where your app's manage.py lives before running the following commands.
cd /opt/django/myproject
# Restart Nginx
systemctl restart nginx
# Run Gunicorn 
gunicorn  --workers 3 --bind unix:/opt/django/run/socket myproject.wsgi

The site should now load up and work! If it doesn't, try to do it better next time. Stop the gunicorn process.

Systemd Service File

To make Gunicorn/Django start automatically we have to create a Systemd service definition file /etc/systemd/system/django-gunicorn.service.

[Unit]
Description=Django Gunicorn service
After=network.target

[Service]
User=django
Group=www-data
WorkingDirectory=/opt/django/myproject
ExecStart=/opt/django/venv/bin/gunicorn --workers 3 --bind unix:/opt/django/run/socket myproject.wsgi
Environment=DJANGO_SETTINGS_MODULE=myproject.settings_for_prod

[Install]
WantedBy=multi-user.target

Start the service:

systemctl enable django-gunicorn.service --now

And your site should be available! You did it! But you're NOT FINISHED. Time to secure everything!

Securing

If you were to simply turn firewall on, you wouldn't be able to access your site because:

  • Firewalld blocks inbound port 80 and 443
# Reset incoming/outgoing to defaults
ufw default deny incoming
ufw default allow outgoing
 
# Allow SSH (or ufw allow <portnumber> if you've changed the SSH port)
ufw allow ssh
 
# Allow HTTP and TLS
ufw allow http
ufw allow https
 
# Turn on the firewall
ufw enable
 
# If you were running some commands as root, make sure to reset the permissions on the project folder to the django user.
chown -Rv django:django /opt/django/myproject
 
# Remove "group" and "other" permissions from the project folder.  We don't want others to see our settings file!
chmod go-rwx /opt/django/myproject
 
# If you want to check permissions, run the following:
sudo -u nginx cat /opt/django/myproject/myproject/settings_for_prod_centos8.py
# the above command should fail with a "Permission denied" error.
 
# Remove the PostgreSQL superuser permission from the django database user:
su - postgres
psql -d djangodb -c "ALTER USER djangouser WITH NOSUPERUSER;"

Your site should be accessible and somewhat secure. Somewhat? Yeah, you didn't secure SSH, you didn't install fail2ban, you didn't enable TLS, and you didn't set up any backups. You fool. You moron. Go learn how to do that somewhere else.

Final Step

Reboot. If your site comes up and works without any further intervention, congratulations! You did it!


User Tools