# nginx: статический сайт-визитка. Репозиторий клонирован в /opt/vizitka-site, раздаём его папку web/ server { listen 80 default_server; listen [::]:80 default_server; server_name _; root /opt/vizitka-site/web; index index.html; # реальный IP посетителя из X-Forwarded-For, если запрос пришёл от прокси из локальной сети (для лимита в API) 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 127.0.0.0/8; 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 application/json image/svg+xml text/plain application/xml; # заголовки безопасности (строгий CSP: на сайте нет inline-скриптов и стилей, внешних ресурсов нет) server_tokens off; add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always; # API кнопки «Помахать» (Node, systemd-сервис vizitka-api), счётчик хранится вне репозитория location /api/ { proxy_pass http://127.0.0.1:8787; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Client-IP $remote_addr; proxy_read_timeout 10s; client_max_body_size 1k; } location / { try_files $uri $uri/ =404; } # кэш: картинки и шрифты надолго, стили и скрипты на час, данные без кэша location ~* \.(woff2|webp|png|jpe?g|svg|ico)$ { expires 30d; add_header Cache-Control "public, max-age=2592000" always; # add_header в location сбрасывает наследуемые, поэтому дублируем безопасные заголовки add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always; add_header X-Content-Type-Options "nosniff" always; } location ~* \.(css|js)$ { add_header Cache-Control "public, max-age=3600" always; add_header X-Content-Type-Options "nosniff" always; } location = /config.json { add_header Cache-Control "no-cache" always; add_header X-Content-Type-Options "nosniff" always; } # скрытые файлы наружу не отдаём location ~ /\. { deny all; } error_page 404 /404.html; location = /404.html { internal; } }