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