Howto configure Nginx, PHP, Python, uWSGI, Let’s encrypt SSL, HTTP/2 on CentOS 7

Hi out there,
for many years until now, I have been using in Django projects a proven combination Apache and Mod_wsgi and I was fully satisfied. I had no reason to change anything  but the circumstances of the new project forced me to use Nginx . I heard some good things about Nginx, how it is easy to configure, how it is faster than Apache or how I can run Nginx with uWSGI to preform Python scripts.  As a bonus, we are going to add a support for PHP with Php-fpm helper.

Everyone has the right! Nginx is a great and a better choice for a powerful well-configured web server than a good old Apache.  Let’s go to look at things closer.

In this tutorial, I am going to outline how to install and setup a latest version Nginx on CentOS 7 including support Php-fpm 7.x, uWSGI and Let’s encrypt – free SSL/TLS certificates suitable for HTTPS support.

 

Prerequisites

You need to have installed a functional CentOS 7 box and an access to the root account. The working Internet connection is a matter of course.

 

Step 1 – prepare CentOS, install necessary repos including support for PHP 7.4

First at all, we need to add repos and install current Nginx, uWSGI and PHP packages.

# add needed a repo Epel because of other dependencies
yum -y install epel-release

# remove Nginx shipped with CentOS7 if was installed
yum -y remove nginx
# add oficial Nginx repo
rpm -Uvh http://nginx.org/packages/centos/7/noarch/RPMS/nginx-release-centos-7-0.el7.ngx.noarch.rpm

# install latest Nginx, Let's encrypt, uWSGI and other necessary packages
yum -y install nginx uwsgi uwsgi-devel uwsgi-plugin-common uuid-devel libcap-devel letsencrypt

# install a repo Remi because of PHP packages
rpm -Uvh http://rpms.remirepo.net/enterprise/remi-release-7.rpm

# install PHP core packages 
yum -y --enablerepo=remi,remi-php74 install php-fpm php-common

# install popular PHP extension package that are suitable for running e.g. an instance of WordPress website
yum -y --enablerepo=remi,remi-php74 install php-opcache php-pecl-apcu php-cli php-pear php-pdo php-mysqlnd php-pgsql php-pecl-mongodb \
php-pecl-memcache php-pecl-memcached php-gd php-mbstring php-mcrypt php-xml php-zip php-php-pecl-geoip

We are going to prepare folders for additional Nginx config files and Let’s encrypt web directory shared by all virtual hosts that is used like a webroot for SSL certificate  confirmation.

# run these bash commands for creating necessary directories and proper permission settings

mkdir /etc/nginx/domains /etc/nginx/snippets;
chown -R nginx.nginx /etc/nginx /etc/uwsgi.d /etc/uwsgi.ini;
mkdir -p /var/www/letsencrypt/.well-known/acme-challenge;
chown -R nginx.nginx /var/www/letsencrypt;
chmod -R 770 /var/www/letsencrypt/;
mkdir /var/uwsgi;
chown nginx.nginx /var/uwsgi;
chmod 770 /var/uwsgi;
chown -R root.nginx /var/lib/php;
chown -R nginx:nginx /var/lib/php/session;

Step 2 – create configs for PHP packages,  HTTPS support and HTTP compression.

We are going to modify the main PHP.ini config using a bunch of  SED commands in order to set optimal values for important variables.

# run bash commands to set optimal values in /etc/php.ini

# cgi.fix_pathinfo = 0
# mail.add_x_header = On
# max_execution_time = 90
# max_input_time = 90
# memory_limit = 256M
# post_max_size = 128M
# upload_max_filesize = 128M
# display_errors = On
# log_errors = Off
# error_reporting = E_ALL & ~E_NOTICE
# date.timezone = "Europe/Prague"
# short_open_tag = On

sed -i 's/;cgi.fix_pathinfo=1/cgi.fix_pathinfo = 0/g' /etc/php.ini;
sed -i 's/mail.add_x_header = Off/mail.add_x_header = On/g' /etc/php.ini;
sed -i 's/max_execution_time = 30/max_execution_time = 90/g' /etc/php.ini;
sed -i 's/max_input_time = 60/max_input_time = 90/g' /etc/php.ini;
sed -i 's/memory_limit = 128M/memory_limit = 256M/g' /etc/php.ini;
sed -i 's/post_max_size = 8M/post_max_size = 128M/g' /etc/php.ini;
sed -i 's/upload_max_filesize = 2M/upload_max_filesize = 128M/g' /etc/php.ini;
sed -i 's/display_errors = Off/display_errors = On/g' /etc/php.ini;
sed -i 's/log_errors = On/log_errors = Off/g' /etc/php.ini;
sed -i 's/error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT/error_reporting = E_ALL & ~E_NOTICE/g' /etc/php.ini;
sed -i 's,;date.timezone =,date.timezone = "Europe/Prague",g' /etc/php.ini;
sed -i 's/short_open_tag = Off/short_open_tag = On/g' /etc/php.ini;

We are going to create a config snippet for php.conf for handling with *.php extension.

# the whole content of /etc/nginx/snippets/php.conf

location ~ \.php$
{
include fastcgi_params;
try_files $uri =404;
fastcgi_pass unix:///var/run/php-fpm/php-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_read_timeout 180;
fastcgi_buffer_size 128k;
fastcgi_buffers 256 16k;
fastcgi_busy_buffers_size 256k;
fastcgi_temp_file_write_size 256k;
}

We are going to create a config snippet ssl.conf for HTTPS support. Later you can check a quality of  SSL certificate on the page  https://www.ssllabs.com/ssltest/ .

# the whole content of /etc/nginx/snippets/ssl.conf 

ssl_session_timeout 10m;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;

# ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers TLS-CHACHA20-POLY1305-SHA256:TLS-AES-256-GCM-SHA384:TLS-AES-128-GCM-SHA256:HIGH:!aNULL:!MD5;
ssl_ecdh_curve secp384r1;
ssl_prefer_server_ciphers on;
resolver 8.8.4.4 1.1.1.1 9.9.9.9 valid=864000;
resolver_timeout 10s;

ssl_stapling on;
ssl_stapling_verify on;

add_header Strict-Transport-Security 'max-age=31536000; includeSubDomains; preload' always;
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;

We are going to create a config snippet letsencrypt.conf for verifying Let’s encrypt SSL certificate.

# the whole content of /etc/nginx/snippets/letsencrypt.conf

location /.well-known/acme-challenge/
{
default_type "text/plain";
root /var/www/letsencrypt;
allow all;
}

We are going to create a config snippet compress.conf for the support of HTTP compression.

# the whole content of /etc/nginx/compress.conf

gzip on;
gzip_vary on;
gzip_comp_level 3; # value 3 is good compromise between saved bandwidth and CPU load 
gzip_min_length 1024; # do not compress anything smaller than 1Kb
gzip_buffers 4 32k;
gzip_proxied no-cache no-store private expired auth;
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml application/xml+rss;

 

Step 3 – compile uWSGI Python 3.8 plugin

We want to serve Python files using uWSGI application server and therefore we need to compile the appropriate uwsgi-python plugin. We are going to prepare plugin for python 3.8.  Python 3.8 is needed to compile into the folder /opt/python38 because Python 2.7 is shipped with CentOS 7.

Howto compile and install the latest Python 3.8 check out my another post.

For a compilation of python38_plugin.so I used an ugly hack with symlinks because there is no option for a setting of path to Python 3.8. (e.g. /opt/python38/bin/python3.8). Does anyone know?

# Python 3.8 has to be presented in the directory /opt/python38 and we are going to create uWSGI plugin
cd /tmp

# first at all we are going to make the ugly hack. We set temporary python 3.8 as a general Python interpret of CentOS 7.
rm -f /usr/bin/python; ln -s  /opt/python38/bin/python3 /usr/bin/python

PYTHON=python3.8; /usr/sbin/uwsgi  --build-plugin "/usr/src/uwsgi/2.0.17.1/plugins/python/ python38" 
chmod 644 ./python38_plugin.so ; mv -f ./python38_plugin.so  /usr/lib64/uwsgi/

# we are going to get back shipped Python 2.7
rm -f /usr/bin/python; ln -s  /usr/bin/python2  /usr/bin/python

# test a Python version within an ugwsi plugin file
strings  /usr/lib64/uwsgi/python38_plugin.so|grep '3.8'

 

Step 4 – create the main config file of Nginx and an example virtual host

We are going to create the main config file of Nginx with settings for an optimal performance.

# the whole content of /etc/nginx/nginx.conf

user  nginx nginx;
worker_processes  auto;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
    multi_accept on;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    index index.php index.html index.htm;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request"'
                      '$status $body_bytes_sent "$http_referer"'
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log main;

    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 70;
    types_hash_max_size 2048;
    client_max_body_size 128M;
    autoindex off;

    include /etc/nginx/conf.d/*.conf;    
    include /etc/nginx/domains/*.conf;

    open_file_cache max=1000 inactive=60s;
    open_file_cache_valid 120s;
    open_file_cache_min_uses 3;
    open_file_cache_errors off;
}

Here is an example of my virtual host for domain www.mydjango.eu with enabling HTTP/ 2, SSL and SEO friendly redirection from HTTP to HTTPS.
My website is powered by Django and therefore it has to be configurated uWSGI gateway for location “/”.
All my virtual hosts are placed in folder “/home/webhosting”, etc for mydjango.eu is root “/home/webhosting/mydjango.eu”.

You can disable  support of PHP by commenting/deleting line with #include /etc/nginx/php.conf

# the whole content of /etc/nginx/domains/mydjango.eu.conf    
  
server {
    listen 80;
    listen [::]:80;
    server_name mydjango.eu www.mydjango.eu;

    include /etc/nginx/snippets/letsencrypt.conf;
    
    location / {
        return 301 https://www.mydjango.eu$request_uri;
    }    
}

server {
    listen              443 ssl http2;
    listen              [::]:443 ssl http2;
    server_name         www.mydjango.eu;
    
    root /home/webhosting/mydjango.eu/;
    
    index index.php index.html index.htm;

    include /etc/nginx/php.conf

    ssl_certificate /etc/letsencrypt/live/mydjango.eu/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mydjango.eu/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/mydjango.eu/fullchain.pem;
    include /etc/nginx/snippets/ssl.conf;
    
    include /etc/nginx/compress.conf;
    
    client_max_body_size 128M;
    keepalive_timeout   70;
    charset     utf-8;
    
    access_log   /home/webhosting/mydjango.eu/logs/access.log;
    error_log    /home/webhosting/mydjango.eu/logs/error.log;

    location / {
        include    uwsgi_params;
        
        uwsgi_read_timeout          5m;
        uwsgi_send_timeout          5m;
        send_timeout                5m;
        
        uwsgi_pass unix:///var/uwsgi/uwsgi_mydjango.eu.sock;
        # uwsgi_pass server 127.0.0.1:8001;
    }
    
    location = /favicon.ico { 
        # alias  /home/webhosting/mydjango.eu/static/favicon.ico;
        access_log off;
        log_not_found off; 
    }
    
    location  /robots.txt {
        # alias  /home/webhosting/mydjango.eu/static/robots.txt;
        access_log off;
        log_not_found off;
        allow all;  
    }
    
    location /static {
        alias    /home/webhosting/mydjango.eu/static/;
    }

    location /media {
        alias    /home/webhosting/mydjango.eu/media/;
    }
    
    location ~ ^/(static|media)/ {
        expires max;
        add_header Pragma public;
        add_header Cache-Control "public, must-revalidate, proxy-revalidate";
    }
   
    location ~* .(gif|jpg|jpeg|png|ico|wmv|3gp|avi|mpg|mpeg|mp4|flv|mp3|mid|js|css|wml|swf)$ {
        expires max;
        add_header Pragma public;
        add_header Cache-Control "public, must-revalidate, proxy-revalidate";
        log_not_found off; 
    }

    location ~ /\.(?!well-known\/) {
        deny all;
    }
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name mydjango.eu;

    ssl_certificate /etc/letsencrypt/live/mydjango.eu/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mydjango.eu/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/mydjango.eu/fullchain.pem;
    include /etc/nginx/snippets/ssl.conf;

    location / {
        return 301 https://www.mydjango.eu$request_uri;
    }
}

 

Step 5 – setup uWSGI gateway

We are going to modify the main config file uwsgi.ini of uWSGI application server and enable emperor mode.

# the whole content of /etc/uwsgi.ini
 
[uwsgi]
uid = uwsgi
gid = uwsgi
emperor = /etc/uwsgi.d
pidfile = /tmp/uwsgi_emperor.pid
stats = /tmp/uwsgi_emperor_stats.sock
emperor-tyrant = true
cap = setgid,setuid
master = true
chmod-socket = 666
logto = /var/log/uwsgi_emperor.log

Here is an example uWSGI configuration for a virtual host mydjango.eu

# the whole content of /etc/uwsgi.d/mydjango.eu.ini

[uwsgi]
project = mydjango
domain = mydjango.eu
base_dir = /home/webhosting/mydjango.eu
wsgi_file = wsgi.py

plugins = python38 # we use uwsgi plugin python38_plugin.so
uid = nginx
gid = nginx
disable-logging = false
umask = 002

processes = 4 # set 2 times number of CPU cores (in my case I have a dual-core processor)
threads = 2 # run each worker in threaded mode with the specified number of threads, also enable GIT threads in the Python module
limit-as = 512 # set the memory limit for a project  to 512 MB
limit-nproc = 128 # limit number of processes

touch-reload = %(base_dir)/%(project)/%(wsgi_file) # for developers - reload when the file changed (you can use bash command touch) 
#--------------------------------------------------------------------------------------------------------------------------------------
chdir = %(base_dir)
virtualenv = %(base_dir)/env
wsgi-file = %(base_dir)/%(project)/%(wsgi_file)

socket = /var/uwsgi/uwsgi_%(domain).sock
# socket = 127.0.0.1:8001

pidfile = /tmp/uwsgi_%(domain).pid
daemonize = %(base_dir)/logs/uwsgi.log
procname-prefix = %(domain)_

catch-exceptions = false # report exception as Http output
disable-logging = true # disable request logging
log-x-forwarded-for = true # use the IP from X-Forwarded-For header instead of REMOTE_ADDR
log-date = true
log-slow = 5000 # log requests slower than the specified number of milliseconds
log-big = 10 # log requestes bigger than the specified size, e.g. 10MB
log-5xx = true
log-4xx = true
memory-report = false

chmod-socket = 666
vacuum = true
master = true
thunder-lock = true
no-orphans = true
harakiri = 120 # respawn processes taking more than 120 seconds
harakiri-verbose = false
post-buffering = 8192  # up to 65535
max-requests = 5000 # respawn processes after serving 5000 requests
env = LANG=en_US.UTF-8  # set because of uploading files with file names that contain non-ASCII characters

Step 6 – do some cleanup

We are going to do some cleanup and set up autostart for Nginx, uWSGI and Php-fpm.

mkdir /var/uwsgi;
chown nginx.nginx /var/uwsgi;
chmod 770 /var/uwsgi;
chown -R root.nginx /var/lib/php;
chown -R nginx:nginx /var/lib/php/session;

chown -R nginx.nginx /etc/nginx /etc/uwsgi.d /etc/uwsgi.ini;

chown -R nginx:nginx /home/webhosting;

find /home/webhosting/ -type d  -print0 | xargs -0 chmod 0770;  # for directories
find /home/webhosting/ -type f  -print0 | xargs -0 chmod 0660;  # for files

systemctl enable nginx.service; systemctl restart nginx.service; # autostart enable
systemctl enable uwsgi.service; systemctl restart uwsgi.service; # autostart enable
systemctl enable php-fpm.service; systemctl restart php-fpm.service; # autostart enable

 

Step 7 – create SSL certificate for our virtual host

There are examples (either for webroot or standalone plugin) of creating SSL certificate with Let’s encrypt for a domain mydjango.eu. Choose one of them.

# a webroot plugin mode
/usr/bin/certbot certonly --webroot --non-interactive --rsa-key-size 4096 --agree-tos --no-eff-email --email support@yourdomain.eu -w /var/www/letsencrypt -d mydjango.eu,www.mydjango.eu

# a standalone plugin mode over http port 80, the webserver cannot run on port 80 at the same time 
/usr/bin/certbot certonly --standalone --non-interactive --rsa-key-size 4096 --agree-tos --no-eff-email --email support@yourdomain.eu -d mydjango.eu,www.mydjango.eu

There are two ways of cron records for a renew of SSL certificates, for either webroot or standalone plugin mode.
Choose the right one according to your case.

# for webroot plugin mode
1 22 * * * certbot renew --quiet && nginx -t && systemctl reload nginx

# for standalone plugin mode
# 1 22 * * * systemctl stop nginx; certbot renew --quiet; systemctl start nginx

 

Conclusion

And that is all.

I hope this guide will help you and if you have some tips for improvements or found a mistake, let me know.

Enjoy!
Hanz