Travis Bartlett

Nginx, Django & uWSGI on CentOS

Published Jul 14, 2020HacksTravis Bartlett
image

Getting Started

This post will cover the deployment of a Djano project with Nginx and uWSGI on CentOS 7. uWSGI will be running in Emperor mode to make the deployment of future projects easier. uWSGI will communicate with Nginx through a unix socket. The post will also cover using Let's Encrypt and SELinux.

Start by updating your system and installing Nginx, Python3.6, as well as a few other required packages. uWSGI will be installed later in the guide within our virtualenv.

yum -y update
yum -y install yum-utils
yum -y groupinstall development
yum -y install epel-release # Extra Packages for Enterprise Linux repository
yum -y install nginx 
yum -y install https://centos7.iuscommunity.org/ius-release.rpm
yum -y install python36u # installs alongside python2.7
yum -y install python36u-devel
yum -y install certbot-nginx # for use with letsencrypt

Python

Create Virtual Environment

Create a directory to house your virtual python enviroments. This will make it easier to manage dependencies between multiple Django projects in the future.

Create a virtual envrironment for your project, for this example our venv will be named blog

mkdir /opt/python-venv
python3.6 -m venv /opt/python-venv/blog

Activate the demo virtual environtment with: 

source /opt/python-venv/blog/bin/activate

Install any dependencies that are needed for your project. Minimum requirements are:

pip install django
pip install uwsgi

Adding your Django Project

Your Django project will live in /var/www. It’s useful to create directory that will house your django project. For this tutorial we’ll use django_demo

mkdir /var/www/django_demo

You can clone an existing Django project from gitlab or create a new one. Start by changing into the /var/www/django_demo directory then clone your project.

source /opt/python-venv/blog/bin/activate # activate the virtual environment
git clone https://github.com/example/your-project.git
# be sure to install any dependencies with pip, you may also need to configure/connect to a database

OR create a new project:

source /opt/python-venv/blog/bin/activate # activate the virtual environment
django-admin startproject blog

You’ll need a directory to store the Nginx error and access logs for your project. There are plenty of ways this can be accomplished, but I prefer to store the logs within my /var/www/django_demo folder.

mkdir /var/www/django_demo/logs
touch /var/www/django_demo/logs/access.log
touch /var/www/django_demo/logs/error/error.log

Lastly, change the owner and group of /var/www/django_demo to nginx.

chown -Rv nginx:nginx /var/www/django_demo

The remainder of this guide will use blog as the Django Project name. If your project is named differently you'll need to make the necessary changes to the configurations below.

Uwsgi Configuration

Create a directory for uwsgi configuation files to live:

mkdir /etc/uwsgi
mkdir /etc/uwsgi/vassals # stores all instances that will run by the uwsgi emperor
touch /etc/uwsgi/emperor.ini # runs all vassals found in /etc/uwsgi/vassals
touch /etc/uwsgi/vassals/blog.ini # used to serve your Django project
mkdir /opt/uwsgi # location used for socket uwsgi files

Open the /etc/uwsgi/emperor.ini file with your preferred text editor and add the following lines (making changes to match your file paths if necessary):

[uwsgi]
emperor = /etc/uwsgi/vassals
uid = uwsgi
gid = nginx
logto = /var/log/uwsgi.log

Save the emperor.ini file.

Open the /etc/uwsgi/vassals/blog.ini file and add the following (make any necessary changes to accomodate file paths, ports, etc.):

[uwsgi]
http = :5000
socket = /opt/uwsgi/blog.sock
chdir = /var/www/django_demo/blog
pythonpath = /var/www/django_demo/blog/blog
home = /opt/python-venv/blog_venv
module = blog.wsgi
uid = uwsgi
gid = nginx
chmod-socket = 664
chown-socket = uwsgi:nginx

Save the blog.ini file.

Create the uwsgi user

This user will run the uwsgi.service. The below command will create the wsgi user with no shell account - meaning the account can’t be used to login to the system.

useradd -s /bin/false -r uwsgi

Create the uwsgi service

To ensure the uwsgi service starts at boot and runs in emperor mode you’ll need to create a service file.

touch /etc/systemd/system/uwsgi.service

Open the /etc/systemd/system/uwsgi.service file and add the following:

[Unit]
Description = uWSGI Emperor
After = syslog.target

[Service]
ExecStart = /opt/python-venv/blog/bin/uwsgi --ini /etc/uwsgi/emperpor.ini
ExecStop = kill -INT 'cat /run/wsgi.pid'
ExecReload = kill -TERM 'cat /run/wsgi.pid'
Restart = always
Type = notify
NotifyAccess = main
PIDFile = /run/uwsgi.pid

[Install]
WantedBy = multi-user.target

Save the uwsgi.service file.

Enable and start the uwsgi service with:

systemctl enable uwsgi
systemctl start uwsgi

Nginx

Next we’ll enable and start the nginx service.

systemctl enable nginx
systemctl start nginx

Visit your servers address in a browser, you should see the Welcome to nginx on Fedora! page.

It is important that this is working BEFORE moving on to the next steps. If you’re encountering errors at this stage in the process, read the output of systemctl status nginx -l for potential clues and troubleshoot as necessary.

Nginx Config

Create a configuration file for nginx to host your Django app

touch /etc/nginx/conf.d/blog.conf

If you’re not using a FQDN, replace your servers IP address for the server_name. In this example django.iambartlett.com is the servername. Open the /etc/nginx/conf.d/blog.conf file and add the following (replacing server_name with your domain or IP address)

server {
    listen 80;
    server_name django.iambartlett.com; # Name of server

        client_max_body_size 64M;

        charset utf-8;

        # Error logs paths, make sure these exist!
        error_log /var/www/django_demo/logs/error.log;
        access_log /var/www/django_demo/logs/access.log;

        # Django media
        location /media {
            alias /var/www/django_demo/blog/media; # path to the media folder of your Djano project

        }

        # Django static
        location /static {
            alias /var/www/django_demo/blog/static; # path to static folder of you Django project
        }

        # Everything else to Django server
        location / {
            uwsgi_pass unix:/opt/uwsgi/blog.sock; # path to unix socket for uwisgi
            include uwsgi_params;
        }
}

Save the blog.conf file.

Let’s Encrypt

Assuming you’re using a FQDN you can use Let's Encrypt to automatically configure the /etc/nginx/conf.d/blog.conf file to use ssl certificates and listen on port 443. The below command will do the trick (replace django.iambartlett.com with your domain)

certbot --nginx -d django.iambartlett.com

Follow the prompts. I recommend choosing to redirect unencrypted traffic to https (option 2).

Please choose whether HTTPS access is required or optional.
-------------------------------------------------------------------------------
1: Easy - Allow both HTTP and HTTPS access to these sites
2: Secure - Make all requests redirect to secure HTTPS access
-------------------------------------------------------------------------------
Select the appropriate number [1-2] then [enter] (press 'c' to cancel):

If you encounter issues with Let’s Encrypt, check out their documentation here.

Applying Permissions

Ensure your Django project has the correct permissions set for directories and files:

find /var/www/django_demo -type d -exec chmod 755 {} +
find /var/www/django_demo -type f -exec chmod 644 {} +

You’ll also want to change the owner and group of your project. The following will allow uwsgi and nginx to access and serve the files.

chown -Rv uwsgi:nginx /var/www/django_demo

SELinux

Temporarily Disable SELinix for Nginx

To make the process of troubleshooting a bit easier, it makes sense to temporarily disable SELinux just long enough to verify that the Nginx and uWSGI services are working together to host your Django project.

Temporarily disable SELinux for nginx with:

semanage permissive -a httpd_t

Restart uwsgi and nginx:

systemctl restart uwsgi
systemctl restart nginx

If either service complains or fails to restart successfully I’d recommend looking at the output of systemctl status servicename -l.

You may also want to look at the uwsgi logs located in /var/log/uwsgi.log or the nginx logs which should be located in /var/www/django_demo/logs/error.log

If both services started successfully and you're able to access your Django project from the browser, continue on.

Re-enable SELinux for Nginx

After verifying that Nginx and uWSGI are working sans-SELinux, re-enable it with

semanage permissive -d httpd_t

Restart the uWSGI and Nginx services

systemctl restart uwsgi
systemctl restart nginx

You’ll likely receive the following error when nginx tries to reload.

Job for nginx.service failed because the control process exited with error code. See "systemctl status nginx.service" and "journalctl -xe" for details.

Extend the httpd_t Domain Permissions

To fix this issue we’ll need to extend the httpd_t domain permission to allow access to additional file locations. By default the httpd_t domain is restricted to /var/www/html.

The audit2allow tool lets you create an semodule based on actions that were denied in the /var/log/audit/audit.log. This semodule can be loaded to extend the httpd_t domain permissions as needed.

grep nginx /var/log/audit/audit.log | audit2allow -m nginx > nginx.te

View the contents of the nginx.te file:

cat nginx.te

The output should be similar to this:

module nginx 1.0;

require {
    type httpd_t;
    type httpd_sys_content_t;
    type usr_t;
    class sock_file write;
    class file append;
}

#============= httpd_t ==============

#!!!! This avc is allowed in the current policy
allow httpd_t httpd_sys_content_t:file append;

#!!!! WARNING: 'usr_t' is a base type.
allow httpd_t usr_t:sock_file write;

Generate a compliled policy with:

grep nginx /var/log/audit/audit.log | audit2allow -M nginx

Load the policy with (this is persistent across reboots):

semodule -i nginx.pp

Reload uWSGI and Nginx

At this point you should be close to a successful Django project deployment. Resart the uWSGI and Nginx services and then try visiting the URL or IP for your app in a browser to see if it works.

systemctl restart uwsgi
systemctl restart nginx

That's it, you've successfully deployed a Django project on Centos 7 with Nginx and uWSGI!

Troubleshooting

If Nginx is throwing a proxy error of some kind, you may need to repeat the steps in the Extend the httpd_t Domain Permissions section above.

The /var/log/audit/audit.log used to create your semodule may not have logged permission denied errors that occur when accessing your /opt/uwsgi/blog.sock file, recreating the semodule and loading it will most likely fix this issue.