Nginx as a caching reverse proxy for Apache


Scenario:

Consider you have been assigned a task to improve the response time of an application. Assume that, it is a php application running in a server with apache serving requests to the end user. You have identified that there is a lot of static content being served by apache. You needed a solution where the processing of static content should be off loaded from apache to make better use of its power for serving dynamic content. You also need a caching solution to further increase the response time.  You need to implement these solutions without the overhead of introducing new technologies that need to be learned and maintained.

 

Nginx as a caching reverse proxy for apache:

Apache and nginx are the two most widely used webservers .  Apache reigns as the number one web server for websites and  nginx takes the second place. Apache is very good in processing dynamic content with its powerful engine whereas  Nginx is very much faster compared to apache in serving static content. Further Nginx can act as a caching server when placed in front of web/application servers. By enabling caching, requests for static or dynamic assets that are cached, need not even reach the application (or static content) servers – our cache server can handle many requests all by itself!

This is exactly what our requirement is as per our scenario. We can get a better solution to our scenario by combining both these aspects of Apache and Nginx.  Let us see how we can achieve this.

 

What we are going to do?

We will perform the below steps to implement the above said things.

  • Install Nginx which will serve as the front end of our site
  • Install Apache which act as the origin server for Nginx
  • Configure Nginx to act as reverse proxy for apache
  • Configure Apache
  • Enable Nginx caching  and customize cache settings
  • Enable necessary apache modules
  • Install php and validate the setup

 

Consideration:

  •  We will be using Ubuntu 14.04 instance for this example. Modify the installation steps as per the distribution which you are running on.
  • We will be running both Nginx and Apache in the same server
  • Is is assumed that all the commands are executed as root user

 

Install Nginx

We can install nginx using apt-get package manager.

$ apt-get install nginx

 

If your package manager version of nginx is old and you want the latest one, then you can follow the below step to get the latest version.

$ echo "deb http://nginx.org/packages/ubuntu/ `lsb_release -cs` nginx" >> /etc/apt/sources.list.d/nginx.list

$ echo "deb-src http://nginx.org/packages/ubuntu/ `lsb_release -cs` nginx" >> /etc/apt/sources.list.d/nginx.list

$ apt-get update

$ apt-get install nginx

 

Note: You might receive a GPG error while running ‘apt-get update’ after updating the repository url for nginx as below.

 W: GPG error: http://nginx.org trusty Release: The following signatures couldn't be verified because the public key is not available: NO_PUBKEY ABF5BD827BD9BF62

 

Run the below command to fix this error.

 $ apt-key adv --keyserver keyserver.ubuntu.com --recv-keys $key 

Where $key is the value of NO_PUBKEY. In our case it is ABF5BD827BD9BF62

 

Check the version of nginx by running the below command

# nginx -v
nginx version: nginx/1.10.2

 

Check the status of nginx by running the below command

 $ service nginx status 
   * nginx is running

 

 

Install Apache

We can install apache using apt-get package manager.

But before that, we already have nginx running in port 80. When we try to install apache in the same machine, by default it’ll try to start itself in port 80 since it is the default http port for both nginx and apache.

We will get an error stating ‘The port is already in use’. In order to overcome this, we can prevent apache from starting after its installation using the below method.

Create a file with the name policy-rc.d under “/usr/sbin/policy-rc.d”

 touch /usr/sbin/policy-rc.d

 

The file should have the below content

#!/bin/sh
exit 101

 

Now install apache by running the below command

 $ apt-get install apache2

 

Now that we have got apache installed, we can remove the ‘policy-rc.d’ file which we created.

 $ rm -rf /usr/sbin/policy-rc.d

 

Verify the version of apache server by running the below command.

 $ apache2 -v
Server version: Apache/2.4.7 (Ubuntu)

 

Configure Nginx to act as reverse proxy for apache

Create a new configuration file under “/etc/nginx/conf.d”. Disable the default configuration file that comes with installation by removing it.

$ touch /etc/nginx/conf.d/mysite.conf

$ cd /etc/nginx/conf.d/

$ rm default.conf

For this example, we name the configuration file as mysite.conf . You can name it as per your need and requirement.

 

The below configuration considers that both apache and Nginx are running in the same host. The content of mysite.conf should be as below.

server {
        listen   80; 

        root /var/www/mysite; 
        index index.php index.html index.htm;

        server_name mysite.com; 

        proxy_set_header X-Real-IP  $remote_addr;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
        

        
        location ~ \.php$ {

                proxy_pass http://127.0.0.1:8080;
        }

        location / {

                try_files $uri $uri/  @backend;
        }

         

        location @backend {
                # essentially the same as passing php requests back to apache
                proxy_pass http://127.0.0.1:8080;
        }

        
        location ~ /\.ht {
                deny  all;
        }
     

}

 

The following changes were implemented in the configuration:

  • We have set the port that nginx should listen on using the listen directive.
  • The root was set to the correct web directory. This corresponds to Document root in apache
  • The index line specifies the default index file that should be served. In this case it first checks for index.php
  • The server_name specifies the FQDN or ip address for which this configuration will be applied on. Pass your respective domain name here.
  • Using the proxy_set_header, we are forwarding the client ip address to apache.
  • The first location directive ( location ~ \.php$ ) will check for php files. The proxy_pass attribute lets you define the address of the proxied server. In our case since we have apache server running in the same host, we have provided the address as http://127.0.0.1:8080 assuming that we will run apache in port 8080.
  • In the second location directive ( location / ) try_files attempts to serve whatever page the visitor requests. If nginx is unable to serve it, then the file is passed to the backend context ( location @backend ), where the proxy details are provided.
  • The ( location ~ /\.ht ) will refuse all requests for files beginning with the characters .ht . This directive is useful if your Apache deployment relies on settings from .htaccess and .htpasswd

 

There are few important things that you need to note here. The order of the location directive is important here. If we had kept the ( location / ) directive in the first place before ( location ~ \.php$ ), then if your index page is a php page (index.php) then the file gets downloaded instead of being rendered when you access it. This is because, we have not installed php-fpm which nginx requires in order to process php files. We have not installed it since we want our php files to be processed by apache.

Make sure that the Document root for apache and nginx are same. This will ensure that nginx can deliver static files directly without passing the request to Apache. Static files (like JavaScript, CSS, images, PDF files, static HTML files, etc.) can be delivered much faster with nginx than Apache.

At times specifying the proxy_pass address to loopback address (127.0.0.1) might break some functionalities in your application. In that case consider using the public ip for proxy_pass

 

SSL Configuration for Nginx

In case your have enabled SSL, then mysite.conf should be configured as below.

server {
        listen 80;
        server_name mysite.com;
        return 301 https://$server_name:443$request_uri;
  }


server {
        listen 443 ssl; 

        server_name mysite.com; 

        root /var/www/mysite; 
        index index.php index.html index.htm;

        # SSL configuration
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
        ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';
        ssl_prefer_server_ciphers on;
        ssl_session_cache shared:SSL:128m;
        ssl_session_timeout 10m;
        ssl_dhparam /etc/nginx/ssl/dhparam.pem;
        ssl_certificate /etc/nginx/ssl/chained-cert.crt;
        ssl_certificate_key /etc/nginx/ssl/private.key;

        # Proxy headers
        proxy_set_header X-Real-IP  $remote_addr;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
        

        
        location ~ \.php$ {

                proxy_pass http://backend_hosts;;
        }

        location / {

                try_files $uri $uri/  @backend;
        }

}

 

The following changes were implemented in the configuration:

  • The first server module redirects all http requests to https using the return statement.
  • We have further enabled nginx to use SSL by configuring the required SSL settings.

 

Configure Apache

We need to configure apache to take over the backend, which as we told nginx, will be running on port 8080. Make changes in the ports.conf file to make apache listen in the required port.

 $ vi /etc/apache2/ports.conf

 

Edit the value of  ‘Listen’ which instructs the port in which apache should run on. The content of ports.conf should look something like below.

Listen 8080

<IfModule ssl_module>
Listen 8443
</IfModule>

<IfModule mod_gnutls.c>
Listen 8443
</IfModule>

 

Next, create a new virtual host file, copying the layout from the default apache file:

$ cp /etc/apache2/sites-available/000-default.conf  /etc/apache2/sites-available/mysite.conf

 

Ideally, the content of the apache virtual host file mysite.conf should be as below.

<VirtualHost *:8080>

ServerAdmin [email protected]
DocumentRoot /var/www/mysite


ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined

</VirtualHost>

 

The following changes were implemented in the apache mysite.conf  file:

  • We have changed the value of Virtual host port from 80 to 8080.
  • We have made sure that the document root of apache is same as that of nginx.

 

Next, disable the default site and enable ‘mysite’ which we have just configured.

$ a2dissite 000-default.conf

$ a2ensite mysite.conf

 

Restart apache server

 $ service apache2 restart

 

Enable Caching in Nginx

Only two directives are needed to enable basic caching in Nginx: proxy_cache_path and proxy_cache. The proxy_cache_path directive sets the path and configuration of the cache, and the proxy_cache directive activates it.

 

Edit the nginx.conf file in which we will configure the cache settings

 vi /etc/nginx/nginx.conf

 

The content of the nginx.conf file should be something like below.

user  nginx;
worker_processes  1;

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


events {
    worker_connections  1024;
}


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

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

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

    # Log format for caching
    log_format cache '***$time_local '
                   'req_time=$request_time '
                   '$upstream_cache_status '
                   'Cache-Control: $upstream_http_cache_control '
                   'Expires: $upstream_http_expires '
                   '"$request" ($status) '
                   '"$http_user_agent" ';

    # Additional logging for caching
    access_log /var/log/nginx/cache.log cache;

    sendfile        on;
    tcp_nopush     on;

    server_tokens off;
    keepalive_timeout  65;

         # Enable gzip for response compression
         gzip  on;
         gzip_disable "msie6";
         gzip_vary on;
         gzip_proxied any;
         gzip_comp_level 6;
         gzip_buffers 16 8k;
         gzip_http_version 1.1;
         gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;


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

        # Enable caching
        proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=edge-cache:10m inactive=20m max_size=1g;
        proxy_temp_path /var/cache/nginx/tmp;

        # Further cache customization
        proxy_buffering on;
        proxy_cache_lock on;
        proxy_cache_use_stale updating;
        proxy_bind 0.0.0.0;
        proxy_cache_valid 5m;

        
        # Allow underscores in header
        underscores_in_headers on;

        # Set Max Client Body size to 10 MB        
        client_max_body_size 10M;   

}

 

The above settings are optimal for server having 1 CPU and 2GB RAM. If the number of CPU’s are higher, then the worker_process value can be altered accordingly.

There are few additional configurations apart from enabling caching from the default configuration. Those are highlighted in the snippet in blue colour.

Explaining all the configuration settings is beyond the scope of this article. Further reading from reader is appreciated in order to understand it and further customize it to adopt to your environment.

Let us breeze through some of the important setting’s that we should make a note of.

  • The $request_time parameter in log_format gives us the time taken for the request to be served to the client. It is given in milliseconds. This would be handy when monitoring performance.
  • We have additionally setup caching log apart from the regular nginx access log. $upstream_cache_status parameter in the log_format of caching log will show whether the request was a “MISS”, “BYPASS”, “EXPIRED”, “STALE”, “UPDATING”, “REVALIDATED”, or “HIT”.
  • We have disabled security tokens ( security_tokens off; ) for security reasons.
  • Gzip compression is enabled to enhance performance by compressing the response.
  • Caching is enabled using proxy_cache_path and proxy_cache directives. We have named our cache as ‘edge-cache‘ . For further reading on caching parameters, refer this
  • underscores_in_headers directive will enable us to use underscores in our headers. If your application uses headers that contain underscores, then you should enable this.
  • Using client_max_body_size directive you can set the size of the content that clients can send to the server. If your application accepts large files from end user, then you need to tweak this directive to better suit your needs.

 

Now that we have enabled caching, we should make changes in nginx’s mysite.conf file to make use of caching. Edit the ‘/etc/nginx/conf.d/mysite.conf’ file again and add the contents highlighted in blue below.

# /etc/nginx/conf.d/mysite.conf

server {

        listen   80; 

        root /var/www/mysite; 
        index index.php index.html index.htm;

        server_name mysite.com; 

        proxy_set_header X-Real-IP  $remote_addr;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;

        
        # Proxy Directives to make use of caching
        proxy_cache edge-cache;
        add_header X-Cache-Status $upstream_cache_status;
        add_header X-Handled-By $proxy_host;
        proxy_pass_request_headers on;
        proxy_cache_revalidate on;
        proxy_read_timeout 180s;

        # Use the below directives in order to allow basic authentication. Without this, nginx will not forward the basic authentication headers to apache.
        proxy_no_cache $cookie_nocache $arg_nocache$arg_comment;
        proxy_no_cache $http_pragma $http_authorization;
        proxy_cache_bypass $cookie_nocache $arg_nocache $arg_comment;
        proxy_cache_bypass $http_pragma $http_authorization;

        
        location ~ \.php$ {

                proxy_pass http://127.0.0.1:8080;
        }

        location / {

                try_files $uri $uri/  @backend;
        }

         

        location @backend {
                # essentially the same as passing php requests back to apache
                proxy_pass http://127.0.0.1:8080;
        }

        
        location ~ /\.ht {
                deny  all;
        }
     

}

 

If  your application makes use of basic authentication headers, then you need to make sure that it gets passed to apache. Refer the comments in the above snippet on how to accomplish this.

 

Enable necessary apache modules

We want the client ip’s to be reflected in apache logs as well. We would need to enable apache ‘remoteip‘ module for this. Without this, we will get the ip address that we use in proxy_pass in apache logs.

Run the below command to enable remoteip module

$ a2enmod remoteip

 

Manually create remoteip.conf file /etc/apache/conf-available .

 $ touch /etc/apache2/conf-available/remoteip.conf

Content of remoteip.conf should be as below.

RemoteIPHeader X-Forwarded-For
RemoteIPTrustedProxy 127.0.0.1

If your are using a different ip in the proxy_pass directive, then you need to pass that IP in RemoteIPTrustedProxy.

 

Install php and validate the setup

Run the below command to install php

 $ apt-get install php5

 

Validation

Alright !! We have everything in place. Lets test the setup now by creating some sample php and html files.

Run the below command to create a sample php and html file under document root.

 $ echo "<?php phpinfo(); ?>" > /var/www/mysite/index.php
 $ echo -e "<html> <body> <h1>Nginx Caching Reverse Proxy </h1> <p>nginx caching reverse proxy</p> </body> </html>" > /var/www/mysite/test.html

 

Let us test our setup by using curl command. Let us try accessing index.php file first.

$ curl -X GET -I 127.0.0.1/index.php
HTTP/1.1 200 OK
Server: nginx
Date: Sun, 23 Oct 2016 20:53:07 GMT
Content-Type: text/html
Transfer-Encoding: chunked
Connection: keep-alive
Vary: Accept-Encoding
X-Powered-By: PHP/5.5.9-1ubuntu4.20
Vary: Accept-Encoding
X-Cache-Status: MISS
X-Handled-By: 127.0.0.1:8080

It is a MISS because the file has not been requested before. Therefore the cache server (Nginx) needed to proxy the request to the Origin Server (Apache) to get the resource. Take a look at X-Handled-By which indicates that the request has been handled by apache

Let’s try accessing the same page again

$ curl -X GET -I 127.0.0.1/index.php
HTTP/1.1 200 OK
Server: nginx
Date: Sun, 23 Oct 2016 20:53:11 GMT
Content-Type: text/html
Transfer-Encoding: chunked
Connection: keep-alive
Vary: Accept-Encoding
X-Powered-By: PHP/5.5.9-1ubuntu4.20
Vary: Accept-Encoding
X-Cache-Status: HIT
X-Handled-By: 127.0.0.1:8080

Now we can see it was a cache HIT via the X-Proxy-Cache header. We have confirmed that our cache setup is working as expected.

 

Let’s access test.html file and see how it behaves

$ curl -X GET -I 127.0.0.1/test.html
HTTP/1.1 200 OK
Server: nginx
Date: Sun, 23 Oct 2016 21:02:56 GMT
Content-Type: text/html
Content-Length: 122
Last-Modified: Sun, 23 Oct 2016 20:20:24 GMT
Connection: keep-alive
Vary: Accept-Encoding
ETag: "580d1b88-7a"
Accept-Ranges: bytes

From the curl output it is clear that the html file has been served directly by nginx and it has not been proxied to apache. This confirms that nginx proxies only php requests to apache and it directly servers all other files by itself.

We have successfully setup nginx as a caching reverse proxy for apache and validated the same.

Post your queries / suggestions in the comment section.

DevopsIdeas

Established in 2016, a community where system admins and devops practitioners can find useful in-depth articles, latest trends and technologies, interview ideas, best practices and much more on Devops

Any Udemy course for $9.99 REDEEM OFFER
Any Udemy course for $9.99 REDEEM OFFER