Friday 10 May 2024

Introduction to Nginx

 




What is Nginx?

  • HTTP and reverse proxy server
  • widely used webserver 


How to install Nginx on Linux?


Nginx serves a static HTML file by default so we can verify the deployment.


Where is Nginx binary?

# which nginx
/usr/sbin/nginx

To check Nginx version:

# nginx -v
nginx version: nginx/1.24.0


Nginx Configuration



Nginx configuration file is /etc/nginx/nginx.conf.

cat /etc/nginx/nginx.conf
#######################################################################
#
# This is the main Nginx configuration file.
#
# More information about the configuration options is available on
#   * the English wiki - http://wiki.nginx.org/Main
#   * the Russian documentation - http://sysoev.ru/nginx/
#
#######################################################################

#----------------------------------------------------------------------
# Main Module - directives that cover basic functionality
#
#   http://wiki.nginx.org/NginxHttpMainModule
#
#----------------------------------------------------------------------

user                 mywebapp-user;
# user                 nginx;

worker_processes     auto;
worker_rlimit_nofile 10000;

#timer_resolution 100ms;

error_log  /var/log/nginx/error.log;
#error_log  /var/log/nginx/error.log  notice;
#error_log  /var/log/nginx/error.log  info;

pid        /var/run/nginx.pid;

#----------------------------------------------------------------------
# Events Module
#
#   http://wiki.nginx.org/NginxHttpEventsModule
#
#----------------------------------------------------------------------

events {
worker_connections  4000;
# worker_connections  1024;
use epoll;
#    accept_mutex on;
}

#----------------------------------------------------------------------
# HTTP Core Module
#
#   http://wiki.nginx.org/NginxHttpCoreModule
#
#----------------------------------------------------------------------

http {

server_names_hash_bucket_size 128;

        map $http_cf_ipcountry $allowed_country {
           default 1;
           "KP"    0; # Korea, Democratic Peoples Republic of (North Korea)
        }

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

        log_format    main '$remote_addr - $remote_user [$time_local] "$host"
                      "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for" 
                      "$http_cf_ipcountry"';

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

gzip            on;
gzip_vary       on;
gzip_types      text/plain 
                        text/xml 
                        text/css 
                        application/json
                        application/x-javascript;
gzip_min_length 1024;

server_tokens off;

# Replace client IP from proxies
real_ip_header X-Forwarded-For;
set_real_ip_from 10.0.0.0/8;  # AWS VPC CIDR
set_real_ip_from 108.22.214.0/22; # Cloudflare 
...
set_real_ip_from 2b06:27c0::/29; # Cloudflare 
real_ip_recursive on;

sendfile        on;
# tcp_nopush      on;
# tcp_nodelay     on;

keepalive_timeout  60;

server {
  listen 80;
server_name "";
return 403;
}

# Load config files from the /etc/nginx/conf.d directory
include /etc/nginx/conf.d/*.conf;
}

This is a base config file which is usually enough if we're hosting a single website.

If we're hosting multiple websites/domains/brands, we want to keep their configuration isolated in separate files which reside in /etc/nginx/conf.d/*.conf. We create a config file for each website and define for example different error and access log files.

ls -la /etc/nginx/conf.d/
total 56
drwxr-xr-x. 3 root     root       175 May  8 13:07 .
drwxr-xr-x. 4 root     root     16384 May  8 13:07 ..
drwxr-x---. 2 ec2-user ec2-user 16384 May  8 13:07 includes
-rw-r-----. 1 ec2-user ec2-user  1474 May  8 13:03 nginx.brandA.conf
-rw-r-----. 1 ec2-user ec2-user  1434 May  8 13:03 nginx.brandB.conf
-rw-r-----. 1 ec2-user ec2-user  1545 May  8 13:03 nginx.brandC.conf
-rw-r-----. 1 ec2-user ec2-user   858 May  8 13:03 nginx_status.conf

Let's check what can be in the override file: 

cat  /etc/nginx/conf.d/nginx.brandA.conf 
server {

# server settings
listen      80;
# listen      443 ssl;

server_name ~^(dev-|stage-|beta-)?(mywebapp)\.mydomainA\.com$;
      # server_name phpfpm.local;
      # server_name localhost; 
               
resolver    149.214.169.253; # Amazon EC2 inside VPC

# log settings
error_log /home/mywebapp-user/logs/nginx/error-mywebapp.log;
access_log /home/mywebapp-user/logs/nginx/access-mywebapp.log main;

# client settings
client_body_temp_path /home/mywebapp-user/tmp/client_body;

        # block visitors from certain countries
        if ($allowed_country = 0) {
          return 403; # forbidden
        }

# path settings
root  /home/mywebapp-user/public;
index index.html index.htm index.php;

# protect private files
        location ~ ^/private/(.*)$ {
include conf.d/includes/nginx.trusted-ips.conf;
include conf.d/includes/nginx.php-fpm-brandA.conf;
}

        # serve dmg files
        location ~ \.dmg$ {
            try_files $uri $uri/ /index.php?$query_string;
        }

        # serve pkg files
        location ~ \.pkg$ {
            try_files $uri $uri/ /index.php?$query_string;
        }

        # serve bz2 files
        location ~ \.bz2$ {
            try_files $uri $uri/ /index.php?$query_string;
        }

        # serve zip files
        location ~ \.zip$ {
            try_files $uri $uri/ /index.php?$query_string;
        }

        # serve exe files
        location ~ \.exe$ {
            add_header Content-Disposition "attachment";
            try_files $uri $uri/ /index.php?$query_string;
        }

        # return 404 for anything else
        location / {
           return 404;
        }

        # block countries defined in nginx.conf
        include conf.d/includes/nginx.geoip2.conf;

# allow .php files to be executed
include conf.d/includes/nginx.php-fpm-brandA.conf;
}


server block defines a virtual server that will handle requests. We can define multiple server blocks to handle different domains or ports.

  • listen <port_number> 
    • Listen on port <port_number> for incoming HTTP connections. 
  • listen <port_number> ssl
    • Listen on port <port_number> for incoming HTTPS connections. 
  • server_name <domain_name | IP>
    • Domain name or IP address that this server block should respond to.
    • localhost is typically used for local development. For local development we can actually use an arbitrary domain name value but then in /etc/hosts we need to add a line: 127.0.0.1 <domain_name>
    • For a live server, replace this with the actual domain name.
  • resolver
  • error_log
  • access_log
  • client_body_temp_path
  • root
    • Set the root directory for the server. This is where Nginx will look for files to serve.
    • This is where our application files should be located.
    • /var/www/html is a common directory for web content. 
  • index <file_1> <file_2> ... <file_N>
    • Specifies the default files to serve when a directory is requested.
    • If a user navigates to http://example.com/, Nginx will look for index.html, index.htm and index.php, in that order.
  • location / { ... }
    • Defines how to process requests for the root URL path.
    • This block specifies how to handle requests to the root of the website.
  • location ~ \.php$  { ... }
    • This block handles requests for PHP files. 
    • The tilde (~) indicates a regular expression match. Any request that ends in .php will be processed by this block.
  • include


location block attributes:

  • fastcgi_pass
    • Specifies the address and port of the FastCGI server (PHP-FPM).
    • Example: fastcgi_pass php-fpm:9000;
      • In this case, php-fpm:9000 indicates that PHP-FPM is running on the php-fpm service on port 9000. php-fpm matches the hostname of the machine which runs the PHP-FMP service 
  • fastcgi_index
    • Sets the default file to serve if the FastCGI request points to a directory. If a directory is requested, Nginx will serve index.php from that directory.
    • Example: 
      • fastcgi_index index.php;
      • fastcgi_index helloworld.php;
  • include fastcgi_params;
    • Includes a file with FastCGI parameters. These parameters are necessary for Nginx to communicate with PHP-FPM. The fastcgi_params file typically resides in /etc/nginx/ or /etc/nginx/conf.d/.
  •  fastcgi_param - sets FastCGI parameter's value that Nginx will send to php-fpm service (which is usually listening on port 9000)
    • SCRIPT_FILENAME $document_root$fastcgi_script_name;
      • Sets the SCRIPT_FILENAME parameter, which tells PHP-FPM the path of the script to execute. The value is constructed by appending the script name ($fastcgi_script_name) to the document root ($document_root).
    • PATH_INFO $fastcgi_path_info;
  • try_files path1 [path2] uri - Checks for the existence of files in order, and returns the first file that is found. A trailing slash indicates a directory - $uri /. In the event that no file is found, an internal redirect to the last parameter is invoked. The last parameter is the fallback URI and must exist, or else an internal error will be raised.
        
Nginx config file variables:

  • $document_root
    • a variable that represents the root directory for the current request. It is used to specify the base directory from which files are served. This variable is particularly useful when configuring PHP handling, redirections, and other file-serving directives.
    • holds the path to the root directory specified in the root directive of the server or location block handling the request.
    • commonly used in conjunction with other variables like $fastcgi_script_name to construct the full path to the PHP script being executed or served.
    • Example: if root is set to /var/www/html, a request is made for /path/to/file.html (https://www.example.com/path/to/file.html), Nginx will look for the file at /var/www/html/path/to/file.html
  • $fastcgi_script_name
    • path of the requested PHP script relative to the document root. It is used to pass the script's path to PHP-FPM for processing.
    • Suppose a client requests http://example.com/index.php
      • If $document_root is /var/www/html,  Nginx will look in /var/www/html for index.php
      • $fastcgi_script_name would be /index.php, the path relative to $document_root.
      • The full path passed to PHP-FPM via fastcgi_param SCRIPT_FILENAME would be /var/www/html/index.php.

--- --- ---

Before restarting or reloading Nginx server completely it is advisable to test its configuration file:

nginx -t
nginx: [warn] could not build optimal types_hash, you should increase either types_hash_max_size: 1024 or types_hash_bucket_size: 64; ignoring types_hash_bucket_size
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

If we try to run this command as non-admin user, we'll get an error:

nginx -t
nginx: [alert] could not open error log file: open() "/var/log/nginx/error.log" failed (13: Permission denied)
2024/05/09 16:12:33 [emerg] 349382#349382: open() "/etc/nginx/nginx.conf" failed (13: Permission denied)
nginx: configuration file /etc/nginx/nginx.conf test failed


Nginx Service


To check if Nginx service is running:

# systemctl status nginx
● nginx.service - The nginx HTTP and reverse proxy server
     Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; preset: disabled)
    Drop-In: /usr/lib/systemd/system/nginx.service.d
             └─php-fpm.conf
     Active: active (running) since Wed 2024-05-08 13:31:37 UTC; 1 day 19h ago
    Process: 3797 ExecStartPre=/usr/bin/rm -f /run/nginx.pid (code=exited, status=0/SUCCESS)
    Process: 3804 ExecStartPre=/usr/sbin/nginx -t (code=exited, status=0/SUCCESS)
    Process: 3805 ExecStart=/usr/sbin/nginx (code=exited, status=0/SUCCESS)
   Main PID: 3806 (nginx)
      Tasks: 3 (limit: 2120)
     Memory: 9.6M
        CPU: 42.250s
     CGroup: /system.slice/nginx.service
             ├─3806 "nginx: master process /usr/sbin/nginx"
             ├─3807 "nginx: worker process"
             └─3808 "nginx: worker process"

May 08 13:31:37 ip-10-107-112-168.us-east-1.env.my-domain.com systemd[1]: Starting nginx.service - The nginx HTTP and reverse proxy server...
May 08 13:31:37 ip-10-107-112-168.us-east-1.env.my-domain.com nginx[3804]: nginx: [warn] could not build optimal types_hash, you should increase either types_hash_max_size: 1024 or types_hash_bucket_size: 64; ignoring types_hash_bucket_size
May 08 13:31:37 ip-10-107-112-168.us-east-1.env.my-domain.com nginx[3804]: nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
May 08 13:31:37 ip-10-107-112-168.us-east-1.env.my-domain.com nginx[3804]: nginx: configuration file /etc/nginx/nginx.conf test is successful
May 08 13:31:37 ip-10-107-112-168.us-east-1.env.my-domain.com nginx[3805]: nginx: [warn] could not build optimal types_hash, you should increase either types_hash_max_size: 1024 or types_hash_bucket_size: 64; ignoring types_hash_bucket_size
May 08 13:31:37 ip-10-107-112-168.us-east-1.env.my-domain.com systemd[1]: Started nginx.service - The nginx HTTP and reverse proxy server.

If we try to run the binary when service is already working, it will try to bind to local ports which would obvously fail:

# nginx
nginx: [warn] could not build optimal types_hash, you should increase either types_hash_max_size: 1024 or types_hash_bucket_size: 64; ignoring types_hash_bucket_size
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
nginx: [emerg] bind() to 0.0.0.0:81 failed (98: Address already in use)
nginx: [emerg] still could not bind()


To verify that nginx processes are listening on TCP ports 80 and 81 we can use lsof (list open files) command with option -i (match Internet address):

# lsof -i TCP:80
COMMAND    PID          USER   FD   TYPE  DEVICE SIZE/OFF NODE NAME
nginx     3806          root   18u  IPv4   20970      0t0  TCP *:http (LISTEN)
nginx   559036 my-user    3u  IPv4 3101478      0t0  TCP ip-10-107-112-168.ec2.internal:http->ip-10-107-112-40.ec2.internal:36372 (ESTABLISHED)
nginx   559036 my-userk    9u  IPv4 3101479      0t0  TCP ip-10-107-112-168.ec2.internal:http->ip-10-107-112-141.ec2.internal:14406 (ESTABLISHED)
...
nginx   559036 my-user   15u  IPv4 3101485      0t0  TCP ip-10-107-112-168.ec2.internal:http->ip-10-107-112-59.ec2.internal:29186 (ESTABLISHED)
nginx   559036 my-user   18u  IPv4   20970      0t0  TCP *:http (LISTEN)
nginx   559037 my-user   18u  IPv4   20970      0t0  TCP *:http (LISTEN)

# lsof -i TCP:81
COMMAND    PID          USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
nginx     3806          root   19u  IPv4  20971      0t0  TCP *:81 (LISTEN)
nginx   559036 my-user   19u  IPv4  20971      0t0  TCP *:81 (LISTEN)
nginx   559037 my-user   19u  IPv4  20971      0t0  TCP *:81 (LISTEN)


Changing the configuration


After changing nginx.conf we need to restart nginx:

$ /etc/init.d/nginx stop
rm: cannot remove ‘/var/run/nginx.pid’: Permission denied  [FAILED]

We need to become root in order to restart nginx:

# /etc/init.d/nginx stop
Stopping nginx:                                            [  OK  ]

# /etc/init.d/nginx start
Starting nginx:                                            [  OK  ]


To reload the configuration without downtime:

# systemctl reload  nginx


Let's check the status again:

# systemctl status nginx
● nginx.service - The nginx HTTP and reverse proxy server
     Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; preset: disabled)
    Drop-In: /usr/lib/systemd/system/nginx.service.d
             └─php-fpm.conf
     Active: active (running) since Wed 2024-05-08 13:31:37 UTC; 1 day 19h ago
    Process: 3797 ExecStartPre=/usr/bin/rm -f /run/nginx.pid (code=exited, status=0/SUCCESS)
    Process: 3804 ExecStartPre=/usr/sbin/nginx -t (code=exited, status=0/SUCCESS)
    Process: 3805 ExecStart=/usr/sbin/nginx (code=exited, status=0/SUCCESS)
    Process: 559035 ExecReload=/usr/sbin/nginx -s reload (code=exited, status=0/SUCCESS)
   Main PID: 3806 (nginx)
      Tasks: 3 (limit: 2120)
     Memory: 10.3M
        CPU: 42.343s
     CGroup: /system.slice/nginx.service
             ├─  3806 "nginx: master process /usr/sbin/nginx"
             ├─559036 "nginx: worker process"
             └─559037 "nginx: worker process"

May 08 13:31:37 ip-10-107-112-168.us-east-1.env.my-domain.com systemd[1]: Starting nginx.service - The nginx HTTP and reverse proxy server...
...
May 10 09:11:32 ip-10-107-112-168.us-east-1.env.my-domain.com systemd[1]: Reloading nginx.service - The nginx HTTP and reverse proxy server...
May 10 09:11:32 ip-10-107-112-168.us-east-1.env.my-domain.com nginx[559035]: nginx: [warn] could not build optimal types_hash, you should increase either types_hash_max_size: 1024 or types_hash_bucket_size: 64; ignoring types_hash_bucket_size
May 10 09:11:32 ip-10-107-112-168.us-east-1.env.my-domain.com systemd[1]: Reloaded nginx.service - The nginx HTTP and reverse proxy server.


Running Nginx & PHP-FPM as Docker containers


Nginx and PHP-FPM can run separately, each in its own Docker container. For Nginx, we can use the original Nginx image and for PHP-FPM we can crate a custom image, which also contains our PHP application.

It is important that both containers need to have PHP application at the same path
  • In PHP-FPM Dockerfile we can set WORKDIR to e.g. /var/www/html or /usr/share/nginx/app and copy our application's source code there
  • In Nginx conf file, in server block, we then need to set root to the absolute path to the main php script e.g. index.php. If web requests is http://example.com/path/to/index.php and path to this script in PHP-FPM container is e.g. /usr/share/nginx/app/index.php then in nginx conf we'll use  /usr/share/nginx/app/ as the value of root ($document_root)
  • As Nginx container needs to have an access to PHP app's source code, we'll then use a volume which maps local directory with app to /var/www/html or /usr/share/nginx/app

----

References:



No comments: