Using NGINX as a Web Server for Drupal

0
Nginx (engine-x) is a web server that is regarded to be faster than Apache and with a better performance on heavy load. The difference is summed up succinctly in a quote by Chris Lea on the Why Use Nginx? page: “Apache is like Microsoft Word, it has a million options but you only need six. Nginx does those six things, and it does five of them 50 times faster than Apache.”
Technically speaking, Apache is a process-and-thread-driven application, while Nginx is event-driven. In practice this means that Nginx needs much less memory than Apache to do the work, and also can work faster. There are claims that Nginx, working in a server of 512MB RAM, can handle 10,000 (yes, ten thousands) concurrent requests without problem, while Apache with such a load would just commit harakiri (suicide). Besides, the configuration of Nginx, once you get used to it, is simpler and more intuitive than that of Apache.
It seemed like something that I should definitely give a try, since my web server already had performance problems and I cannot afford to pay for increasing its capacity. Here I describe the steps for installing and configuring Nginx to suit the needs of my web application (which is based on Drupal7, running on a 512MB RAM server at Rackspace).

Table of Contents

  1. Installing nginx and php5-fpm
  2. Configuring php5-fpm
  3. Configuring nginx
  4. Configuration for phpMyAdmin
  5. SSL (HTTPS) support
  6. Avoid any DOS attacks
  7. Full configuration of the site

1. Installing nginx and php5-fpm

In ubuntu server this is very easy:
sudo apt-get install nginx nginx-doc php5-fpm
update-rc.d apache2 disable
update-rc.d nginx enable
service apache2 stop
service nginx start

2. Configuring php5-fpm

The main config file (/etc/php5/fpm/php-fpm.conf) did not need to be changed at all.
On the pool configuration file (/etc/php5/fpm/pool.d/www.conf) I made only some small modifications:
  • Listen to a unix socket, instead if a TCP socket:
    ;listen = 127.0.0.1:9000
    listen = /var/run/php-fpm.sock
  • Other modified options:
    pm.max_requests = 5000
    php_flag[display_errors] = on
    php_admin_value[memory_limit] = 128M
    php_admin_value[max_execution_time] = 90
I also made these modifications on /etc/php5/fpm/php.ini:
cgi.fix_pathinfo=0
max_execution_time = 90
display_errors = On
post_max_size = 16M
upload_max_filesize = 16M
default_socket_timeout = 90
Finally restarted the service php5-fpm:
service php5-fpm restart

3. Configuring nginx

On ubuntu, the configuration of Nginx is located at /etc/nginx/.
  • Create a configuration file for the website, based on the drupal example configuration file:
    cd /etc/nginx/sites-available/
    cp /usr/share/doc/nginx-doc/examples/drupal.gz .
    gunzip drupal.gz
    mv drupal btranslator_dev
    cd /etc/nginx/sites-enabled/
    ln -s ../sites-available/btranslator_dev .
  • At /etc/nginx/sites-enabled/btranslator_dev modify servername and root, and also add accesslog and errorlog:
    server_name dev.btranslator.org l10n-dev.org.al;
    root /var/www/dev.btranslator.org;
    
    access_log /var/log/nginx/btranslator_dev.access.log;
    error_log  /var/log/nginx/btranslator_dev.error.log info;
  • At /etc/nginx/sites-enabled/btranslator_dev, modify the name of the unix socket at the fastcgipass line:
    location ~ .php$ {
            fastcgi_split_path_info ^(.+.php)(/.+)$;
            include fastcgi_params;
            # Intercepting errors will cause PHP errors to appear in Nginx logs
            fastcgi_intercept_errors on;
            fastcgi_pass unix:/var/run/php-fpm.sock;
    }
  • At /etc/nginx/sites-enabled/btranslator_dev, add the index line as well, at the root location:
    location / {
           index index.php;
           try_files $uri $uri/ @rewrite;
    }
  • At /etc/nginx/sites-enabled/btranslator_dev, allow only localhost to access txt and log files:
    location ~* .(txt|log)$ {
           allow 127.0.0.1;
           deny all;
    }
  • At /etc/nginx/nginx.conf, decrease worker processes to 1 or 2:
    # worker_processes 4;
    worker_processes 2;
These modifications are all we need, and then we can reload or restart the nginx service:
service nginx restart

4. Configuration for phpMyAdmin

Add these lines inside the server section, at /etc/nginx/sites-enabled/btranslator_dev:
# Configuration for phpMyAdmin
location /phpmyadmin {
       root /usr/share/;
       index index.php index.html index.htm;
       location ~ ^/phpmyadmin/(.+.php)$ {
               try_files $uri =404;
               root /usr/share/;
               fastcgi_pass unix:/var/run/php-fpm.sock;
               fastcgi_index index.php;
               fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
               include /etc/nginx/fastcgi_params;
       }
       location ~* ^/phpmyadmin/(.+.(jpg|jpeg|gif|css|png|js|ico|html|xml|txt))$ {
               root /usr/share/;
       }
}
location /phpMyAdmin {
       rewrite ^/* /phpmyadmin last;
}
Then reload the nginx service.

5. SSL (HTTPS) support

Add these lines at /etc/nginx/sites-enabled/btranslator_dev:
server {
    listen                  80;
    listen                  443 ssl;
    ssl_certificate         /etc/ssl/certs/ssl-cert-snakeoil.pem;
    ssl_certificate_key     /etc/ssl/private/ssl-cert-snakeoil.key;
    . . . . .
}
Since SSL connections have some overhead, to make them more efficient, add these lines as well at /etc/nginx/nginx.conf (in order to increase session timeout and and use less expensive encryption):
http {
      . . . . .
      #keepalive_timeout 65;
      keepalive_requests    50;
      keepalive_timeout     300;

      ## Global SSL options
      ssl_ciphers HIGH:!aNULL:!MD5:!kEDH;
      ssl_prefer_server_ciphers on;
      ssl_protocols TLSv1;
      ssl_session_cache shared:SSL:10m;
      ssl_session_timeout 10m;
      . . . . .
 }
Then reload nginx.

6. Avoid any DOS attacks

In order to avoid any DOS attacks, add these lines at /etc/nginx/nginx.conf
http {
      . . . . .
      ## limit request frequency to 2 requests per second
      limit_req_zone  $binary_remote_addr  zone=one:10m   rate=2r/s;
      limit_req   zone=one  burst=5;
      . . . . .
}

7. Full configuration of the site

A full version of the file /etc/nginx/sites-enabled/btranslator_dev looks like this:
server {
        listen                  80;
        listen                  443 ssl;
        ssl_certificate         /etc/ssl/certs/ssl-cert-snakeoil.pem;
        ssl_certificate_key     /etc/ssl/private/ssl-cert-snakeoil.key;


        server_name dev.btranslator.org l10n-dev.org.al;
        root /var/www-ssl/dev.btranslator.org;

        access_log /var/log/nginx/btranslator_dev.access.log;
        error_log  /var/log/nginx/btranslator_dev.error.log info;

        location = /favicon.ico {
                log_not_found off;
                access_log off;
        }

        location = /robots.txt {
                allow all;
                log_not_found off;
                access_log off;
        }

        # This matters if you use drush
        location = /backup {
                deny all;
        }

        # Very rarely should these ever be accessed outside of your lan
        location ~* .(txt|log)$ {
                allow 127.0.0.1;
                deny all;
        }

        # This location block protects against a known attack.
        location ~ ..*/.*.php$ {
                return 403;
        }

        # This is our primary location block.
        location / {
                index index.php;
                try_files $uri $uri/ @rewrite;
                expires max;
        }

        # This will rewrite our request from domain.com/node/1/ to domain.com/index.php?q=node/1
        # This could be done in try_files without a rewrite however, the GlobalRedirect
        # module enforces no slash (/) at the end of URL's. This rewrite removes that
        # so no infinite redirect loop is reached.
        location @rewrite {
                rewrite ^/(.*)$ /index.php?q=$1;
        }

        # If a PHP file is served, this block will handle the request. This block
        # works on the assumption you are using php-cgi listening on /tmp/phpcgi.socket.
        # Please see the php example (usr/share/doc/nginx/exmaples/php) for more
        # information about setting up PHP.
        # NOTE: You should have "cgi.fix_pathinfo = 0;" in php.ini
        location ~ .php$ {
                fastcgi_split_path_info ^(.+.php)(/.+)$;
                include fastcgi_params;
                # Intercepting errors will cause PHP errors to appear in Nginx logs
                fastcgi_intercept_errors on;
                fastcgi_pass unix:/var/run/php-fpm.sock;
        }

        # The ImageCache module builds an image 'on the fly' which means that
        # if it doesn't exist, it needs to be created. Nginx is king of static
        # so there's no point in letting PHP decide if it needs to be servered
        # from an existing file.
        # If the image can't be served directly, it's assumed that it doesn't
        # exist and is passed off to PHP via our previous rewrite to let PHP
        # create and serve the image.
        # Notice that try_files does not have $uri/ in it. This is because an
        # image should never be a directory. So there's no point in wasting a
        # stat to serve it that way.
        location ~ ^/sites/.*/files/imagecache/ {
                try_files $uri @rewrite;
        }

        # As mentioned above, Nignx is king of static. If we're serving a static
        # file that ends with one of the following extensions, it is best to set
        # a very high expires time. This will generate fewer requests for the
        # file. These requests will be logged if found, but not if they don't
        # exist.
        location ~* .(js|css|png|jpg|jpeg|gif|ico)$ {
                expires max;
                log_not_found off;
        }

        # Configuration for phpMyAdmin
        location /phpmyadmin {
               root /usr/share/;
               index index.php index.html index.htm;
               location ~ ^/phpmyadmin/(.+.php)$ {
                       try_files $uri =404;
                       root /usr/share/;
                       fastcgi_pass unix:/var/run/php-fpm.sock;
                       fastcgi_index index.php;
                       fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
                       include /etc/nginx/fastcgi_params;
               }
               location ~* ^/phpmyadmin/(.+.(jpg|jpeg|gif|css|png|js|ico|html|xml|txt))$ {
                       root /usr/share/;
               }
        }
        location /phpMyAdmin {
               rewrite ^/* /phpmyadmin last;
        }

}