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.