Files
2026-10-11 22:24:54 +05:00

141 lines
8.2 KiB
Nginx Configuration File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Брауз-тормоз на флуд заказами: с одного IP не чаще 10 заказов в минуту
# (burst 10 сглаживает «двойной клик»). Работает ДО запроса в CRM.
limit_req_zone $binary_remote_addr zone=orders_zone:1m rate=10r/m;
# Общий тормоз на прочее /api/ (NF-04): флуд catalog/stoplist выедал пул БД
# CRM. Легитимный сайт опрашивает эти эндпоинты раз в минуту-десять,
# 120r/m с burst 60 — с большим запасом. Панель персонала ходит на CRM
# напрямую (127.0.0.1:47613) и этот лимит не замечает.
limit_req_zone $binary_remote_addr zone=api_zone:1m rate=120r/m;
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Реальный IP гостя для лимитеров CRM. Доверие к X-Forwarded-For —
# только 127.0.0.1 и docker-подсеть compose (NF-10): раньше верили всем
# RFC1918-пирам, и LAN-клиент при экспоузе сайта мог вращать per-IP
# лимитеры подделкой заголовка. Подсеть зафиксирована в
# docker-compose.yml (networks.default.ipam) — при смене её там
# обновите и здесь.
set_real_ip_from 127.0.0.1;
set_real_ip_from 172.18.0.0/16;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
# single-file сборка (~750 KB html) без gzip = лишний трафик гостям
gzip on;
gzip_comp_level 5;
gzip_min_length 1024;
gzip_vary on;
gzip_types text/css application/javascript application/json image/svg+xml application/xml text/plain;
server_tokens off;
# Базовые заголовки безопасности. Внимание nginx: location со своим
# add_header (например /media/) НЕ наследует их — там заголовки повторены.
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options DENY always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
# CSP: бандл инлайнен в single-file index.html (поэтому 'unsafe-inline' у
# script-src), плюс Метрика/GA и Google Fonts. object/base/frame-ancestors
# наглухо закрыты — внедрённый в страницу тег не подгрузит и не отправит.
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://mc.yandex.ru https://yastatic.net https://www.googletagmanager.com https://www.google-analytics.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' data: https://fonts.gstatic.com; img-src 'self' data: https://mc.yandex.ru https://www.google-analytics.com https://www.googletagmanager.com; connect-src 'self' https://mc.yandex.ru https://www.google-analytics.com https://analytics.google.com; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'" always;
# Заказы и стоп-лист живут в CRM-контейнере (см. docker-compose.yml).
# Точный location = /api/orders ниже переопределяет этот для POST заказов
# со своей зоной (приоритет nginx: точный бьёт префиксный).
location /api/ {
limit_req zone=api_zone burst=60 nodelay;
limit_req_status 429;
proxy_pass http://crm:47613;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_connect_timeout 5s;
proxy_read_timeout 15s;
}
# Точка создания заказа — отдельный тормоз поверх лимитеров CRM
# (nginx отвечает 429 раньше, чем запрос дойдёт до Python).
location = /api/orders {
limit_req zone=orders_zone burst=10 nodelay;
limit_req_status 429;
proxy_pass http://crm:47613;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_connect_timeout 5s;
proxy_read_timeout 15s;
}
# Входная точка SPA: ТОЛЬКО no-cache — в single-file сборке внутри
# index.html инлайнен весь бандл, закэшировать его на 7 дней = гости
# не увидят обновлений сайта до конца срока кэша. (Точное совпадение
# бьёт регэксп-локейшен расширений ниже; прочие .html честно 404.)
location = /index.html {
add_header Cache-Control "no-cache";
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options DENY always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://mc.yandex.ru https://yastatic.net https://www.googletagmanager.com https://www.google-analytics.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' data: https://fonts.gstatic.com; img-src 'self' data: https://mc.yandex.ru https://www.google-analytics.com https://www.googletagmanager.com; connect-src 'self' https://mc.yandex.ru https://www.google-analytics.com https://analytics.google.com; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'" always;
}
# SEO-файлы: точный Content-Type и кэш не больше часа
# (types {} сбрасывает mime.types — иначе .xml ушёл бы как text/xml)
location = /robots.txt {
types { }
default_type text/plain;
expires 1h;
}
location = /sitemap.xml {
types { }
default_type application/xml;
expires 1h;
}
# Загруженные владельцем фото позиций — из CRM (/media/<id>.<ext>).
# ^~ обязателен: без него регэксп-локейшен расширений (*.jpg, *.avif…)
# перехватывал бы запросы, и все фото отдавали 404 вместо прокси в CRM.
location ^~ /media/ {
proxy_pass http://crm:47613;
proxy_http_version 1.1;
proxy_set_header Host $host;
expires 7d;
add_header Cache-Control "public, max-age=604800";
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options DENY always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
add_header Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'" always;
}
# Vite SPA fallback: client-side routes should resolve to index.html.
location / {
try_files $uri $uri/ /index.html;
}
location = /healthz {
access_log off;
add_header Content-Type text/plain;
return 200 'ok\n';
}
# Файлы с расширениями отдаются только с диска: несуществующие — честный
# 404, а не index.html (soft-404 вреден для поисковиков). SPA-fallback
# выше работает только для путей без расширения.
location ~* \.(?:css|js|mjs|cjs|json|map|webmanifest|txt|xml|svg|ico|png|jpg|jpeg|gif|webp|avif|jfif|woff2?|ttf|otf|eot|mp4|webm|mp3|wav|ogg|pdf|html)$ {
expires 7d;
add_header Cache-Control "public, max-age=604800";
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options DENY always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
add_header Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'" always;
try_files $uri =404;
}
}