Nginx — конфиги под задачу

Приветствую! Здесь вы наверняка найдете, что ищете. Примеры в лаборатории рассчитаны на то, что мы разбираем что-то конкретное.

Текущая статья посвящена примерам: nginx.conf для статики, reverse proxy, React/Vue SPA, PHP, SSL и балансировки.

Поэтому за теорией по текущей теме вам — в энциклопедию. Если ещё не погружались, то маршрут прост:

  1. Основы
  2. Система и сеть
  3. Данные и разметка
  4. Код и разработка
  5. Языки
  6. Искусственный интеллект
  7. Проект
  8. Инфраструктура и безопасность
  9. Спин-офф

Обязательно пройдитесь.

А теперь приступим к нашему предмету.

Теория и соседние материалы

Полный справочник директив — Nginx.

Как веб-сервер вписан в цепочку «DNS → HTTP → приложение» — веб-серверы.

Nginx в Docker — готовые стеки Compose.

Проверка ответов — curl.


Разбор конфига по частям

Nginx читает конфиг сверху вниз. Директива заканчивается ;, блок — { }. Запрос попадает в один блок server (по порту и домену), затем nginx выбирает один location (по URL).

Блоки (контексты)

Блок Зачем нужен
events { } Сколько соединений держит один рабочий процесс
http { } Всё про HTTP — MIME, gzip, upstream, include сайтов
server { } Один «виртуальный хост» — домен + порт + корень сайта
location /path { } Правило для URL, начинающегося с /path
upstream имя { } Список бэкендов для балансировки и proxy

Частые директивы

Директива Смысл простыми словами
listen 80 Слушать порт 80 (обычный HTTP)
listen 443 ssl Слушать HTTPS
server_name example.com Этот блок срабатывает, если в запросе Host: example.com
root /var/www/site Папка на диске: URL /img/a.png → файл /var/www/site/img/a.png
index index.html Если URL — каталог, отдать этот файл
try_files $uri … Порядок «найти файл на диске или сделать fallback»
proxy_pass http://… Не искать файл — отправить запрос другому серверу
return 301 URL Сразу ответ «перейди по другому адресу»
include файл Подключить ещё один конфиг

Переменные — «подстановки» в конфиге

Переменная Откуда берётся Пример значения
$uri Путь запроса без query /api/users
$request_uri Путь и query /api/users?page=1
$host Заголовок Host shop.example.com
$remote_addr IP клиента 203.0.113.5
$scheme http или https https
$document_root Значение root /var/www/site

Два адреса для proxy_pass — самая частая путаница на лабораторных:

Ситуация Пишите в proxy_pass
Nginx и app на одной Linux-машине http://127.0.0.1:3000
Nginx в Docker, app — другой контейнер http://api:3000 (имя сервиса из compose)

127.0.0.1 внутри контейнера nginx — это сам контейнер, не ваш ПК и не соседний сервис.


Порядок действий

  1. Конфиги на Ubuntu/Debian — /etc/nginx/, главный nginx.conf, сайты в conf.d/*.conf или sites-enabled/.
  2. Сохраните фрагмент, например /etc/nginx/conf.d/my-site.conf.
  3. Проверка и перезагрузка:
sudo nginx -t
sudo systemctl reload nginx
  1. Смотрите ответ:
curl -I http://localhost
curl -I -H "Host: shop.local" http://127.0.0.1
  1. Логи: ошибки — /var/log/nginx/error.log, все запросы — access.log.
Права и секреты

Правка /etc/nginx/ — через sudo. Ключ TLS (ssl_certificate_key) не кладите в Git. Домены и пути в примерах замените на свои.


Обязательный каркас

Перед любым примером ниже — минимальный nginx.conf (или один файл в conf.d/, если в главном уже есть include /etc/nginx/conf.d/*.conf;):

events {
    worker_connections 1024;
}

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

    sendfile on;
    keepalive_timeout 65;

    # server { } из примеров — сюда или в conf.d/
}
Строка Что делает
events { worker_connections 1024; } Без блока events nginx не запустится
include mime.types Сопоставляет .csstext/css, .jsapplication/javascript
default_type application/octet-stream Если расширение неизвестно — отдать как «сырые байты»
sendfile on Ядро ОС отдаёт файл с диска быстрее, без лишнего копирования в память
keepalive_timeout 65 Держать HTTP-соединение открытым до 65 с (меньше рукопожатий TCP)

Что делает команда проверки:

sudo nginx -t
  1. Nginx читает все include и склеивает конфиг в памяти.
  2. Проверяет синтаксис (скобки, точки с запятой, директивы в правильном блоке).
  3. Печатает syntax is ok и test is successful — только после этого безопасен reload.

Стартовые конфиги

Пять сценариев, которые чаще всего ищут на первых курсах — от HTML на диске до прокси на backend.


1. Статический сайт — «Hello»

Задача: отдать готовый index.html с диска. Подходит для лабораторной «поднять веб-сервер», для простой визитки или папки dist/ после сборки фронта без client-side роутинга.

Когда искать этот конфиг: «nginx отдача статики», «nginx index.html», «nginx root try_files».

server {
    listen 80;
    server_name localhost;

    root /var/www/hello;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}
Строка Смысл
server { … } Один виртуальный хост — набор правил для домена
listen 80 Принимать HTTP на порту 80
server_name localhost Блок выбирается, если браузер шлёт Host: localhost
root /var/www/hello URL /logo.png → файл /var/www/hello/logo.png
index index.html Запрос / или /about/ → попробовать index.html в каталоге
location / Правило для любого пути (префикс /)
try_files $uri $uri/ =404 1) файл как есть 2) каталог + index 3) иначе ошибка 404

Что происходит по шагам (запрос GET /):

  1. Nginx выбирает этот server (порт 80, Host: localhost).
  2. Путь / попадает в location /.
  3. try_files: ищет файл / — нет; ищет каталог /var/www/hello/ — есть.
  4. Из-за index index.html отдаёт /var/www/hello/index.html со статусом 200.

Что происходит (запрос GET /missing.txt):

  1. Файла /var/www/hello/missing.txt нет, каталога /var/www/hello/missing.txt/ тоже нет.
  2. =404 — nginx отвечает 404 Not Found.
URL Файл на диске Ответ
/ index.html есть 200, HTML
/style.css style.css есть 200, CSS
/nope ничего нет 404

Подготовка и проверка:

sudo mkdir -p /var/www/hello
echo '<h1>Hello from Nginx</h1>' | sudo tee /var/www/hello/index.html
sudo nginx -t && sudo systemctl reload nginx
curl -i http://localhost/

Что делает код:

  1. mkdir -p — создаёт каталог для файлов сайта.
  2. tee — записывает HTML (от root, если нет прав у обычного пользователя).
  3. nginx -t — проверка конфига до reload.
  4. curl -i — показывает заголовки и тело; в теле должен быть ваш <h1>.

Частая ошибка: забыли index index.html — для / nginx вернёт 403 Forbidden (каталог есть, но «листинг» по умолчанию запрещён).


2. Редирект HTTP → HTTPS

Задача: весь трафик с порта 80 перенаправить на HTTPS. Браузер сам откроет https://…, пользователь не вводит протокол вручную.

Когда искать: «nginx redirect http to https», «nginx return 301 ssl».

server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}
Строка Смысл
server_name example.com www.example.com Оба домена — с www и без
return 301 https://… Ответ без тела: «навсегда переехали» (код 301)
$host Подставится домен из запроса (example.com или www.example.com)
$request_uri Путь и ?query=… сохраняются

Что происходит по шагам (GET http://example.com/old-page?q=1):

  1. Запрос приходит на порт 80.
  2. Nginx не ищет файлы — сразу return 301.
  3. В заголовке Location: https://example.com/old-page?q=1.
  4. Браузер повторяет запрос уже на порт 443 (нужен отдельный блок с SSL — пример №8).
Запрос Заголовок Location
http://example.com/ https://example.com/
http://www.example.com/blog/1 https://www.example.com/blog/1

Проверка:

curl -I http://example.com/old-page?q=1

В выводе ищите строку HTTP/1.1 301 и Location: https://….

Частая ошибка: есть редирект с 80, но нет server &#123; listen 443 ssl; … &#125; — браузер после редиректа получит «connection refused» или ошибку сертификата.


3. SPA — fallback на index.html

Задача: React, Vue, Angular, Vite после npm run build — при обновлении страницы по адресу /users/42 nginx отдаёт index.html, а маршрутизацию делает JavaScript в браузере.

Когда искать: «nginx react router», «nginx vue history mode», «try_files index.html spa».

Строка Смысл
root …/dist Сюда копируют результат npm run build (папка dist/ или build/)
try_files … /index.html Нет файла для пути — отдать один index.html (shell приложения)
`location ~* .(js css…)$`
~* Регистронезависимое совпадение (.PNG тоже)
expires 7d Браузер может кэшировать файл 7 дней
Cache-Control "public, immutable" Файлы с хешем в имени (app.a1b2c3.js) не перезапрашивать зря

Что происходит по шагам (GET /dashboard после F5):

  1. Nginx ищет файл /var/www/spa/dist/dashboard — нет.
  2. Ищет каталог dashboard/ — нет.
  3. Внутренний переход на /index.html — отдаёт SPA, статус 200 (это важно: SPA ожидает 200, а не 404).

Что происходит (GET /assets/index-a1b2.js):

  1. Совпадает второй location (расширение .js).
  2. Отдаётся реальный файл из dist/assets/ с заголовками кэша.
URL Есть файл в dist? Ответ
/ index.html 200, HTML
/dashboard только index.html 200, тот же HTML
/assets/app.js да 200, JS + Cache-Control

Проверка:

curl -I http://app.local/dashboard
curl -I http://app.local/assets/index.js

Первый запрос — 200 и Content-Type: text/html. Второй — 200 и длинный Cache-Control.

Частая ошибка: забыли /index.html в try_files — F5 на /profile даёт 404, хотя в приложении страница открывалась по клику.


4. Reverse proxy на Node.js / Python / Go

Задача: снаружи пользователь ходит на порт 80, nginx передаёт запрос приложению на 127.0.0.1:3000 (Express, FastAPI, Flask, Gin и т.д.).

Когда искать: «nginx reverse proxy nodejs», «nginx proxy_pass localhost 3000», «nginx fastapi».

Строка Смысл
upstream app_backend Именованная группа бэкендов (позже добавите второй server для балансировки)
keepalive 32 До 32 idle-соединений nginx → app (меньше накладных расходов TCP)
proxy_pass http://app_backend Запрос уходит на app, ответ nginx отдаёт клиенту
proxy_http_version 1.1 HTTP/1.1 нужен для keepalive к бэкенду
Host $host App видит реальный домен (api.local), а не 127.0.0.1
X-Real-IP IP клиента (для логов и rate limit в приложении)
X-Forwarded-For Цепочка IP через прокси
X-Forwarded-Proto http или https — app строит правильные ссылки
Connection "" Сброс Connection: close — иначе keepalive к upstream ломается

Что происходит по шагам (GET http://api.local/api/users):

  1. Nginx принимает запрос на :80.
  2. location / — проксирует весь путь на 127.0.0.1:3000/api/users.
  3. Node/Python отвечает JSON.
  4. Nginx возвращает тот же статус и тело клиенту.

Схема:

Браузер  →  :80 nginx  →  :3000 приложение
              ↑ proxy_set_header добавляет заголовки

Проверка (app уже слушает 3000):

# Терминал 1 — простой backend
python -m http.server 3000

# Терминал 2 — через nginx
curl -i http://api.local/

Что делает код:

  1. python -m http.server 3000 — временный HTTP-сервер для проверки (на лабораторной вместо него — ваш Express/FastAPI).
  2. curl -i — если nginx настроен, увидите ответ app через прокси (в access.log nginx — запись запроса).

API под префиксом /api/ — отдельный location и слэш в конце proxy_pass:

location /api/ {
    proxy_pass http://127.0.0.1:3000/;
    proxy_set_header Host $host;
}
Запрос клиента Что получит backend
/api/users /users
/api/health /health

Слэш после 3000/ отрезает префикс /api/ — это самый частый «тихий» баг на лабораторных (backend ждёт /api/users, а приходит /users или наоборот).

Частая ошибка: 502 Bad Gateway — app не запущен или слушает другой порт. Проверка: curl http://127.0.0.1:3000 минуя nginx.


5. PHP через PHP-FPM

Задача: .html и картинки — как файлы, .php — выполнить через PHP-FPM (Laravel, Symfony, WordPress, учебные скрипты).

Когда искать: «nginx php-fpm config», «nginx laravel public», «fastcgi_pass unix sock».

Строка Смысл
root …/public У Laravel/Symfony открыт только каталог public/ — там index.php
index index.php index.html Сначала PHP, потом HTML
try_files … /index.php?$query_string Front controller — все «красивые» URL идут в index.php
location ~ \.php$ Любой URL, заканчивающийся на .php
include fastcgi_params Стандартный набор параметров для PHP
fastcgi_pass unix:…sock Сокет процесса PHP-FPM (путь зависит от версии PHP и ОС)
SCRIPT_FILENAME Полный путь к скрипту на диске — обязательная строка
fastcgi_intercept_errors on Ошибки PHP можно обработать через error_page в nginx
location ~ /\. Запрет .env, .git, .htaccess по HTTP

Что происходит по шагам (GET /index.php):

  1. Совпадает location ~ \.php$.
  2. Nginx не отдаёт файл как текст — передаёт в FPM через сокет.
  3. FPM выполняет PHP, возвращает HTML.
  4. Nginx отдаёт HTML клиенту.

Что происходит (GET /users/list в Laravel):

  1. location /try_files не находит файл.
  2. Внутренний запрос /index.php? (query из $query_string).
  3. Laravel роутит /users/list внутри приложения.
URL Результат
/index.php Выполнение PHP
/style.css Статический файл
/.env 403 Forbidden (блок ~ /\.)
/index.php скачивается как файл Сломан location ~ \.php$ или FPM не запущен

Проверка:

sudo systemctl status php8.3-fpm
curl -i http://php.local/index.php
ls -la /run/php/php8.3-fpm.sock

Частая ошибка: root указывает на корень репозитория, а не на public/ — в браузере открывается структура проекта, .env может стать доступен без правильного location ~ /\..


Конфиги для типовых задач

Сценарии для VPS, нескольких сайтов, HTTPS, WebSocket и защиты.


6. Два сайта на одном IP

Задача: один сервер, один IP, разные домены — shop.local и blog.local с разными папками.

Когда искать: «nginx несколько сайтов», «nginx virtual host», «nginx server_name».

Строка Смысл
Два блока server Два «сайта» на одном порту 80
Разный server_name Nginx смотрит заголовок Host и выбирает блок
Разный root Файлы магазина и блога не смешиваются

Что происходит по шагам:

  1. Оба блока слушают :80.
  2. Запрос с Host: shop.local → первый server, файлы из /var/www/shop.
  3. Запрос с Host: blog.local → второй server.

Проверка без DNS/etc/hosts или просто curl):

curl -H "Host: shop.local" http://127.0.0.1/
curl -H "Host: blog.local" http://127.0.0.1/

Частая ошибка: оба домена показывают один сайт — в /etc/hosts на клиенте неверный IP или сработал default_server (первый listen 80 default_server ловит чужие Host).


7. Балансировка между двумя app-серверами

Задача: распределить нагрузку между app1:8080 и app2:8080, временно отключать «упавший» инстанс.

Строка Смысл
least_conn Запрос на сервер с меньшим числом активных соединений
max_fails=3 После 3 неудачных попыток сервер считается недоступным
fail_timeout=30s 30 с не слать на него трафик, потом проверить снова
proxy_next_upstream … При 502/503 повторить запрос на другом сервере из pool

Что происходит: nginx по очереди или по least_conn выбирает backend; если app1 вернул 502, клиент может получить ответ от app2 (если включён proxy_next_upstream).


8. HTTPS с Let's Encrypt (Certbot)

Задача: шифрование TLS после получения сертификата (certbot certonly или certbot --nginx).

Строка Смысл
listen 443 ssl http2 HTTPS + мультиплексирование HTTP/2
fullchain.pem Сертификат сайта + промежуточные CA
privkey.pem Закрытый ключ (доступ только root/nginx)
ssl_protocols TLSv1.2 TLSv1.3 Старые небезопасные версии SSL отключены

Сертификат Let's Encrypt живёт ~90 дней; certbot ставит timer для продления. После продления: sudo nginx -t && sudo systemctl reload nginx.

Проверка:

curl -I https://example.com
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | openssl x509 -noout -dates

9. WebSocket через reverse proxy

Задача: чат, live-лenta, Socket.io — соединение «апгрейдится» с HTTP до WebSocket.

Строка Смысл
map $http_upgrade … Если клиент шлёт Upgrade: websocketConnection: upgrade, иначе close
Upgrade / Connection Обязательные заголовки для WebSocket handshake
proxy_read_timeout 86400 24 ч — прокси не рвёт «висящее» WS-соединение по умолчанию (60 с)

Что происходит: обычный GET с Upgrade: websocket nginx проксирует на backend; дальше идёт двусторонний поток сообщений.


10. Rate limit — защита от флуда

Задача: ограничить число запросов с одного IP на /login (brute-force, DDoS на уровне приложения).

# Эту строку — один раз внутри http { }, не внутри server { }
limit_req_zone $binary_remote_addr zone=login_limit:10m rate=10r/s;

server {
    listen 80;
    server_name secure.local;

    location /login {
        limit_req zone=login_limit burst=20 nodelay;
        proxy_pass http://127.0.0.1:3000;
    }
}
Строка Смысл
limit_req_zone Создаёт «карту» лимитов в памяти (10 МБ под IP)
$binary_remote_addr Ключ — IP клиента в компактном виде
rate=10r/s В среднем 10 запросов в секунду с IP
burst=20 nodelay Разрешить «всплеск» до 20, без очереди
limit_req zone=… Применить лимит в конкретном location

При превышении — 503 (можно сменить на 429: limit_req_status 429; в location).

Частая ошибка: limit_req_zone внутри servernginx -t выдаст directive is not allowed here.


11. Gzip и заголовки безопасности

Задача: уменьшить размер HTML/JSON/CSS и добавить базовые заголовки защиты браузера.

Строка Смысл
gzip on Сжимать ответы, если клиент шлёт Accept-Encoding: gzip
gzip_min_length 256 Мелкие ответы не сжимать (накладные расходы)
X-Frame-Options SAMEORIGIN Запрет встраивать сайт в <iframe> на чужих доменах (clickjacking)
X-Content-Type-Options nosniff Браузер не «угадывает» MIME
add_header … always Заголовок даже при 404/500

Проверка gzip:

curl -H "Accept-Encoding: gzip" -I http://safe.local/

В ответе должно быть Content-Encoding: gzip (для достаточно большого HTML).


12. Nginx в Docker — свой conf через volume

Задача: свой default.conf без пересборки образа. Дополнение к стекам Compose.

nginx-proxy/
  compose.yaml
  conf/
    default.conf
  html/
    index.html

compose.yaml:

services:
  web:
    image: nginx:1.27-alpine
    ports:
      - "8080:80"
    volumes:
      - ./conf/default.conf:/etc/nginx/conf.d/default.conf:ro
      - ./html:/usr/share/nginx/html:ro
Строка Смысл
"8080:80" localhost:8080 на ПК → порт 80 в контейнере
./conf/default.conf:…:ro Ваш конфиг поверх дефолтного в образе
./html:…:ro Статика с хоста, read-only

conf/default.conf:

server {
    listen 80;
    server_name localhost;
    root /usr/share/nginx/html;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

Что делает код:

docker compose up -d
curl -I http://localhost:8080/
docker compose exec web nginx -t
  1. Поднимается контейнер с вашим conf и html.
  2. curl проверяет проброс порта.
  3. exec web nginx -t — синтаксис внутри контейнера (удобно при отладке mount).

Шпаргалка — слэш в proxy_pass

Самый частый вопрос на Stack Overflow и лабораторных:

location proxy_pass Запрос клиента URI на backend
/api/ http://127.0.0.1:3000/ /api/users /users
/api/ http://127.0.0.1:3000 /api/users /api/users
/ http://127.0.0.1:3000 /users /users

Правило: если в proxy_pass есть URI (хотя бы / в конце) — nginx заменяет совпавший префикс location на этот URI. Если URI в proxy_pass нет — на backend уходит полный путь клиента.


Шпаргалка — root и alias

Директива URL Путь на диске
root /var/www; + location /img/ /img/a.png /var/www/img/a.png
alias /var/static/; + location /img/ /img/a.png /var/static/a.png

alias отрезает префикс location из пути; для /static/ часто пишут именно alias, а не root.


Каркас проверки — скрипт для лабораторной

Сохраните и подставляйте свой домен:

#!/bin/bash
DOMAIN="${1:-localhost}"
URL="http://${DOMAIN}/"

echo "=== nginx -t ==="
sudo nginx -t || exit 1

echo "=== HTTP HEAD ==="
curl -sI "$URL" | head -n 15

echo "=== последние ошибки ==="
sudo tail -n 5 /var/log/nginx/error.log

Что делает код:

  1. $1 — домен из аргумента (./check.sh shop.local) или localhost.
  2. nginx -t — выход с ошибкой, если конфиг битый.
  3. curl -sI — только заголовки, первые 15 строк (статус, Content-Type, Location).
  4. tail error.log — последние причины 502/403/permission denied.

Шпаргалка команд

Задача Команда Пояснение
Проверка синтаксиса sudo nginx -t Обязательно перед reload
Перезагрузка sudo systemctl reload nginx Без обрыва текущих соединений
Статус systemctl status nginx active (running) / failed
Ошибки sudo tail -f /var/log/nginx/error.log 502, permission denied, unknown directive
Запросы sudo tail -f /var/log/nginx/access.log IP, код, URL, User-Agent
Весь конфиг sudo nginx -T 2>/dev/null | less С комментариями # configuration file
Виртуальный Host curl -H "Host: shop.local" http://127.0.0.1/ Тест vhost без DNS
Заголовки HTTPS curl -I https://example.com Сертификат, редиректы, HSTS

Типичные ошибки новичка

Ошибка Симптом Как исправить
Пропустили nginx -t reload failed, сайт «старый» или down всегда -t, читать stderr
Слэш в proxy_pass 404 на backend, «route not found» таблица выше, сверить с API
root вместо alias /static/static/app.js alias для префикса или поправить путь
PHP скачивается браузер предлагает сохранить .php location ~ \.php$, запущен php-fpm
502 Bad Gateway белая страница nginx curl напрямую на backend-порт
Permission denied 403, в error.log «permission denied» www-data должен читать root
Два default_server по IP открывается «чужой» сайт один listen 80 default_server
limit_req_zone в server nginx -t падает зона только в http { }
SPA без fallback F5 на /page → 404 try_files … /index.html
127.0.0.1 в Docker 502 из контейнера nginx имя сервиса compose

Практика — три мини-задания

  1. Статика. Пример №1: свой index.html, curl -i, в access.log должна появиться строка с кодом 200.
  2. Proxy. Python http.server 3000 + пример №4: сравните curl :3000 и curl через nginx — тело одинаковое, в proxy-варианте в access.log nginx есть запись.
  3. SPA. Положите dist/ Vite/React, пример №3: curl -I /random/path — 200 и text/html.

Подробный справочник директив — Nginx.


Чек-лист перед сдачей лабораторной

  • nginx -t — ok.
  • Для proxy есть Host, X-Real-IP, X-Forwarded-ForProto при HTTPS).
  • PHP — root на public/, .env закрыт.
  • Сертификаты на месте, ключ не в репозитории.
  • В отчёте — скрин или вывод curl -I, фрагмент конфига, что делает каждый блок.

Связанные материалы

Материал Зачем открыть
Справочник по Nginx все директивы, кэш, FastCGI
Веб-серверы HTTP, виртуальные хосты
Docker Compose — готовые стеки nginx + volumes + proxy
curl заголовки, POST, отладка API
SPA client-side routing
Шаблоны каркасы для других инструментов