Responsive Images using Nginx Image-Filter module

In this chapter, we will discuss about image manipulation using NGINX module called ngx_http_image_filter_module.  We can perform various image manipulation tasks like rotating, resizing, cropping, sharpening, changing quality etc of images.  This is very much helpful in optimizing responsiveness of the website and also helps with  better user experience. Manually creating various sizes of same image to maintain responsiveness of the website takes a lot of time and resources. But with the help of image filter module, we can generate these various image sizes on the fly without any manual intervention. If you have not already installed NGINX in your system, go to the following link:

https://www.nodexplained.com/deploy-web-applications-with-nginx-web-server/

Now that the NGINX web server is successfully installed, check the version of it.

   
   	 nginx -v
   

For successful installation, it should give following output:

   
   	 nginx version: nginx/1.22.0
   

If you want to know, how to enable additional features in NGINX using modules in detail, go to the following link:

https://www.nodexplained.com/allow-or-block-visitors-from-certain-countries-using-nginx/

To enable transforming of images in various formats like jpeg, gif, png and webp, we need to first download the nginx source files of version 1.22.0, as shown in above version check section. Go to the following url to see the list of available download options:

https://nginx.org/download/

Navigate to the home directory and then download & extract nginx source files using following command:

   
cd $HOME
wget http://nginx.org/download/nginx-1.22.0.tar.gz
tar -xvf nginx-1.22.0.tar.gz
cd nginx-1.22.0/
   

In the nginx source folder directory, we can see following contents:

NGINX source files

We need to install some software dependencies before moving ahead:

In Amazon Linux 2:

   
   	sudo yum install gcc gcc-c++ make pcre-devel zlib-devel openssl-devel kernel-devel automake libtool  -y
   
   
	sudo yum install nginx-module-image-filter -y
   

In Ubuntu:

   
   	sudo apt-get install build-essential libpcre3-dev zlib1g-dev libssl-dev -y
   
   
	sudo apt-get install nginx-module-image-filter -y
   


Since the ngx_http_image_filter_module is not built by default, we need to enable it using --with-http_image_filter_module configuration parameter. Also, the module compilation and utilization is dependent on libgd library. GD is an open source code library for dynamic creation of images and is written in C. Various image formats like BMP, GIF, TGA, WebP, PNG, JPEG, TIFF, AVIF and many others are supported by it. To download it, go to the following link and then download the latest release tag. As of writing this article, latest available release tag is 2.3.3

https://github.com/libgd/libgd/releases

Download library and extract the files:

   
cd $HOME
wget https://github.com/libgd/libgd/releases/download/gd-2.3.3/libgd-2.3.3.tar.gz
tar -xvf libgd-2.3.3.tar.gz
cd libgd-2.3.3/
   

Install GD library using following commands:

   
./configure
make
make check
sudo make install
   


Now that, all of our pre-requisite requirements are fulfilled, we can start the compilation of NGINX with dynamic image filter module. Issue following command to start the compilation process:

   
cd $HOME/nginx-1.22.0/
./configure --with-compat --with-http_image_filter_module
   

After successful compilation of NGINX with image filter module, we need to run the following command to build and install it:

   
make
sudo make install
   


After successful installation, check the modules folder inside of /etc/nginx directory. You will see some new files there and one of them is ngx_http_image_filter_module.so file, as shown in below image:

NGINX image filter module files

Finally, we are ready to use image manipulation feature with NGINX. Adding the module exposes many additional directives. Some of them, which we will be using in this chapter are as follows:

image_filter

To use image_filter directive, following is the syntax that we need to follow:

   
Syntax:
image_filter rotate 90 | 180 | 270;
image_filter resize width height;
image_filter crop width height;
Default:	
image_filter off;
Context:	location
   
  • rotate 90 | 180 | 270

    It rotates the image counter-clockwise using the specified number of degrees. Only 90, 180 & 270 values are supported and it can be used in combination with resize and crop transformations. rotate happens after resize and before crop operations.
  • resize width height

    It proportionally reduces size of an image to the specified width and height. To reduce by only one dimension, specify another dimension as "-"

    returns 415 (Unsupported Media Type), if error is occurred
  • crop width height

    It proportionally reduces an image to the larger side size and crops extraneous edges by another side. To reduce by only one dimension, specify another dimension as "-"

    returns 415 (Unsupported Media Type), if error is occurred

image_filter_buffer


This is one of the most important directive, we need to specify when doing image manipulation using NGINX. It sets the maximum buffer size for reading images. When the size is exceeded, server returns error 415 (Unsupported Media Type). Default size is 1MB.

   
Syntax:	image_filter_buffer size;
Default:	
image_filter_buffer 1M;
Context:	http, server, location
   

image_filter_jpeg_quality


It sets the quality of transformed JPEG images. Values accepted by this directive are from 1 to 100. Higher the value means higher the image quality.

   
Syntax:	image_filter_jpeg_quality quality;
Default:	
image_filter_jpeg_quality 75;
Context:	http, server, location
   

image_filter_webp_quality


It sets the quality of transformed WebP images. Values accepted by this directive are from 1 to 100. Higher the value means higher the image quality.

   
Syntax:	image_filter_webp_quality quality;
Default:	
image_filter_webp_quality 80;
Context:	http, server, location
   

For other directives, please go to the following link:

https://nginx.org/en/docs/http/ngx_http_image_filter_module.html

We need to load ngx_http_image_filter_module.so file in the main configuration context. To do that, modify nginx.conf file and load the module right after pid directive or at the very top of main configuration file, before any block directives. We can load a module using load_module directive and it is only allowed in the main configuration context.

   
   load_module modules/ngx_http_image_filter_module.so;
   


Now the basic nginx.conf file looks like below:

   
user  nginx;
worker_processes  auto;

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

load_module modules/ngx_http_image_filter_module.so;

events {
    worker_connections  65535;
}

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

    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;
    keepalive_timeout  65;
    include /etc/nginx/conf.d/*.conf;
}

   

Also, let's modify default.conf file, located at conf.d directory and add multiple location blocks to handle image filtering process as following:

   
server {
    listen       80;
    server_name  localhost;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

    image_filter_buffer 10M;
    image_filter_webp_quality 80;
    image_filter_jpeg_quality 95;

    location ~ ^/images/(?<width>.*)/(?<height>.*)/(?<filename>.*)$ {
        alias   /usr/share/nginx/html/images/$filename;
        image_filter resize $width $height;
    }

	location ~ ^/images/(?<width>.*)/(?<filename>.*)$ {
        alias   /usr/share/nginx/html/images/$filename;
        image_filter resize $width -;
    }

    location ~ ^/images/(?<filename>.*)$ {
        alias   /usr/share/nginx/html/images/$filename;
        # try_files /$filename = 404;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}
   

Check the correctness of recent configuration file changes using following command:

   
   	sudo nginx -t
   

Reload the NGINX configuration using following command:

   
   	sudo nginx -s reload
   


Now, we can test image filter feature in the browser using HTML5 img tag. In img tag, we have a  srcset attribute which defines various sizes of same image, allowing browser to select the most appropriate image source, as per the device. Modify index.html file located at /usr/share/nginx/html/ folder and paste the following html content. The example shows srcset attribute with width descriptors.

Note:

For you, the folder location for index.html file can be different than /usr/share/nginx/html folder. Check the root directive in default.conf file.

Specify image path as according to your setup.

To modify the files stored at /usr/share/nginx/html folder, we need to give necessary permissions to the current user. To check for owner and group of a folder, run the following command:

   
   	ls -l /usr/share/nginx/html/
   

If current user is not shown as the owner of index.html file, run following command so that you can edit the index.html file without sudo access.

In Amazon Linux 2:

   
   	sudo chown ec2-user:ec2-user -R /usr/share/nginx/html/
   

In Ubuntu:

   
   	sudo chown ubuntu:ubuntu -R /usr/share/nginx/html/
   


index.html file looks like below:

   
<!DOCTYPE html>
<html>
<head>
  <title>Image Filtering with NGINX</title>
</head>
<body>

  <h1>Image filetering with NGINX</h1>
  <img src="/images/nagarkot.jpg"
  sizes="(max-width: 480px) 100vw, (max-width: 2400px) 50vw, 100vw"
  srcset="/images/400/nagarkot.jpg 400w,
          /images/800/nagarkot.jpg 800w, /images/1200/nagarkot.jpg 1200w, /images/1600/nagarkot.jpg 1600w">
</body>
</html>
   

Result is:

NGINX serving various image sizes for the same images using srcset attribute


The way we have performed image processing using NGINX as shown above, has some serious performance issues - relatively it consumes a high amount of CPU & memory than other tasks, and when there is a lot of customer traffic, it can create a very significant load on the web server, thereby impacting our main application web server. So, in productions scenarios, the best way to handle this is to create a separate auto-scalable image processing server, which handles all the image manipulation tasks. The main web server will only proxy the image manipulation requests to an image processing server.

Image filter module with nginx using separate image processing server

Main application web server:

Remove following line from nginx.conf file, if it exists already:

   
   load_module modules/ngx_http_image_filter_module.so;
   

We will proxy all of the image manipulation requests to an image processing server in the following way. Modify default.conf file, located at conf.d directory.

   
server {
    listen       80;
    server_name  localhost;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

    location /images/ {
        proxy_pass http://172.26.10.108;
        proxy_set_header Host $host;
		proxy_set_header X-Real-IP $remote_addr;
		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
		proxy_set_header X-Forwarded-Proto $scheme;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}
   


Replace 172.26.10.108 with internal IP address of an image processing server.

Image Processing Server:

Since this is a new server instance, we need to repeat the process of adding dynamic module ngx_http_image_filter_module, as we did above. Once this NGINX web server is compiled, build and installed with image filter module, add following line to the nginx.conf file:

   
   load_module modules/ngx_http_image_filter_module.so;
   


Now, nginx.conf file looks like below:

   
user nginx;
worker_processes  auto;

error_log   /var/log/nginx/error.log;
error_log   /var/log/nginx/error_debug.log debug;
error_log   /var/log/nginx/error_extreme.log emerg;
error_log   /var/log/nginx/error_critical.log crit;

pid        /var/run/nginx.pid;

load_module modules/ngx_http_image_filter_module.so;

events {
    worker_connections  65535;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
    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;
    keepalive_timeout  65;
    include /etc/nginx/conf.d/*.conf;
}
   


Modify default.conf file to include multiple location blocks, which will handle all the image manipulation tasks. The default.conf file looks like below:

   
server {
    listen       80;
    server_name  localhost;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

    image_filter_buffer 10M;
    image_filter_webp_quality 80;
    image_filter_jpeg_quality 95;

    location ~ ^/images/(?<width>.*)/(?<height>.*)/(?<filename>.*)$ {
        alias   /usr/share/nginx/html/images/$filename;
        image_filter resize $width $height;
    }

    location ~ ^/images/(?<width>.*)/(?<filename>.*)$ {
        alias   /usr/share/nginx/html/images/$filename;
        image_filter resize $width -;
    }

    location ~ ^/images/(?<filename>.*)$ {
        alias   /usr/share/nginx/html/images/$filename;
        # try_files /$filename = 404;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}
   


Now, you can test these recent changes, in the browser using test approach, that we discussed earlier.

This solves some of the performance issues, that we have specified in our earlier section, as the server load is now shifted from main application web server to an image processing server. However, there is still some issue left with this approach, as for every image manipulation request, we still need to execute the image filter tasks. With increasing number of customer requests, this will generate a huge amount of server load to an image processing server. So, to solve this, we can implement various caching techniques both in backend as well in frontend. When caching is enabled, all the cached files are saved in a disk as specified by proxy_cache_path directive. Subsequent client requests are served from that cached location.

Image filter module with nginx using separate image processing server with caching enabled

Modify default.conf file in image processing server and paste the following contents:

   
proxy_cache_path /usr/share/nginx/cache levels=1:2 keys_zone=image_cache:10m max_size=10g inactive=24h use_temp_path=off;

server {
    listen       3000;
    server_name _;

    root   /usr/share/nginx/html;

    image_filter_buffer 10M;
    image_filter_webp_quality 80;
    image_filter_jpeg_quality 95;

	location ~ ^/images/(?<width>.*)/(?<height>.*)/(?<filename>.*)$ {
        alias   /usr/share/nginx/html/images/$filename;
        image_filter resize $width $height;
    }

	location ~ ^/images/(?<width>.*)/(?<filename>.*)$ {
        alias   /usr/share/nginx/html/images/$filename;
        image_filter resize $width -;
    }

    location ~ ^/images/(?<filename>.*)$ {
        alias   /usr/share/nginx/html/images/$filename;
        try_files /$filename = 404;
    }
}

server {
    listen       80;
    server_name  localhost;

    root   /usr/share/nginx/html;

	location ~ ^/images/(?<width>.*)/(?<height>.*)/(?<filename>.*)$ {
        proxy_cache image_cache;
        proxy_cache_revalidate on;
        proxy_cache_use_stale error timeout updating http_500 http_502
                              http_503 http_504;
        proxy_cache_background_update on;
        proxy_cache_lock on;
        proxy_set_header       Host $host;
        proxy_buffering        on;
		proxy_cache_valid      200  30d;
        add_header X-Cache-Status $upstream_cache_status;
        proxy_pass http://127.0.0.1:3000/images/$width/$height/$filename;
    }

	location ~ ^/images/(?<width>.*)/(?<filename>.*)$ {
        proxy_cache image_cache;
        proxy_cache_revalidate on;
        proxy_cache_use_stale error timeout updating http_500 http_502
                              http_503 http_504;
        proxy_cache_background_update on;
        proxy_cache_lock on;
        proxy_set_header       Host $host;
        proxy_buffering        on;
		proxy_cache_valid      200  30d;
        add_header X-Cache-Status $upstream_cache_status;
        proxy_pass http://127.0.0.1:3000/images/$width/$filename;
    }

    location ~ ^/images/(?<filename>.*)$ {
        alias   /usr/share/nginx/html/images/$filename;
        try_files /$filename = 404;
    }
}
   


As you can see, there is some duplicate code issue in the above configuration content. You can fix that by moving all the duplicated directives in a separate file and then include the file in both the proxy_pass location blocks.

Go to the following links to get detail information about content caching and the directives associated to it:

https://docs.nginx.com/nginx/admin-guide/content-cache/content-caching/

https://www.nginx.com/blog/nginx-caching-guide/

Reload the nginx configuration and then test the changes using width descriptors. Following example shows the srcset attribute with it. Paste the content in index.html file.

   
<!DOCTYPE html>
<html>
<head>
  <title>Image Filtering with NGINX</title>
</head>
<body>

  <h1>Image filetering with NGINX</h1>
  <img src="/images/nagarkot.jpg"
  sizes="(max-width: 480px) 100vw, (max-width: 2400px) 50vw, 100vw"
  srcset="/images/400/300/nagarkot.jpg 400w,
          /images/800/600/nagarkot.jpg 800w, /images/1200/900/nagarkot.jpg 1200w, /images/1600/1200/nagarkot.jpg 1600w">
</body>
</html>
   

To find the number of cached files:

You may need to use sudo keyword for the below command.

   
   	find /usr/share/nginx/cache/ -type f | wc -l
   

Output: Currently there are 5 files in cached directory

>> 5

To check the size of cached files:

You may need to use sudo keyword for the below command.

   
	du -sh /usr/share/nginx/cache/
   

Output will be as follows: Currently the size of cached files are 612KB.

   
   	612K	/usr/share/nginx/cache/
   


To purge the cache, we need to empty the folder specified by proxy_cache_path directive as shown below:

   
   	sudo rm -rf /usr/share/nginx/cache/*
	sudo find /usr/share/nginx/cache/  -type f -delete
   


Note:

For security, we should enable rate limiting on image processing server, as malicious persons can target it. We will look into rate limiting in detail in our upcoming chapters.

That's it for this chapter.


Other tutorials on NGINX:

Chapter - 1: Deploy Web applications with NGINX web server

Chapter 2 - Set Up Load Balancing using Nginx web server

Chapter - 3: Block visitors from certain countries using NGINX