# Сплит-тренер: статический сайт и прокси к API аккаунтов. Один и тот же файл годится и для контейнера, и для nginx на хосте. # Скрипт deploy/install-debian.sh заменяет строки с пометкой "# <-": listen, server_name, root и адрес API. # Совместимо с nginx 1.22 (Debian 12) и 1.26 (Debian 13). # Грубая защита API от потока запросов (точные лимиты на вход, регистрацию и сброс пароля считает сам API) limit_req_zone $binary_remote_addr zone=splitapi_zone:10m rate=10r/s; server { listen 8080; # <- порт или адрес:порт server_name _; # <- имя сайта root /usr/share/nginx/html; # <- каталог с файлами index index.html; server_tokens off; # За обратным прокси редиректы не должны подставлять внутренний хост и порт absolute_redirect off; port_in_redirect off; # Настоящий адрес клиента (для логов) берём из X-Forwarded-For, но только от прокси из частных сетей и localhost set_real_ip_from 127.0.0.1; set_real_ip_from ::1; set_real_ip_from 10.0.0.0/8; set_real_ip_from 172.16.0.0/12; set_real_ip_from 192.168.0.0/16; set_real_ip_from fc00::/7; real_ip_header X-Forwarded-For; real_ip_recursive on; gzip on; gzip_vary on; gzip_min_length 512; gzip_types text/css application/javascript text/javascript text/plain application/json image/svg+xml; # API аккаунтов (отдельный процесс). Если его нет, ответ 502: сайт это замечает и просто не показывает вход. # Адрес в переменной и resolver (DNS Docker): так nginx стартует, даже если контейнер API ещё не поднялся. resolver 127.0.0.11 valid=10s ipv6=off; location /api/ { set $api_upstream http://splitapi:8000; # <- адрес API proxy_pass $api_upstream; limit_req zone=splitapi_zone burst=20 nodelay; limit_req_status 429; client_max_body_size 300k; proxy_connect_timeout 3s; proxy_read_timeout 20s; proxy_set_header Host $host; # $remote_addr здесь уже настоящий клиент (real_ip выше); API верит этому заголовку, только если порт API недоступен снаружи proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # Проверка живости для мониторинга и healthcheck контейнера location = /healthz { access_log off; default_type text/plain; return 200 "ok\n"; } # Все заголовки безопасности заданы в одном location: add_header не наследуется между уровнями. # no-cache не запрещает кеш, а заставляет браузер сверять версию (ETag) при каждом открытии: # после обновления никто не застрянет на старом app.js. location / { try_files $uri $uri/ =404; add_header Cache-Control "no-cache" always; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "no-referrer" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always; add_header Content-Security-Policy "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'self'" always; # HSTS не ставим здесь: HTTPS завершает внешний прокси, он и должен отдавать Strict-Transport-Security. } # Скрытые файлы (.git, .env и т. п.) не отдаём никогда location ~ /\. { return 404; } }