Nginx — рецепты

Решил собирать на этой странице всяческие полезные настройки, с которыми пришлось повозиться.

Но, прежде чем приступить, хочу Вам предложить освежить знания о том, как nginx обрабатывает запрос:

Синтаксис:

location [ = | ~ | ~* | ^~ ] uri { ... }
location { } @ name { ... }
=полное строковые совпадение, например = / исключительно для корня, и даже файлы в корне уже сюда не подходят (приоритет максимальный)
^~требуется совпадение только начала строки, например ^~ /img/ считается не регулярным выражением (приоритет высокий)
~*регулярное выражение без учета регистра (приоритет средний)
~регулярное выражение с учетом регистра (приоритет ниже среднего)
не указаннапример "location / {" или "location /data/ {" (приоритет минимальный), при этом если у вас написано сразу два данных правила, то нет уверенности в том, что отработает вначале (/ или /data/)

Примеры специально расположенные в обратном порядке приоритетов:

# Данная конфигурация сопоставляется с любым запросом, в котором есть /data/ например /foo/data/bar
location /data/ {
    return 200 "data on $uri";
}
# Совпадает с любым запросом, начинающимся с /, однако после всех регулярных выражений.
location / {
    return 200 "root is $document_root on $uri";
}
# Соответствует запросам /api и /api/ но если файл doc.txt не найден, то nginx сделает
# внутренний запрос словно клиент запросил /info.gif (см. ниже регулярку с gif)
location ~ ^/api(|/)$ {
    try_files /doc.txt /info.gif;
}
# Совпадает с любым запросом, начинающимся с  /img/ и затем поиск прекращается,
# в этом примере нет регулярных выражений.
location ^~ /img/ {
    try_files $uri =404;
}
# Совпадает с любым запросом заканчивающимся на png, ico, gif, jpgили jpeg. Однако, все
# запросы /img/ будут обработаны в предыдущем location
location ~* \.(png|ico|gif|jpg|jpeg)$ {
    try_files $uri =404;
}
# Если совпадает точно с запросом /
location = / {
    index README.md;
}
# Произойдет поиск файла /my.js и если его нет, заново начнется поиск, словно клиент запросил файл /my.php
location = /my.js {
    try_files $uri /my.php$is_args$args;
}

Как проверить существование файла в директории и если файла нет - выдать 404

location ^~ /dir/ {
     try_files $uri =404;
}

Как проверить существование файла в директории и если файла нет - показать файл по умолчанию

location ^~ /dir/ {
     try_files $uri /images/default.gif;
}

*выше описанные способы можно совместить:

location ^~ /dir/ {
     try_files $uri /images/default.gif =404;
}

Проксируем запросы на site.ru

location ^~ /img/ {
   resolver 8.8.8.8;
   proxy_pass http://site.ru$uri;
   
   # если site.ru ответит кодом ответа > 300, то что делать с таким ответом решит директива error_page
   proxy_intercept_errors on;
   # например: при 400 показать 200 https://nginx.org/ru/docs/http/ngx_http_core_module.html#error_page
   error_page 404 =200 /empty.gif;
}

или без dns-resolver:

location ^~ /img/ {
    proxy_pass http://1.9.107.167:80;
    proxy_redirect http://1.9.107.167:80/ /;
    proxy_set_header Host $host;
}

Проксируем запросы на site.ru

location ^~ /outer/office/users/ {
    proxy_pass http://site.ru:80;
    # подменяем заголовок 'Location' словно ответил текущий сайт (а не site.ru)
    proxy_redirect default;
}

Проксируем yapro.ru/backend/page -> http://127.0.0.1:80/page (обрезая начало URL /backend/):

location ^~ /backend/ {
    # если не указать, то в $_SERVER['HTTP_HOST'] будет содержаться 'yapro.backend:8888'
    proxy_set_header Host yapro.backend;
    # т.к. в конце указал слэш, то запрос на yapro.front/backend/page будет перенаправлен на yapro.backend/page
    proxy_pass http://127.0.0.1:80/;
}

Проксируем yapro.ru/backend/page -> http://yapro.backend/backend/page :

location ^~ /backend/ {
    proxy_set_header Host yapro.backend;
    proxy_pass http://127.0.0.1:80/backend/;
}

Проксируем yapro.ru/article/page -> http://yapro.backend/backend/article/page :

location ^~ /article/ {
    proxy_set_header Host yapro.backend;
    proxy_pass http://127.0.0.1:80/backend/article/;
}

Проксируем запрос на yapro.ru/v1/chat/completions -> https://api.openai.com/v1/chat/completions

    location = /v1/chat/completions {
        proxy_pass https://api.openai.com;
        proxy_pass_request_headers on;
        proxy_ssl_name api.openai.com;
        proxy_ssl_server_name on;
    }

Проверяем существование файла в директории, а и если файла нет - ищем на другом сайте, и если там нет - выдаем 404

location ^~ /dir/ {
    try_files $uri @static_svr1;
}
location @static_svr1 {
   resolver 8.8.8.8;
   proxy_pass http://site.ru$uri;
}

Как подгружать файлы определенной директории с директории другого проекта

location ^~ /dir/ {
     root /var/www/yandex.ru/;
}

* переопределять root параметр является единственным верным и рабочим способом и никакие try_file не помогут (и не нужны).

Как использовать динамичный роутинг с ограничением (например по расширению файла)

location ~* ^/mobile/(.+)\.(ettf|svg) {
    try_files /$1.$2 @rewrite;
}

Как вернуть код 500 с содержимым

location /500.html {
       return 500 "Whoa! Internal Server Error";
}

Как обработать GET-переменные

location = /my/page.html {
    if ($query_string = "id=15&category=8") {
        return 403;
    }
}

Как изменить количество секунд, которое будет ждать Nginx от PHP-fpm

location ^~ /my/page {
    fastcgi_read_timeout 55s;
    include php-fpm.conf;
}

Redirect c HTTP на HTTPS

server {
    listen 80;
    server_name www.yapro.ru yapro.ru;
    return 301 https://yapro.ru$request_uri;
}
server {
    listen 443 ssl http2;
    server_name yapro.ru;
    # . . . SSL files paths and other code
    return 301 https://yapro.ru$request_uri;
}
server {
    listen 443 ssl http2;
    server_name yapro.ru;
    # . . . SSL files paths and other code
}

При обрщении по www подгружать контент с сайта без www

server {
    server_name www.yapro.ru;
    rewrite ^(.*) http://yapro.ru$1 permanent;
}

Дебагинг

Чтобы убедиться, что поддержка отладки сконфигурирована, необходимо в консоле выполнить команду:

nginx -V | grep "--with-debug"

если результат есть, значит nginx скомпилен с возможностью отладки и теперь для error-лога можно просто указать примечание debug:

server {
    error_log /var/log/nginx/site.ru.error debug;
}

Anti-hotlinking – защита файлов вашего сайта от прямого доступа с других сайтов или сервисов. По этому вопросу у меня отдельная статья »

Не проверялось: Autoindex

Autoindex — это функция, которая включает листинг директорий по http, средствами веб-сервера (конечно, если в директории нет настоящего index-файла).

location /testing {
  autoindex on;
  autoindex_exact_size off;
  autoindex_localtime on;
}

Не проверялось: как проксировать на нужный сервер, используя переменные из Cookies

Ситуация: приложение кладет в куку JSESSIONID значение k3t2, где 3 это номер сервера, 2 это порт 8020. Следовательно необходимо заворачивать клиентов по этим данным.

if ( $cookie_JSESSIONID ~ k(\d)t(\d)$ ) {
     set $i $1;
     set $p $2;
     proxy_pass  "http://10.0.0.${i}:80${p}0";
}

*есть мнение, что в proxy_pass надо задавать url полностью. Т.е. нужно записать все в переменную и только потом proxy_pass http://$my_var

Не проверялось:  под-директории и фронт-контроллеры в них

location /nested {
    alias /var/www/nested/public;
    try_files $uri $uri/ @nested;

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_param SCRIPT_FILENAME $request_filename;
        fastcgi_pass unix:/var/run/php/php7.2-fpm.sock;
    }
}

location @nested {
    rewrite /nested/(.*)$ /nested/index.php?/$1 last;
}

Однако вот пример работы вложенного location-a (проверено - работает).

Не проверялось: перенаправляем udp в http

server {
    listen 80 udp;
    proxy_pass http://elasticsearch:9200;
}

или так

listen 80 udp;
location ^~ / {
    proxy_pass http://elasticsearch:9200;
}

или так

listen 80 udp;
location ^~ / {
#    proxy_bind $remote_addr transparent;
#    proxy_bind 1.0.0.0;
    proxy_pass http://2.0.0.0:2555;
}

PHP-fpm разные примеры в одном месте

    server {
        listen 80 default;
        root /var/www/public;

        error_log /var/log/nginx/error.log;
        access_log /var/log/nginx/access.log;

        location ~ ^/fpm_(status|ping)$ {
           fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
           fastcgi_index index.php;
           include fastcgi_params;
           fastcgi_pass localhost:9000;
        }

        location ^~ /my/api {
            fastcgi_pass localhost:9000;
            # GLOBAL-переменные, будут доступны в PHP https://www.nginx.com/resources/wiki/start/topics/examples/phpfcgi/
            fastcgi_param SCRIPT_FILENAME $document_root/index.php;
            include fastcgi_params;
        }

        location / {
            try_files $uri /index.php$is_args$args;
        }

        location ~ ^/index\.php(/|$) {
            fastcgi_pass localhost:9000;
            fastcgi_split_path_info ^(.+\.php)(/.*)$;
            include fastcgi_params;
            fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
            fastcgi_param DOCUMENT_ROOT $realpath_root;
        }

Декодирование

Nginx имеет возможность декодировать URI, в реальном времени. Например, для того, чтобы найти соответствия “/app/%20/images” вы можете использовать “/app/ /images” для определения местоположения.

Сколько было параллельных запросов

Для понимания ситуации, я использую nginx переменные:

  • $time_local - время на момент записи в лог
  • $request_time - время обработки запроса в секундах с точностью до миллисекунд; время, прошедшее с момента чтения первых байт от клиента до момента записи в лог после отправки последних байт клиенту
  • $msec - не использую, ведь это тоже самое, что и $time_local только в виде тамштампа

Таким образом: $time_local - $request_time = дата времени начала запроса.

nginx.conf:

http {
    log_format  my_format  '"$time_local - $request_time"';
    access_log  /var/log/nginx/access.log  my_format;
    server {
        access_log /var/log/nginx/access.log my_format;
    }
}

Отбрасываем запросы которые не сможем обслужить

Для этого в nginx есть (встроенный в бесплатную версию) модуль limit_req_zone, правда который показал себя странно при тестировании (или я его неправильно настроил). Я специально задрал значения в nginx-конфиге, сделал нагрузочный тест, но limit_req_zone отбросил тебе запросы, которые не должен был:

http {
    limit_req_zone $request_uri zone=by_uri:10m rate=2000r/s;
    server {
    location / {
        limit_req zone=by_uri burst=5 nodelay;

Планирую этот модуль подвергнуть дальнейшему исследованию.

CORS

Официальный CORS-конфиг у меня работает, только если во все три места добавить:

add_header "Access-Control-Allow-Origin" $http_origin;
add_header 'Access-Control-Allow-Credentials' 'true';

плюс указав нужные HTTP-методы, например PUT:

add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT';

Но как Вы заметили, в официальном конфиге используются if-ы, но if-ы это зло, и я решил в этом убедиться, написав: 

set $baseCorsHeaders 0;
set $additionalCorsHeaders 0;
if ($request_method = 'OPTIONS') {
    set $baseCorsHeaders 1;
}
if ($request_method = 'GET') {
    set $baseCorsHeaders 1;
    set $additionalCorsHeaders 1;
}
if ($request_method = 'POST') {
    set $baseCorsHeaders 1;
    set $additionalCorsHeaders 1;
}
if ($baseCorsHeaders = 1) {
    add_header 'Access-Control-Allow-Origin' $http_origin always;
    add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
    add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range' always;
    add_header 'Access-Control-Allow-Credentials' 'true';
}
# Wide-open CORS config for nginx - https://enable-cors.org/server_nginx.html
if ($request_method = 'OPTIONS') {
   # Tell client that this pre-flight info is valid for 20 days
   add_header 'Access-Control-Max-Age' 1728000;
   add_header 'Content-Type' 'text/plain; charset=utf-8';
   add_header 'Content-Length' 0;
   add_header 'Access-Control-Allow-Credentials' 'true';
   return 204;
}
if ($additionalCorsHeaders) {
   add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;
}

Как Вы наверное поняли, я убедился, что if-ы это зло (в примере выше отрабатывает только последний if).

CORS-вывод: nginx не очень удобный инструмент для написания конфигов с if-ами, к тому же если вы все же написали if, то конфиг может неожиданно переставать работать или работать неожиданно (что еще хуже).

Скрываем версию nginx

Для этого в файле nginx.conf добавляем параметр server_tokens off;

server_tokens      off;
server {
        listen       80;
       server_name  site.com;

Debian-путь: /etc/nginx/nginx.conf 
FreeBSD-путь: /usr/local/etc/nginx/nginx.conf

После этого нужно перезапустить nginx.

Надеюсь Вам пригодятся данные настройки, удачки!

Проксируем все запросы на api.openai.com (не проверено)

server {
    listen 80;
    server_name openai-api.xxx.link;
    error_log /var/log/nginx/openai-api/nginx-error.log;
    access_log /var/log/nginx/openai-api/nginx-access.log main;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name openai-api.xxx.link;
    error_log /var/log/nginx/openai-api/nginx-error-ssl.log;
    access_log /var/log/nginx/openai-api/nginx-access-ssl.log main;

    ssl_certificate /data/acme.sh/xxx.link/fullchain.cer;
    ssl_certificate_key /data/acme.sh/xxx.link/bi83.link.key;
    include /etc/nginx/ssl/options-ssl-nginx.conf;
    ssl_dhparam /etc/nginx/ssl/ssl-dhparam.pem;

    ignore_invalid_headers off;
    client_max_body_size 0;
    proxy_buffering off;

    location /v1/audio/speech {
            proxy_pass https://api.openai.com/v1/audio/speech;
            proxy_ssl_name api.openai.com;
            proxy_ssl_server_name on;
            # Forward all headers
            proxy_pass_request_headers on;
            # proxy_http_version 1.1;
    }
}

map - как работает

Следующий код:

map $arg_one $var_two {
    "one"    "two";
}

означает:

if ($arg_one = "one") {
    set $var_two "two";
}

Можно даже писать сразу несколько условий:

map $arg_one $var_two {
    "one"    "two";
    "three"  "four";
}

что будет означать:

if ($arg_one = "one") {
    set $var_two "two";
}

if ($arg_one = "three") {
    set $var_two "four";
}

А если переменная не совпала ни с одним значением, то можно указать значение по-умолчанию:

map $arg_one $var_two {
    "one"    "two";
    "three"  "four";
    default  "five";
} 

Детальнее »

Отличие в трафике

БалансировщикВеб-сервис
{
  "msec": "1714803204.924",
  "connection": "22189",
  "connection_requests": "1",
  "pid": "19",
  "request_id": "8ec90545ed9b77b1779acba2cec86406",
  "request_length": "157",
  "remote_addr": "97.165.102.48",
  "remote_user": "",
  "remote_port": "52390",
  "time_local": "04/May/2024:06:13:24 +0000",
  "time_iso8601": "2024-05-04T06:13:24+00:00",
  "request": "GET /mypage HTTP/1.1",
  "request_uri": "/mypage",
  "args": "",
  "status": "200",
  "body_bytes_sent": "18253",
  "bytes_sent": "18526",
  "http_referer": "",
  "http_user_agent": "curl/7.81.0",
  "http_x_forwarded_for": "",
  "http_host": "yapro.ru",
  "server_name": "yapro.ru",
  "request_time": "0.487",
  "upstream": "127.0.0.1:8000",
  "upstream_connect_time": "0.000",
  "upstream_header_time": "0.484",
  "upstream_response_time": "0.488",
  "upstream_response_length": "18225",
  "upstream_cache_status": "",
  "ssl_protocol": "TLSv1.2",
  "ssl_cipher": "ECDHE-ECDSA-AES256-GCM-SHA384",
  "scheme": "https",
  "request_method": "GET",
  "server_protocol": "HTTP/1.1",
  "pipe": ".",
  "gzip_ratio": ""
}
{
  "msec": "1714803204.923",
  "connection": "370",
  "connection_requests": "1",
  "pid": "9",
  "request_id": "66a326a5a041f5c29e9471b72dbf1ce3",
  "request_length": "301",
  "remote_addr": "177.17.0.1",
  "remote_user": "",
  "remote_port": "43076",
  "time_local": "04/May/2024:06:13:24 +0000",
  "time_iso8601": "2024-05-04T06:13:24+00:00",
  "request": "GET /mypage HTTP/1.0",
  "request_uri": "/mypage",
  "args": "",
  "status": "200",
  "body_bytes_sent": "18225",
  "bytes_sent": "18472",
  "http_referer": "",
  "http_user_agent": "curl/7.81.0",
  "http_x_forwarded_for": "",
  "http_host": "yapro.ru",
  "server_name": "web-server",
  "request_time": "0.485",
  "upstream": "127.0.0.1:9000",
  "upstream_connect_time": "0.000",
  "upstream_header_time": "0.484",
  "upstream_response_time": "0.485",
  "upstream_response_length": "18267",
  "upstream_cache_status": "",
  "ssl_protocol": "",
  "ssl_cipher": "",
  "scheme": "http",
  "request_method": "GET",
  "server_protocol": "HTTP/1.0",
  "pipe": ".",
  "gzip_ratio": "",
  "request_type": "phpfpmfile"
}

Делаю вывод

Нужно поправить:

  • request_id - должен быть как на балансировщике
  • remote_addr - должен быть как на балансировщике
  • server_protocol - должен быть как на балансировщике (наверное)

Что отличается и это правильно:

  • scheme - в балансировщике htpps, в веб-сервере http
  • request_type - в балансировищке такого поля нет

Источники: 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15


21.12.2014 08:20