We know that Nginx is more faster than Apache and most of us prefer to replace Apache with Nginx as their web server. Nginx is known to serve faster static content and run with less RAM. As of this writing, Virtualmin supports Apache as its web server. To take advantage of Nginx, we will install it as reverse proxy for Apache and continue using Virtualmin to manage your domains. This guide also applies to Nginx+PHP FPM setup just skip the “Configure Apache” section and skip the “Configure Virtualmin” section if you are not using Virtualmin. Nginx configurations for virtual host are tailored for Drupal site and the following are the features:
- Support for Virtualmin to manage domains.
- Microcache support for anonymous and authenticated users.
- Seamless upstream switch between reverse proxy to Apache and PHP FPM backends.
- Script that aids to generate Nginx configuration from template for your web site domain (for those who don’t use Virtualmin).
- Protections from: MIME type deviation, DoS attacks, bad bot/referrer user agents, clickjacking attacks, unauthorized access to private file directory and hotlinking.
- Disabled access to any code files and known Drupal files and directories.
- Better handling of static resources: css, cur, json, js, jpg/jpeg, gif, htc, ico, png, htm, html, xml, txt, otf, ttf, eot, woff, svg, webp, webm, zip, gz, tar, rar, pdf, pptx, mp3, ogg, flv, mp4, mpa.
- Nginx PageSpeed support.
- Boost module support.
- Advanced CSS/JS Aggregation module support.
- RobotsTxt module support.
- XML Sitemap module support.
- Advanced Help module support.
- imagecache module support.
- File Force Download module support.
- Provision to support Filefield Nginx Progress module.
- RSS feed support.
- Supports Drupal 8, Drupal 7 and Durpal 6.
- The configurations observes Nginx’s inheritance rules for add_header directives.
- Protection against illegal HTTP methods (HEAD, GET and POST are only allowed).
- The error_log is set to “error” level and access_log is disabled in this Nginx configurations to avoid spending disk IO time.
- Implements Nginx PageSpeed module’s Shard domains technique for non-SSL sites.
- Uses the Let’s Encrypt free SSL/TLS certificates.
The following procedures are tested on Linode server running Centos 7 64-bit Linux distribution.
Install Nginx
If you need to install Nginx with PageSpeed module please follow the steps here instead then jump to configure Nginx section.
- In able to install the latest Nginx server we will need to register Nginx repository:
1vi /etc/yum.repos.d/nginx.repo
12345name=nginx repobaseurl=http://nginx.org/packages/mainline/centos/$releasever/$basearch/gpgcheck=0priority=1enabled=0 - Install Nginx using yum:
1yum --enablerepo=nginx -y install nginx - Make Nginx auto-start upon reboot:
1chkconfig nginx on
Configure Nginx
- We will not need the native Nginx configurations provided because we will create new configurations. Lets backup the original Nginx configurations first:
1mv /etc/nginx /etc/nginx.bak - Create the folders following the directory structure shown below:
In the next steps, we will populate these folders with Nginx configurations. We will start populating each folder from bottom folder (utils) to top folder (apps).
- Create the main Nginx file
/etc/nginx/nginx.conf
and copy the following scripts to this file:123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260## Pagespeed optimized resources that returns 404, redirect to original resourcelocation ~* (.+)/x(.+),(.+).pagespeed.[.-_[:alnum:]]+$ {## Handle resources with query string and Pagespeed optimized resources with## file name prefixed with x (eg. xMyImage.png,qitok=qwer.pagespeed.asdf.webp)error_page 404 = @orig-resource;set $orig_resource_uri $1/$2?$3;try_files $uri $uri/ $orig_resource_uri;}location ~* (.+),(.+).pagespeed.[.-_[:alnum:]]+$ {## Handle resources with query stringerror_page 404 = @orig-resource;set $orig_resource_uri $1?$2;try_files $uri $uri/ $orig_resource_uri;}location ~* (.+)/x(.+).pagespeed.[.-_[:alnum:]]+$ {## Handle Pagespeed optimized resources with file name prefixed with x## (eg. xMyImage.png.pagespeed.asdf.webp)error_page 404 = @orig-resource;set $orig_resource_uri $1/$2;try_files $uri $uri/ $orig_resource_uri;}location ~* (.+).pagespeed.[.-_[:alnum:]]+$ {## Default handlererror_page 404 = @orig-resource;set $orig_resource_uri $1;try_files $uri $uri/ $orig_resource_uri;}## Regular private file serving.location ^~ /system/files/ {include utils/service/php_pass.conf;## For not signalling a 404 in the error log whenever the## system/files directory is accessed add the line below.## Note that the 404 is the intended behaviour.log_not_found off;}## Trying to access private files directly returns a 404.location ^~ /sites/[.-[:alnum:]]+/files/private/ {internal;}location ^~ /sites/[.-[:alnum:]]+/private/ {internal;}## Support for the file_force module## https://drupal.org/project/file_force.location ^~ /system/files_force/ {include utils/service/php_pass.conf;## For not signalling a 404 in the error log whenever the## system/files directory is accessed add the line below.## Note that the 404 is the intended behaviour.log_not_found off;}## If accessing an image generated by Drupal 6 imagecache, serve it## directly if available, if not relay the request to Drupal to (re)generate## the image.location ~* /imagecache/ {## Image hotlinking protection. If you want hotlinking## protection for your images uncomment the following line.include map/hotlinking_protection_allowed_hosts.conf;access_log off;expires 30d;try_files $uri $uri/ @drupal;}## Drupal 7 generated image handling, i.e., imagecache in core. See:## https://drupal.org/node/371374.location ~* /files/styles/ {## Image hotlinking protection. If you want hotlinking## protection for your images uncomment the following line.include map/hotlinking_protection_allowed_hosts.conf;access_log off;expires 30d;try_files $uri $uri/ @drupal;}## Advanced Aggregation module CSS/JS## support. https://drupal.org/project/advagg.location ~ ^/sites/[.-[:alnum:]]+/files/advagg_(?:css|js)/ {location ~* (?:css|js)[_-[:alnum:]]+.(?:css|js)(.gz)?$ {access_log off;expires max;gzip_static on;add_header ETag '';add_header Accept-Ranges '';## Set a far future Cache-Control header to 52 weeks and add no-transform## to make Pagespeed bypass these CSS/JS optimized by advagg moduleadd_header Cache-Control 'max-age=31449600, no-transform, public';## Inheritance Rules for add_header Directives## Because this 'server' block contains another 'add_header' directive,## we must redeclare the 'add_header' from 'http' contextinclude utils/mod_header.conf;add_header Strict-Transport-Security $hsts;try_files $uri $uri/ @drupal;}}## All static files will be served directly.location ~* ^.+.(?:css|cur|json|js|jpe?g|gif|htc|ico|png|txt|otf|ttf|eot|woff|svg|webp|webm|zip|gz|tar|rar)$ {access_log off;expires 30d;## No need to bleed constant updates. Send the all shebang in one## fell swoop.tcp_nodelay off;## Set the OS file cache.open_file_cache max=3000 inactive=120s;open_file_cache_valid 45s;open_file_cache_min_uses 2;open_file_cache_errors off;try_files $uri $uri/ @cache;}## PDFs and powerpoint files handling.location ~* ^.+.(?:pdf|pptx?)$ {access_log off;expires 30d;## No need to bleed constant updates. Send the all shebang in one## fell swoop.tcp_nodelay off;try_files $uri $uri/ @drupal;}## MP3 and Ogg/Vorbis files are served using AIO when supported. Your OS must support it.location ~ ^/sites/[.-[:alnum:]]+/files/audio/mp3 {location ~* .*.mp3$ {access_log off;directio 4k; ## for XFS## If you're using ext3 or similar uncomment the line below and comment the above.#directio 512; ## for ext3 or similar (block alignments)tcp_nopush off;aio on;output_buffers 1 2M;try_files $uri $uri/ @drupal;}}location ~ ^/sites/[.-[:alnum:]]+/files/audio/ogg {location ~* .*.ogg$ {access_log off;directio 4k; # for XFS## If you're using ext3 or similar uncomment the line below and comment the above.#directio 512; ## for ext3 or similar (block alignments)tcp_nopush off;aio on;output_buffers 1 2M;try_files $uri $uri/ @drupal;}}## Pseudo streaming of FLV files:## http://wiki.nginx.org/HttpFlvStreamModule.## If pseudo streaming isn't working, try to comment## out in nginx.conf line with:## add_header X-Frame-Options SAMEORIGIN;location ~ ^/sites/[.-[:alnum:]]+/files/video/flv {location ~* .*.flv$ {access_log off;flv;try_files $uri $uri/ @drupal;}}## Pseudo streaming of H264/AAC files. This requires an Nginx## version greater or equal to 1.0.7 for the stable branch and## greater or equal to 1.1.3 for the development branch.## Cf. http://nginx.org/en/docs/http/ngx_http_mp4_module.html.location ~ ^/sites/[.-[:alnum:]]+/files/video/mp4 { # videoslocation ~* .*.(?:mp4|mov)$ {access_log off;mp4;mp4_buffer_size 1M;mp4_max_buffer_size 5M;try_files $uri $uri/ @drupal;}}location ~ ^/sites/[.-[:alnum:]]+/files/audio/m4a { # audioslocation ~* .*.m4a$ {access_log off;mp4;mp4_buffer_size 1M;mp4_max_buffer_size 5M;try_files $uri $uri/ @drupal;}}## Advanced Help module makes each module provided README available.location ^~ /help/ {location ~* ^/help/[^/]*/README.txt$ {access_log off;include utils/service/php_pass.conf;}}## Replicate the Apache directive of Drupal standard## .htaccess. Disable access to any code files. Return a 404 to curtail## information disclosure. Hide also the text files.location ~* ^(?:.+.(?:htaccess|gitignore|txt|engine|inc|info|install|make|module|profile|test|po|sh|.*sql|test|theme|twig|tpl(?:.php)?|xtmpl|yml)(~|.sw[op]|.bak|.orig|.save)?$|^(/.(?!well-known).*|/code-style.pl|/Entries.*|/Repository|/Root|/Tag|/Template|/composer.(json|lock))$|^#.*#$|/.*.php(.sw[op]|.bak|.orig|.save)+)$ {return 404;}## Disallow access to .bzr, .git, .hg, .svn, .cvs directories:## return 404 as not to disclose information.location ^~ /.bzr {return 404;}location ^~ /.git {return 404;}location ^~ /.hg {return 404;}location ^~ /.svn {return 404;}location ^~ /.cvs {return 404;}## Disallow access to patches directory.location ^~ /patches {return 404;}## Disallow access to drush backup directory.location ^~ /backup {return 404;}## Disable access logs for robots.txt.location = /robots.txt {access_log off;## Add support for the robotstxt module## https://drupal.org/project/robotstxt.try_files $uri $uri/ @drupal;}## RSS feed support.location = /rss.xml {try_files $uri $uri/ @drupal;}## XML Sitemap support.location = /sitemap.xml {try_files $uri $uri/ @drupal;}## Support for favicon. Return an 1x1 transparent GIF if it## doesn't exist.location = /favicon.ico {expires 30d;try_files /favicon.png @empty;}Create the file
/etc/nginx/drupal/php_handler.conf
and add the following to it:12345## PHP handlerinclude utils/service/php_pass.conf;include utils/service/microcache_auth.conf;#include utils/service/microcache.conf;We have two options here:
microcache_auth.conf
which uses the Nginx cache for anonymous users only andmicrocache.conf
which uses the Nginx cache for both anonymous and authenticated users. Themicrocache_auth.conf
is enabled by default. Select between the two according to your requirements.Create the file
/etc/nginx/drupal/named_location.conf
and add the following to it:123456789101112131415161718192021222324252627282930## Restrict access to the strictly necessary PHP files. Reducing the## scope for exploits. Handling of PHP code and the Drupal event loop.location @drupal {include apps/drupal/php_handler.conf;## Filefield Upload progress## https://drupal.org/project/filefield_nginx_progress support## through the NginxUploadProgress modules.#track_uploads uploads 60s;}## We define a named location for the Boost cache.location @cache {include apps/drupal/boost.conf;## We try each boost URI in succession, if every one of them## fails then hand it to Drupal.try_files /cache/normal/$host${uri}_${args}.html /cache/normal/$host${uri}_${args}.xml /cache/normal/$host${uri}_${args}.json /cache/normal/$host${uri}_${args}.html.gz /cache/normal/$host${uri}_${args}.xml.gz /cache/normal/$host${uri}_${args}.json.gz /cache/perm/$host${uri}_.css /cache/perm/$host${uri}_.js /cache/$host/0$uri.html /cache/$host/0${uri}/index.html @drupal;}## Return an in memory 1x1 transparent GIF.location @empty {expires 30d;empty_gif;}## Redirect Pagespeed optimized resources that returns 404 to## original resource.location @orig-resource {return 302 $scheme://$server_name$orig_resource_uri;}Create the file
/etc/nginx/drupal/drupal_upload_progress.conf
and add the following to it:123456789101112131415161718192021## Drupal 7 configuration for the Nginx Upload Progress module:## https://github.com/masterzen/nginx-upload-progress-module## This requires the Filefield Nginx Progress module:## https://drupal.org/project/filefield_nginx_progress.## The Nginx module wants ?X-Progress-ID query parameter so## that it report the progress of the upload through a GET## request. But the drupal form element makes use of clean## URLs in the POST.location ~ (?.*)/x-progress-id:(?d*) {rewrite ^ $upload_form_uri?X-Progress-ID=$upload_id;}## Now the above rewrite must be matched by a location that## activates it and references the above defined upload## tracking zone.location ^~ /progress {upload_progress_json_output;report_uploads uploads;}Create the file
/etc/nginx/drupal/drupal_install.conf
and add the following to it:12345678910111213## Directives for installing drupal. This is for drupal 6 and 7.location = /install.php {auth_basic "Restricted Access"; # auth realmauth_basic_user_file key/.htpasswd-users; # htpasswd fileinclude apps/drupal/php_handler.conf;}## This is for drupal 8.location = /core/install.php {auth_basic "Restricted Access"; # auth realmauth_basic_user_file key/.htpasswd-users; # htpasswd fileinclude apps/drupal/php_handler.conf;}Create the file
/etc/nginx/drupal/drupal_cron_update.conf
and add the following to it:12345678910111213141516171819202122232425262728## Configuration file for Drupal if you're not using drush to update your site## or run cron.## XMLRPC. Comment out if not enabled.location = /xmlrpc.php {include apps/drupal/php_handler.conf;}## Restrict cron access to a specific host.location = /cron.php {## If not allowed to run cron then issue a 404 and redirect to the## site root.if ($not_allowed_cron) {return 404 /;}include apps/drupal/php_handler.conf;}## Run the update from the web interface with Drupal 7.location = /authorize.php {include apps/drupal/php_handler.conf;}location = /update.php {auth_basic "Restricted Access"; # auth realmauth_basic_user_file key/.htpasswd-users; # htpasswd fileinclude apps/drupal/php_handler.conf;}Create the file
/etc/nginx/drupal/core.conf
and add the following to it:1234567891011121314151617181920212223242526272829303132333435363738394041424344## Nginx configuration for Drupal## Common Nginx configuration for server contextinclude apps/drupal/common_server_context.conf;## Named locationinclude apps/drupal/named_location.conf;## The 'default' location.location / {## Drupal generated static filesinclude apps/drupal/static_files_handler.conf;try_files $uri $uri/ @cache;}## Drupal index.location = /index.php {include apps/drupal/boost.conf;## We try each boost URI in succession, if every one of them## fails then hand it to Drupal.try_files /cache/normal/$host/_${args}.html /cache/normal/$host/_${args}.html.gz /cache/$host/0.html /cache/$host/0/index.html @drupal;}## Boost stats.location = /boost_stats.php {include apps/drupal/php_handler.conf;}## PHP scipt to log all catch pathslocation = /sites/all/logcatchall.php {include apps/drupal/php_handler.conf;}## Uncomment the line below if you want to enable basic auth for## access to all /admin URIs. Note that this provides much better## protection if use HTTPS. Since it can easily be eavesdropped if you## use HTTP.#include apps/drupal/admin_basic_auth.conf;## Any other attempt to access PHP files returns a 404.location ~* ^.+.php$ {return 404;}Create the file
/etc/nginx/drupal/common_server_context.conf
and add the following to it:12345678910111213141516171819202122232425262728293031323334353637## Common to all hosts Nginx configurations for server context## See the blacklist.conf file at the parent dir: /etc/nginx.## Deny access based on the User-Agent header.if ($bad_bot) {return 444;}## Deny access based on the Referer header.if ($bad_referer) {return 444;}## Protection against illegal HTTP methods. Out of the box only HEAD,## GET and POST are allowed.if ($not_allowed_method) {return 405;}## Limits## limit_req_zone $binary_remote_addr zone=req_limit_per_ip:10m rate=60r/s + limit_req zone=req_limit_per_ip burst=5 nodelay## - Set shared memory as 10MB## - Limit requests per IP as following## - Set maximum requests as rate * burst in burst seconds## For example, maximum value is 300(=60*5) requests in 5 seconds in this case## - With nodelay option : Nginx would return 503 response and not handle excessive requests## - Without nodelay option : Nginx would wait (no 503 response) and handle excessive requests with some delay## limit_conn_zone $binary_remote_addr zone=conn_limit_per_ip:10m + limit_conn conn_limit_per_ip 30## - Set shared memory as 10MB## - Limit connections per IP as 30 in this case## - Note that normal browser makes 2~8 connections and SPDY protocol split each connections## - Nginx would return 503 response if connection exceeds this valuelimit_conn connlimit 32;limit_req zone=reqlimit burst=5 nodelay;## PageSpeed filters## Uncomment the line below if Google PageSpeed module is present#include apps/pagespeed/optimize.conf;Create the file
/etc/nginx/drupal/boost.conf
and add the following to it:1234567891011121314151617181920212223242526272829303132## Boost configurationgzip_static on;## Error page handler for the case where $no_boost_cache is 1, POST## request or authenticated.error_page 418 = @drupal;## If $no_boost_cache is 1 then it means that Boost session cookie## is present or uri has wrong dir. So serve the dynamic page.if ($no_boost_cache) {return 418;}## No caching for POST requests.if ($request_method = POST) {return 418;}## Now for some header tweaking. We use a date that differs## from stock Drupal.add_header Expires "Tue, 25 Dec 1977 03:45:00 GMT";## We bypass all delays in the post-check and pre-check## parameters of Cache-Control. Both set to 0.add_header Cache-Control "must-revalidate, post-check=0, pre-check=0";add_header X-DCache "Boost";## Inheritance Rules for add_header Directives## Because this 'server' block contains another 'add_header' directive,## we must redeclare the 'add_header' from 'http' contextinclude utils/mod_header.conf;add_header Strict-Transport-Security $hsts;## Boost doesn't set a charset.charset utf-8;Populate the
/etc/nginx/apps/pagespeed
folder under this guide.To enable Google PageSpeed in your Nginx configuration (make sure first that your Nginx is compiled with PageSpeed module), uncomment the line seen below at
/etc/nginx/nginx.conf
:
1include apps/pagespeed/core.conf;/etc/nginx/apps/drupal/common_server_context.conf
:
1include apps/pagespeed/optimize.conf;/etc/nginx/site-available/template.conf
:
1pagespeed ShardDomain $scheme://$server_name $scheme://s1.{DOM},$scheme://s2.{DOM}; - To enable this Nginx reverse proxy for Apache setup, execute the following:
1ln -s /etc/nginx/utils/apache /etc/nginx/utils/service
1ln -s /etc/nginx/utils/fastcgi /etc/nginx/utils/service - Set permission:
12chmod -R -x /etc/nginxchmod +x /etc/nginx/lib/ /etc/nginx/apps/ /etc/nginx/apps/drupal/ /etc/nginx/apps/pagespeed/ /etc/nginx/key/ /etc/nginx/sites-available/ /etc/nginx/sites-available/admin/ /etc/nginx/sites-available/dev/ /etc/nginx/sites-available/targeted_server_config/ /etc/nginx/sites-enabled/ /etc/nginx/utils/ /etc/nginx/utils/apache/ /etc/nginx/utils/fastcgi/ /etc/nginx/map/ /etc/nginx/ - Restart Nginx:
1systemctl restart nginx
Setup Nginx requirements
- In this tutorial the Apache will use port 8080 and lets open this port to become accessible:
1234iptables -I INPUT -p tcp -m tcp --dport 8080 -j ACCEPTiptables --line -vnLservice iptables saveservice iptables restart - Create the Nginx cache path folder:
123mkdir /var/cache/nginx/microcachechown apache:root /var/cache/nginx/microcachechmod 700 /var/cache/nginx/microcache - Create Nginx logrotate script:
1vi /etc/logrotate.d/websites_nginx_logs.conf