Skip to main content

Nginx

Install

# Pass buntu
sudo apt-get update

sudo apt-get install nginx

sudo ufw app list

sudo ufw allow 'Nginx Full'
sudo ufw allow 80
sudo ufw allow 443

sudo ufw enable

sudo ufw status

systemctl status nginx

# Try load home page:
curl localhost:80

Config

# Server config
sudo vi /etc/nginx/nginx.conf

# Site config is separable.
# Remove default 80 port web service
sudo ls /etc/nginx/sites-available/default
sudo rm /etc/nginx/sites-available/default
NGINX_CONFIG=<config>
sudo touch /etc/nginx/sites-available/$NGINX_CONFIG
sudo vi /etc/nginx/sites-available/$NGINX_CONFIG

# Make link for enabling site config
sudo ln -s /etc/nginx/sites-available/$NGINX_CONFIG /etc/nginx/sites-enabled/
sudo ls /etc/nginx/sites-enabled/

# Check config syntax
sudo nginx -t

# Restart
sudo systemctl restart nginx

lsof -i -P -n | grep :80

# Check user
ps aux | grep "nginx: worker process"

# Check permission of path
ls -ld /root/service/bitdove-static/

groups www-data
chown -vR :www-data /root/service/bitdove-static/
chmod -vR g+w /root/service/bitdove-static/

# 403, change user to the same as startup nginx
vi conf/nginx.conf
user root;

# To re-size the file to 0 bytes, without having to re-load or re-start nginx
sudo truncate --size 0 /var/log/nginx/access.log
sudo truncate --size 0 /var/log/nginx/access.log

rewrite

# The begin of match string
^

# Example
https://bitdove.net/web3-1/
Reg: web3-1(.*)$
Match1 web3-1/
Group1 /

map

The ngx_http_map_module module creates variables whose values depend on values of other variables.

map $args $foo {
default 0;
debug 1;
}

Variable

$http_name

Arbitrary request header field; the last part of a variable name is the field name converted to lower case with dashes replaced by underscores.

proxy_redirect

Include

/etc/nginx/proxy_params

proxy_set_header Host $http_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;

Optimization

Improve Latency

Enable HTTP2

# Change
listen 443 ssl;
# TO
listen 443 ssl http2;

# Check network package header's protocol property is "h2" or curl response header include HTTP/2.

Cipher priority

No need to modify if using Let's Encrypt.

/etc/letsencrypt/options-ssl-nginx.conf

# Manual chiper
ssl_prefer_server_ciphers on; # prefer a list of ciphers to prevent old and slow ciphers
ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';

OCSP Stapling

Let's Encrypt OCSP is slow in China.

After enable OCSP Stapling, Nginx will not response OCSP immediately, wait few minutes.

ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /path/to/full_chain.pem;

Test

openssl s_client -connect yourdomain.com:443 -servername yourdomain.com -status -tlsextdebug < /dev/null 2>&1 | grep -i "OCSP response"

# Example
openssl s_client -connect bitdove.elecdove.com:443 -servername bitdove.elecdove.com -status -tlsextdebug < /dev/null 2>&1 | grep -i "OCSP response"

# Success
OCSP response:
OCSP Response Data:
OCSP Response Status: successful (0x0)
Response Type: Basic OCSP Response

# Failed
OCSP response: no response sent

Tunning ssl_buffer_size

# Default = 16k
# Big file = 16k
# Web or REST API = 4k
ssl_buffer_size 4k;

Enable SSL Session cache

No need to modify if using Let's Encrypt.

/etc/letsencrypt/options-ssl-nginx.conf

1MB = 4000 connections

# Enable SSL cache to speed up for return visitors
ssl_session_cache shared:SSL:50m; # speed up first time.
ssl_session_timeout 4h;

Mixed Content: The page at ‘https://xxx.com’ was loaded over HTTPS, but requested an insecure stylesheet ‘http://xxx.com’. This request has been blocked. the content must be served over HTTPS.

nginx https site call http api.

add_header Content-Security-Policy "upgrade-insecure-requests"

Selection Algorithms

Block Configurations

Nginx logically divides the configurations meant to serve different content into blocks, which live in a hierarchical structure. Each time a client request is made, Nginx begins a process of determining which configuration blocks should be used to handle the request.

The main blocks are the server block and the location block.

A server block is a subset of Nginx’s configuration that defines a virtual server used to handle requests of a defined type. Administrators often configure multiple server blocks and decide which block should handle which connection based on the requested domain name, port, and IP address.

A location block lives within a server block and is used to define how Nginx should handle requests for different resources and URIs for the parent server. The URI space can be subdivided in whatever way the administrator likes using these blocks. It is an extremely flexible model.

Decides Server Block

Parsing the listen

First, Nginx looks at the IP address and the port of the request. It matches this against the listen directive of each server to build a list of the server blocks that can possibly resolve the request.

The listen directive can be set to:

  • An IP address/port combo.
  • A lone IP address which will then listen on the default port 80.
  • A lone port which will listen to every interface on that port.
  • The path to a Unix socket.

The last option will generally only have implications when passing requests between different servers.

When trying to determine which server block to send a request to, Nginx will first try to decide based on the specificity of the listen directive using the following rules:

  • Translates all “incomplete” listen directives by substituting missing values with their default values so that each block can be evaluated by its IP address and port.
  • Then attempts to collect a list of the server blocks that match the request most specifically based on the IP address and port. This means that any block that is functionally using 0.0.0.0 as its IP address (to match any interface), will not be selected if there are matching blocks that list a specific IP address. In any case, the port must be matched exactly.
  • If there is only one most specific match, that server block will be used to serve the request. If there are multiple server blocks with the same level of specificity matching, Nginx then begins to evaluate the server_name directive of each server block.

It is important to understand that Nginx will only evaluate the server_name directive when it needs to distinguish between server blocks that match to the same level of specificity in the listen directive.

Parsing the server_name

Nginx attempts to find the best match for the value it finds by looking at the server_name directive within each of the server blocks that are still selection candidates. Nginx evaluates these by using the following formula:

  • First try to find a server block with a server_name that matches the value in the Host header of the request exactly. If this is found, the associated block will be used to serve the request. If multiple exact matches are found, the first one is used.
  • If no exact match is found, Nginx will then try to find a server block with a server_name that matches using a leading wildcard (indicated by a * at the beginning of the name in the config). If one is found, that block will be used to serve the request. If multiple matches are found, the longest match will be used to serve the request.
  • If no match is found using a leading wildcard, Nginx then looks for a server block with a server_name that matches using a trailing wildcard (indicated by a server name ending with a * in the config). If one is found, that block is used to serve the request. If multiple matches are found, the longest match will be used to serve the request.
  • If no match is found using a trailing wildcard, Nginx then evaluates server blocks that define the server_name using regular expressions (indicated by a ~ before the name). The first server_name with a regular expression that matches the “Host” header will be used to serve the request.
  • If no regular expression match is found, Nginx then selects the default server block for that IP address and port.

Each IP address/port combo has a default server block that will be used when a course of action can not be determined with the above methods. For an IP address/port combo, this will either be the first block in the configuration or the block that contains the default_server option as part of the listen directive (which would override the first-found algorithm). There can be only one default_server declaration per each IP address/port combination.

Matching Location Blocks

Location Block Syntax

location optional_modifier location_match {
...
}

The location_match in the above defines what Nginx should check the request URI against. The existence or nonexistence of the modifier in the above example affects the way that the Nginx attempts to match the location block. The modifiers below will cause the associated location block to be interpreted as follows:

  • (none): If no modifiers are present, the location is interpreted as a prefix match. This means that the location given will be matched against the beginning of the request URI to determine a match.
  • =: If an equal sign is used, this block will be considered a match if the request URI exactly matches the location given.
  • ~: If a tilde modifier is present, this location will be interpreted as a case-sensitive regular expression match.
  • ~\*: If a tilde and asterisk modifier is used, the location block will be interpreted as a case-insensitive regular expression match.
  • ^~: If a carat and tilde modifier is present, and if this block is selected as the best non-regular expression match, regular expression matching will not take place.

Chooses Location

Nginx evaluates the possible location contexts by comparing the request URI to each of the locations. It does this using the following algorithm:

  • Nginx begins by checking all prefix-based location matches (all location types not involving a regular expression). It checks each location against the complete request URI.
  • First, Nginx looks for an exact match. If a location block using the = modifier is found to match the request URI exactly, this location block is immediately selected to serve the request.
  • If no exact (with the = modifier) location block matches are found, Nginx then moves on to evaluating non-exact prefixes. It discovers the longest matching prefix location for the given request URI, which it then evaluates as follows:
    • If the longest matching prefix location has the ^~ modifier, then Nginx will immediately end its search and select this location to serve the request.
    • If the longest matching prefix location does not use the ^~ modifier, the match is stored by Nginx for the moment so that the focus of the search can be shifted.
  • After the longest matching prefix location is determined and stored, Nginx moves on to evaluating the regular expression locations (both case sensitive and insensitive). If there are any regular expression locations within the longest matching prefix location, Nginx will move those to the top of its list of regex locations to check. Nginx then tries to match against the regular expression locations sequentially. The first regular expression location that matches the request URI is immediately selected to serve the request.
  • If no regular expression locations are found that match the request URI, the previously stored prefix location is selected to serve the request.

It is important to understand that, by default, Nginx will serve regular expression matches in preference to prefix matches. However, it evaluates prefix locations first, allowing for the administer to override this tendency by specifying locations using the = and ^~ modifiers.

It is also important to note that, while prefix locations generally select based on the longest, most specific match, regular expression evaluation is stopped when the first matching location is found. This means that positioning within the configuration has vast implications for regular expression locations.

Finally, it it is important to understand that regular expression matches within the longest prefix match will “jump the line” when Nginx evaluates regex locations. These will be evaluated, in order, before any of the other regular expression matches are considered.

Jump to Other Locations

Generally speaking, when a location block is selected to serve a request, the request is handled entirely within that context from that point onward. Only the selected location and the inherited directives determine how the request is processed, without interference from sibling location blocks.

Although this is a general rule that will allow you to design your location blocks in a predictable way, it is important to realize that there are times when a new location search is triggered by certain directives within the selected location. The exceptions to the “only one location block” rule may have implications on how the request is actually served and may not align with the expectations you had when designing your location blocks.

Some directives that can lead to this type of internal redirect are:

  • index
  • try_files
  • rewrite
  • error_page

Let’s go over these briefly.

The index directive always leads to an internal redirect if it is used to handle the request. Exact location matches are often used to speed up the selection process by immediately ending the execution of the algorithm. However, if you make an exact location match that is a directory, there is a good chance that the request will be redirected to a different location for actual processing.

The try_files directive tells Nginx to check for the existence of a named set of files or directories. The last parameter can be a URI that Nginx will make an internal redirect to.

The rewrite directive can lead to a location block pass off. When using the last parameter with the rewrite directive, or when using no parameter at all, Nginx will search for a new matching location based on the results of the rewrite.

The error_page directive can lead to an internal redirect similar to that created by try_files. This directive is used to define what should happen when certain status codes are encountered. This will likely never be executed if try_files is set, since that directive handles the entire life cycle of a request.

Scenario

Node Front-end

server
{
server_name domain.com www.domain.com;

location /
{
include proxy_params;
proxy_pass http://127.0.0.1:16666/;
proxy_set_header Host $host;
proxy_redirect off;
}

listen 443 ssl http2;
listen [::]:443 ssl http2;
ssl_certificate /etc/letsencrypt/live/domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/domain.com/privkey.pem;

# optimization
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/domain.com/fullchain.pem;
}

server
{
listen 80 default_server;
listen [::]:80 default_server;

server_name domain.com www.domain.com;

location ~
{
return 301 https://$host$request_uri;
}
}

ACME

Ubuntu 24.04, Ubuntu 22.04

# For test
mkdir -p /var/www/letsencrypt/.well-known/acme-challenge/
touch /var/www/letsencrypt/.well-known/acme-challenge/ping.txt
echo 'OK' >> /var/www/letsencrypt/.well-known/acme-challenge/ping.txt

touch /etc/nginx/snippets/letsencrypt-acme-challenge.conf

letsencrypt-acme-challenge.conf

location ^~ /.well-known/acme-challenge/ {
# content header
default_type "text/plain";

# This directory must be the same as in /etc/letsencrypt/cli.ini
# as "webroot-path" parameter. Also don't forget to set "authenticator" parameter
# there to "webroot".
# Do NOT use alias, use root! Target directory is located here:
# /var/www/common/letsencrypt/.well-known/acme-challenge/
root /var/www/letsencrypt;
}

# Hide /acme-challenge subdirectory and return 404 on all requests.
# It is somewhat more secure than letting Nginx return 403.
# Ending slash is important!
location = /.well-known/acme-challenge/ {
return 404;
}

/etc/nginx/sites-available/acme.nginx

server
{
listen [::]:443 ssl;
listen 443 ssl;

include /etc/nginx/snippets/letsencrypt-acme-challenge.conf;
}

server
{
listen 80;
listen [::]:80;

include /etc/nginx/snippets/letsencrypt-acme-challenge.conf;
}

Static

location /static
{
alias /root/app/static/;
autoindex on;
}

Nextjs

For 2 depth

next.config.js

const nextConfig = {
basePath: '/new/path'
}

nginx config

location /new/path
{
include proxy_params;
proxy_pass http://127.0.0.1:3000;
#rewrite ^/new/path(.*)$ $1 break;
#proxy_pass http://127.0.0.1:3000/;
proxy_set_header Host $host;
}

CORS

location /doc/bitdove
{
alias /root/service/app/bitdove-doc/public/;
index index.html;
add_header Access-Control-Allow-Origin $http_origin;
}

Mixed Content XHR

server
{
location /
{
proxy_pass http://127.0.0.1:7001/;

#include proxy_params;
proxy_set_header Host $http_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;

proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
}
}

WebSocket

/etc/nginx/sites-available/xxx

proxy_set_header Connection "upgrade";

nginx.conf

map $http_upgrade $connection_upgrade
{
default Upgrade;
'' close;
}

Connect to Tomcat

upstream tomcat {
server 127.0.0.1:8080 fail_timeout=0;
}

server {
...

#static resource
location ^~ /alone-static {
root /digitslate;
}

location / {
#try_files $uri $uri/ =404;
include proxy_params;
proxy_pass http://tomcat/;
}

...
}

Trouble Shooting

The content must be served over HTTPS

Mixed Content: The page at 'https://<URL>/' was loaded over HTTPS, but requested an insecure script 'http://<URL>/'. This request has been blocked; the content must be served over HTTPS.

Add add_header 'Content-Security-Policy' 'upgrade-insecure-requests'; to server section.

Location rules

Syntax: location [=||*|^~] /uri/ { ... }

= is matched first, then ^~, then the regular-expression matches in the order they appear in the file, and finally the plain / catch-all match. When a match succeeds, matching stops and the request is handled by the current matching rule.

Symbol — meaning

= — a leading = means an exact match.

^~ — a leading ^~ means the uri starts with a given ordinary string; think of it as matching the url path. nginx does not decode the url, so a request for /static/20%/aa is matched by the rule ^~ /static/ /aa (note the space).

~ — a leading ~ means a case-sensitive regular-expression match.

~* — a leading ~* means a case-insensitive regular-expression match.

!~ and !~*!~ and !~* are respectively the case-sensitive "does not match" and case-insensitive "does not match" regular expressions.

/ — the proxy the user is using (usually a browser).

$http_x_forwarded_for — can record the client IP; records the client's ip address through the proxy server.

$http_referer — can record which link the user arrived from.

Location Example

location =/ads.txt {
try_files /ads.txt =404;
}

# if must be placed inside location, otherwise its priority may override location.

Website load balancing plus high availability

Just add SSL on the entry (front) machine.

http {
...
upstream webserver {
server 172.16.0.11:80;
server 172.16.0.12:80;
}
...

server {
...
listen 80;

location / {
proxy_pass http://webserver;
}
...

Reverse Proxy

WebSocket

WebSocket works over HTTP ports 80 and 443 and marks the protocol with the ws:// or wss:// prefix. When establishing a connection it uses the HTTP/1.1 status code 101 to switch protocols. The current standard does not support establishing a WebSocket connection directly between two clients without going through HTTP.

During development, if you need WebSocket persistent connections and push technology, and you must use wss and access it through a domain name, you need an nginx reverse proxy.

How it works

The WebSocket service program we develop usually uses the ws protocol, in plaintext. But how do you make it transmit securely over the internet? At this point you can use nginx to forward directly between the client and the server: the client connects over wss, and nginx and the server communicate over the ws protocol. For example:

Client HTTPS <- WSS -> NGINX WebSocket Proxy <- WS -> Application Server

Configuration

The prerequisite is that you have a domain name and have already obtained a certificate for it.

Create a new nginx configuration file /etc/nginx/conf.d/websocket.conf with the following contents:

map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}

upstream websocket {
server localhost:8282; # appserver_ip:ws_port
}

server {
server_name domain.com;
listen 443 ssl;
location / {
proxy_pass http://websocket;
proxy_read_timeout 300s;
proxy_send_timeout 300s;

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_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
ssl_certificate /etc/letsencrypt/live/domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/domain.com/privkey.pem;
}

Let me explain the key parts of the configuration:

The most important thing is adding the following two lines to the reverse-proxy configuration; everything else is no different from an ordinary HTTP reverse proxy.

proxy_set_header Upgrade $http_upgrade ;
proxy_set_header Connection $connection_upgrade ;

The key part here is adding headers to the HTTP request:

Upgrade: websocket
Connection: Upgrade

These two fields tell the server to upgrade the protocol to WebSocket. After the server finishes processing the request, it returns the following message:

# Status code is 101
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: upgrade

This tells the client that the protocol has been successfully switched and upgraded to WebSocket. After the handshake succeeds, the server and client become peers, and like an ordinary Socket they can communicate bidirectionally. There is no more HTTP interaction; instead the WebSocket data-frame protocol begins and handles the data exchange.

Here the map directive combines variables into a new variable, deciding whether to pass a Connection header to the origin based on whether the connection from the client carries an Upgrade header. This approach is more elegant than simply passing upgrade unconditionally.

By default, a connection is closed after 60 seconds with no data transfer; the proxy_read_timeout parameter can extend this. The origin sends ping frames periodically to keep the connection alive and to confirm whether it is still in use.

The two timeout parameters

proxy_read_timeout

Syntax: proxy_read_timeout time. Default: 60s. Context: http, server, location. Description: this directive sets the read timeout for the connection to the proxied server. It determines how long nginx waits to get a response to a request. This is not the time to receive the entire response, but the time between two read operations.

proxy_send_timeout

Syntax: proxy_send_timeout time. Default: 60s. Context: http, server, location. Description: this directive sets the timeout for sending a request to the upstream server. The timeout is not for the whole sending period, but for the interval between two write operations. If the timeout is reached and the upstream has not received new data, nginx closes the connection.

Multiple proxy hops

I once ran into a situation at work where a certain domain could not be accessed over a mobile network. In that case I needed to forward through a front-end proxy server, which involves two proxy hops.

For example, the websocket service URL being accessed is:

wss: //domain.com

This one is on a cloud host with a public IP and is reachable from all networks. The other domain, inner.domain.com, resolves to the internal network and is deployed at the gateway center; this domain is reachable from the public cloud.

On the public cloud host:

map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}

server {
server_name domain.com;
location / {
proxy_pass http://inner.domain.com;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
#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_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/domain.com/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/domain.com/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}

The only thing to note above is that the proxy_set_header Host $host; line has been commented out.

And on the gateway-center host:

map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}

upstream websocket {
server localhost:8282; # appserver_ip:ws_port
}

server {
listen 80;
server_name board.xncoding.com;
location / {
proxy_pass http://websocket;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
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_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}

Only the outermost layer needs to use the wss protocol; all the internal interactions use the ws protocol, so listening on port 80 is enough.

Security

SSL

To configure SSL in Nginx and redirect HTTP to HTTPS, you need to modify the Nginx.conf configuration file:

# The original port 80 does a 301 redirect
server {
listen 80;
server_name sitea.com www.sitea.com;
return 301 https://www.siteb.com$request_uri; # redirect to HTTPS
}

# Configure the ssl certificate and enable ssl
server {
listen 443;
server_name www.sitea.com;
root wwwroot;
index index.html index.htm;
ssl on;
ssl_certificate /usr/ssl/ca.pem; # certificate path
ssl_certificate_key /usr/ssl/ca.key;

ssl_session_timeout 5m;

ssl_protocols SSLv2 SSLv3 TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP;
ssl_prefer_server_ciphers on;
error_page 497 "https://$host$uri?$args"; # this redirects HTTP requests to HTTPS

location / {
...
}
}

In other words, add another virtual server: one on port 80 and one on port 443.

But some programs only forward to the port and do not automatically correct http to https; there are quite a few such programs, for example phpmyadmin:

When you run into such a program, you need to modify the Nginx.conf configuration file and add a statement to the fastcgi section of the port-443 server:

fastcgi_param HTTPS on; #attention!# For example:

location ~ .*\.(php|php5)?$
{
try_files $uri =404;
fastcgi_pass unix:/tmp/php-cgi.sock;
fastcgi_index index.php;
fastcgi_param HTTPS on; #attention!#
include fcgi.conf;
}

Notes:

Redirecting http to https uses nginx's redirect commands. So how should the redirect be written? Older versions of nginx might have used a format similar to the following.

rewrite ^/(.*)$ http://domain.com/$1 permanent; or

rewrite ^ http://domain.com$request_uri? permanent; Newer versions of nginx have changed the way this is written, and the above is no longer recommended. There are probably still many articles online that use the first form.

The newer, more recommended way is:

return 301 http://domain.com$request_uri;

Limiting concurrency and bandwidth usage

Apply some routine settings on the Nginx server to limit its number of concurrent connections, session space, and so on.

nginx limiting the per-ip concurrency means limiting the number of simultaneous connections a single ip can make to the server.

# limit_conn_zone only for http
http {
...
# Define a limit_zone named lczten, 10M of memory to store sessions,
# keyed by $binary_remote_addr
# Since nginx 1.18, limit_conn_zone replaces limit_conn
limit_conn_zone $binary_remote_addr zone=lczten:10m;

# limit_conn for http, server, location
server{
...
location {
...
limit_conn lczten 10; # connection count limit
# bandwidth limit, applied per single connection; if one ip has two connections, that is 50x2k
limit_rate 50k;
...
}
...
}

Nginx limiting the IP request rate

Apply some routine settings on the Nginx server to limit the number of accesses from the same ip within a certain period of time.

nginx limiting the per-ip request rate means limiting the number of times a single ip connects to the server within a period of time.

With this setting you can, to some extent, prevent attacks that make rapid, high-frequency requests, such as CC attacks.

  • $binary_remote_addr refers to the client's IP; it means I want to rate-limit based on the client's IP.
  • zone=limit:10m means my limiter (zone) is called limit and can use at most 10MB of memory to store the request count for each IP (10MB can store over a hundred thousand IPs, which is certainly enough).
  • rate=100r/m is fairly intuitive: one hundred requests per minute. But note that nginx converts this into how many milliseconds it will accept a new request in — for example, one hundred per minute is converted to one every 600 milliseconds, so if you send ten requests at the same time within 600 milliseconds, only the first is accepted and the other nine are rejected outright.
# limit_conn_zone only for http
http {
...
# Define a limit_req_zone named lrzten to store sessions, 10M of memory,
# keyed by $binary_remote_addr, limiting the average requests per second to 5,
# 1M can store 16000 states, and the rate value must be an integer,
# if you want to limit to one request every two seconds, set it to 30r/m
# Wordpress rate=100r/s
limit_req_zone $binary_remote_addr zone=lrzten:10m rate=100r/s;

# limit_req for http, server, location
server {
...
location {
...
# Limit each ip to no more than 20 requests per second, with a leaky-bucket burst of 5.
# burst means: if seconds 1, 2, 3, and 4 each have 19 requests,
# then 25 requests in second 5 are allowed.
# But if you make 25 requests in second 1, the requests over 20 in second 2 return a 503 error.
# nodelay: if this option is not set, the request count is strictly limited to the average rate,
# so with 25 requests in second 1, 5 of them are deferred to second 2;
# with nodelay set, all 25 requests are executed in second 1.
limit_req zone=lrzten burst=5 nodelay;

Other

Nginx static-resource caching settings

When developing and debugging web pages, you often run into the annoyance of having to clear the browser cache or force a refresh to test, because of browser caching. Here is an nginx no-cache configuration. Among the common cache settings there are two approaches, both set with add_header: Cache-Control and Pragma.

location ~ .*\.(css|js|swf|php|htm|html )$ {
add_header Cache-Control no-store;
add_header Pragma no-cache;
}

For static content on a site that is not modified often (such as images, JS, CSS), you can set an expires time on the server to control browser caching, effectively reducing bandwidth traffic and lowering server load. Using an Nginx server as an example:

location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$ {
# Expiry time of 30 days.
# Image files do not update very often, so the expiry can be set larger;
# if they update frequently, it can be set smaller.
expires 30d;
}

location ~ .*\.(js|css)$ {
expires 10d;
}

Background: Expires is a Web server response-header field. When responding to an http request, it tells the browser that, before the expiry time, the browser can take the data directly from the browser cache without requesting it again.

A copy-paste-oriented reference for common Nginx setups. All domains are normalized to example.com-style placeholders — replace before use.

Contents


Location Matching Rules

Syntax: location [ = | ~ | ~* | ^~ ] /uri/ { ... }

Priority: = (exact) is checked first, then ^~, then regex rules (~, ~*) in the order they appear in the file, and finally the generic / prefix match. Matching stops at the first successful rule, which then handles the request.

ModifierMeaning
=Exact match
^~Prefix match on a literal string; when it matches, regex rules are skipped. Nginx does not URL-decode here, so a request for /static/20%/aa can be matched by ^~ /static/ /aa (note the space)
~Case-sensitive regex match
~*Case-insensitive regex match
!~ / !~*Case-sensitive / case-insensitive regex non-match
/Generic prefix match — matches any request (lowest priority)

Useful variables (e.g. in log_format or proxy headers):

VariableMeaning
$http_user_agentClient user agent (usually the browser)
$http_x_forwarded_forRecords the real client IP when requests pass through a proxy
$http_refererThe link the user came from

Example: exact match

location = /ads.txt {
try_files /ads.txt =404;
}

Put if inside location — otherwise it may take precedence over the location blocks.


Simple File Server (Basic Auth + Autoindex)

Password-protected directory listing with an IP allowlist.

Setup

NGINX_CONFIG=simple-file-server
USER_NAME=user001

# Check which user nginx runs as
ps -o user,group,comm -C nginx

# Create a shared group for web files
sudo groupadd webfiles
sudo usermod -aG webfiles ubuntu
sudo usermod -aG webfiles www-data

# Create the docroot; inspect permissions along the path
sudo mkdir /var/www/sfs
sudo namei -l /var/www/html
sudo namei -l /var/www/sfs

# Ownership + permissions (pick one chown)
sudo chown -R root:webfiles /var/www/sfs
# or: sudo chown -R $USER:$USER /var/www/sfs
sudo chmod -R 750 /var/www/sfs

# Create basic-auth credentials (second command prompts for the password)
sudo sh -c "echo -n '$USER_NAME:' >> /etc/nginx/.htpasswd"
sudo sh -c "openssl passwd -apr1 >> /etc/nginx/.htpasswd"

Config

/etc/nginx/sites-available/simple-file-server:

server {
listen 80 default_server;
listen [::]:80 default_server;

root /var/www/sfs;
server_name files.example.com;

location / {
auth_basic "You need to login";
auth_basic_user_file /etc/nginx/.htpasswd;
autoindex on;
autoindex_exact_size off;
autoindex_localtime on;
allow 122.0.0.0/24;
deny all;
}
}

Enable and test

sudo ln -s /etc/nginx/sites-available/$NGINX_CONFIG /etc/nginx/sites-enabled/
sudo ls /etc/nginx/sites-enabled/

sudo nginx -t
sudo systemctl restart nginx

# Download a file with basic auth
curl -v -u USERNAME:PASSWORD -O http://files.example.com/file.txt

HTTPS with Certbot

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com --email [email protected]

Load Balancing

For load balancing + high availability, only the entry node needs SSL.

http {
upstream webserver {
server 172.16.0.11:80;
server 172.16.0.12:80;
}

server {
listen 80;

location / {
proxy_pass http://webserver;
}
}
}

WebSocket Reverse Proxy

How it works

WebSocket runs on ports 80/443 with the ws:// / wss:// scheme and upgrades from HTTP/1.1 via a 101 Switching Protocols handshake. App servers usually speak plain ws; to expose them securely over the internet, terminate TLS at Nginx:

Client <- WSS -> Nginx (proxy) <- WS -> Application Server

The client requests a protocol upgrade with:

Upgrade: websocket
Connection: Upgrade

The server replies:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: upgrade

After the handshake, both sides are peers and exchange WebSocket data frames directly — no further HTTP.

Config (single hop)

Prerequisite: a domain with a certificate. /etc/nginx/conf.d/websocket.conf:

map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}

upstream websocket {
server localhost:8282; # appserver_ip:ws_port
}

server {
listen 443 ssl;
server_name example.com;

location / {
proxy_pass http://websocket;
proxy_read_timeout 300s;
proxy_send_timeout 300s;

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_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}

ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
}

Notes:

  • The two Upgrade / Connection headers are the only difference from a normal HTTP reverse proxy.
  • The map block forwards Connection: upgrade only when the client actually sent an Upgrade header — more elegant than forwarding it unconditionally.
  • Idle connections close after 60s by default; raise proxy_read_timeout and/or have the upstream send periodic ping frames to keep connections alive.

Timeout parameters

DirectiveDefaultContextMeaning
proxy_read_timeout60shttp, server, locationRead timeout toward the upstream. Not the time for the whole response — the max interval between two successive read operations
proxy_send_timeout60shttp, server, locationWrite timeout toward the upstream. Measured between two successive write operations; if the upstream receives nothing new within it, Nginx closes the connection

Two-tier (double) proxy

Use case: a domain unreachable from some networks (e.g. mobile). A public cloud host forwards to an internal domain (inner.example.com) that resolves only inside the network. Only the outermost hop uses wss; everything behind it is plain ws on port 80.

Public host (TLS termination):

map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}

server {
listen 443 ssl;
server_name example.com;

location / {
proxy_pass http://inner.example.com;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
# NOTE: Host header intentionally NOT overridden on this hop
# 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_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}

ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}

Internal / gateway host:

map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}

upstream websocket {
server localhost:8282; # appserver_ip:ws_port
}

server {
listen 80;
server_name inner.example.com;

location / {
proxy_pass http://websocket;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
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_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}

SSL / HTTPS

Redirect HTTP to HTTPS

# 301 redirect on port 80
server {
listen 80;
server_name example.com www.example.com;
return 301 https://www.example.com$request_uri;
}

return 301 ...$request_uri; is the recommended modern form. The older style rewrite ^/(.*)$ https://example.com/$1 permanent; still appears in many articles but is no longer recommended.

HTTPS server block

server {
listen 443 ssl; # 'ssl on;' is deprecated -- use 'listen ... ssl'
server_name www.example.com;
root wwwroot;
index index.html index.htm;

ssl_certificate /usr/ssl/ca.pem;
ssl_certificate_key /usr/ssl/ca.key;

ssl_session_timeout 5m;
ssl_protocols TLSv1.2 TLSv1.3; # SSLv2/SSLv3/TLSv1.0/1.1 are insecure -- do not enable
ssl_prefer_server_ciphers on;
# ssl_ciphers: modern nginx defaults are reasonable; use the Mozilla
# SSL Configuration Generator if you need a tuned cipher list.

# Redirect plain-HTTP requests that hit the HTTPS port (nginx code 497)
error_page 497 https://$host$uri?$args;

location / {
# ...
}
}

PHP / FastCGI behind HTTPS

Some apps (e.g. phpMyAdmin) only see the forwarded port and keep generating http:// URLs. Fix it by telling FastCGI the connection is HTTPS:

location ~ .*\.(php|php5)?$ {
try_files $uri =404;
fastcgi_pass unix:/tmp/php-cgi.sock;
fastcgi_index index.php;
fastcgi_param HTTPS on; # <-- the fix
include fcgi.conf;
}

Rate Limiting

Concurrent connections + bandwidth (limit_conn)

Limit how many simultaneous connections a single IP may hold, and cap per-connection bandwidth.

http {
# Shared-memory zone 'lczten' (10 MB) keyed by client IP.
# limit_conn_zone replaced the older limit_zone directive.
# limit_conn_zone is valid in http context only.
limit_conn_zone $binary_remote_addr zone=lczten:10m;

server {
location / {
# limit_conn is valid in http, server, location
limit_conn lczten 10; # max 10 concurrent connections per IP
limit_rate 50k; # per-CONNECTION cap: an IP with 2 connections gets 2 x 50k
}
}
}

Request frequency (limit_req)

Limit how many requests a single IP can make over time — helps mitigate CC-style flood attacks.

How it works:

  • $binary_remote_addr — key by client IP (per-IP limiting).
  • zone=lrzten:10m — zone named lrzten using up to 10 MB of memory; 1 MB stores ~16,000 IP states, so 10 MB easily covers 100k+ IPs.
  • rate=100r/s — the rate value must be an integer; for 1 request per 2 seconds write 30r/m. Nginx converts the rate into a per-request interval: e.g. 100r/m becomes one request per 600 ms, so 10 requests arriving within 600 ms means only the first is accepted (plus whatever burst allows) and the rest are rejected.
http {
# limit_req_zone is valid in http context only.
# WordPress reference value: rate=100r/s
limit_req_zone $binary_remote_addr zone=lrzten:10m rate=100r/s;

server {
location / {
# Example with rate=20r/s, burst=5:
# - If seconds 1-4 each had 19 requests, 25 requests in second 5 are allowed.
# - But 25 requests in second 1 -> the excess returns 503.
# nodelay: without it, excess requests within burst are queued and
# served at the average rate (e.g. 5 of 25 pushed to the next second);
# with nodelay, all 25 are served immediately in second 1.
limit_req zone=lrzten burst=5 nodelay;
}
}
}

Static Asset Caching

Disable caching (development)

Avoids constantly clearing the cache / hard-refreshing while debugging:

location ~ .*\.(css|js|swf|php|htm|html)$ {
add_header Cache-Control no-store;
add_header Pragma no-cache;
}

Browser caching with expires (production)

For rarely-changing static content (images, JS, CSS), set an expiry so browsers serve from local cache without re-requesting — reduces bandwidth and server load. Expires is a response header telling the browser it may use cached data until the expiry time passes.

# Images rarely change -> long expiry; shorten it if they update often
location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$ {
expires 30d;
}

location ~ .*\.(js|css)$ {
expires 10d;
}