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”
listendirectives 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.0as 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_namedirective 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_namethat matches the value in theHostheader 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_namethat 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_namethat 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_nameusing regular expressions (indicated by a~before the name). The firstserver_namewith 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.
- If the longest matching prefix location has the
- 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_addrrefers to the client's IP; it means I want to rate-limit based on the client's IP.zone=limit:10mmeans 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/mis 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
- Simple File Server (Basic Auth + Autoindex)
- Load Balancing
- WebSocket Reverse Proxy
- SSL / HTTPS
- Rate Limiting
- Static Asset Caching
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.
| Modifier | Meaning |
|---|---|
= | 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):
| Variable | Meaning |
|---|---|
$http_user_agent | Client user agent (usually the browser) |
$http_x_forwarded_for | Records the real client IP when requests pass through a proxy |
$http_referer | The link the user came from |
Example: exact match
location = /ads.txt {
try_files /ads.txt =404;
}
Put
ifinsidelocation— otherwise it may take precedence over thelocationblocks.
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/Connectionheaders are the only difference from a normal HTTP reverse proxy. - The
mapblock forwardsConnection: upgradeonly when the client actually sent anUpgradeheader — more elegant than forwarding it unconditionally. - Idle connections close after 60s by default; raise
proxy_read_timeoutand/or have the upstream send periodic ping frames to keep connections alive.
Timeout parameters
| Directive | Default | Context | Meaning |
|---|---|---|---|
proxy_read_timeout | 60s | http, server, location | Read timeout toward the upstream. Not the time for the whole response — the max interval between two successive read operations |
proxy_send_timeout | 60s | http, server, location | Write 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 stylerewrite ^/(.*)$ 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 namedlrztenusing 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 write30r/m. Nginx converts the rate into a per-request interval: e.g.100r/mbecomes one request per 600 ms, so 10 requests arriving within 600 ms means only the first is accepted (plus whateverburstallows) 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;
}