# nginx для my-olymp.ru. # # Он и есть та самая «граница на 443», которую требует MAX: TLS, статика # мини-приложения и прокси в бэкенд — на одном домене и с одним # сертификатом. Бэкенд слушает обычный HTTP внутри сети compose и на хост # не публикуется. # # / статика из frontend/ # /api/* бэкенд, авторизация по подписи данных запуска # /webhook бэкенд, события бота от платформы MAX # /health бэкенд, проверка живости # /docs, /openapi.json схема API (закрыта при APP_ENV=production) # # Домен прописан здесь явно — nginx не подставляет переменные окружения. # Всего он указан в трёх местах: здесь, в PRODUCTION_URL внутри # frontend/api.js (им пользуется только копия на GitHub Pages) и в # PUBLIC_BASE_URL в .env. # # В образ frontend/Dockerfile запечена локальная конфигурация без TLS, # local.conf; этот файл подкладывает вместо неё docker-compose.prod.yml. upstream backend { # Имя сервиса из docker-compose.prod.yml. server backend:8000; } # Слушаем только IPv4: у домена одна A-запись, а IPv6 внутри контейнера # есть не всегда — при ipv6.disable=1 на хосте nginx не деградирует, а # падает на старте с «Address family not supported by protocol». Появится # AAAA-запись — добавьте рядом с каждым listen парную строку # `listen [::]:80;` и `listen [::]:443 ssl;`. # --- HTTP: проверка домена и переезд на HTTPS -------------------------- server { listen 80; server_name my-olymp.ru www.my-olymp.ru; # Подтверждение владения доменом для Let's Encrypt. Должно остаться # доступным по http: через него certbot продлевает сертификат. location /.well-known/acme-challenge/ { root /var/www/certbot; } location / { return 301 https://my-olymp.ru$request_uri; } } # --- www: уводим на основной адрес ------------------------------------- # У приложения один адрес — https://my-olymp.ru: его регистрируют в # кабинете MAX, на него оформлен вебхук, его origin перечислен в # CORS_ORIGINS. Вторую копию на www не обслуживаем, а перенаправляем. # # Блок требует, чтобы сертификат был выписан и на www. Если A-записи для # www нет — удалите блок, иначе браузер на www покажет ошибку сертификата. server { listen 443 ssl; http2 on; server_name www.my-olymp.ru; ssl_certificate /etc/letsencrypt/live/my-olymp.ru/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/my-olymp.ru/privkey.pem; return 301 https://my-olymp.ru$request_uri; } # --- HTTPS: мини-приложение и API -------------------------------------- server { listen 443 ssl; http2 on; server_name my-olymp.ru; ssl_certificate /etc/letsencrypt/live/my-olymp.ru/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/my-olymp.ru/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_session_cache shared:SSL:10m; ssl_session_timeout 1d; ssl_session_tickets off; # Адрес мини-приложения открывается только по https, так что незачем # ждать редиректа на второй визит. add_header Strict-Transport-Security "max-age=31536000" always; add_header X-Content-Type-Options "nosniff" always; # Событие от MAX и любой запрос мини-приложения — это небольшой JSON. client_max_body_size 1m; charset utf-8; charset_types text/css application/javascript image/svg+xml; root /srv/frontend; index index.html; # --- Бэкенд ------------------------------------------------------- # Идут первыми: иначе location / попытается найти файл и вернёт # index.html вместо ответа API. location /api/ { include /etc/nginx/proxy.conf; } location = /webhook { include /etc/nginx/proxy.conf; } location = /health { include /etc/nginx/proxy.conf; } # Схема API. Приложение само закрывает её при APP_ENV=production и # отдаёт 404, так что проксировать безопасно. location = /docs { include /etc/nginx/proxy.conf; } location = /openapi.json { include /etc/nginx/proxy.conf; } # --- Статика мини-приложения -------------------------------------- location / { # Не кэшируем: MAX открывает приложение во встроенном браузере, и # закэшированная версия переживает деплой — пользователь остаётся # на старом коде, отладить это почти невозможно. add_header Cache-Control "no-cache, must-revalidate" always; # Повтор, а не дублирование: add_header во вложенном блоке не # добавляется к унаследованным, а заменяет их целиком. Без этих # двух строк статика уезжала бы вообще без заголовков из server. add_header Strict-Transport-Security "max-age=31536000" always; add_header X-Content-Type-Options "nosniff" always; # Неизвестный путь отдаём тем же index.html: приложение # одностраничное, и попасть на 404 из-за лишнего сегмента в # адресе оно не должно. try_files $uri $uri/ /index.html; } }