# Reverse proxy for Docker Compose (HTTP only — add SSL at host/load balancer in real prod) # Use Docker DNS with short TTL so gateway picks up new container IPs after rebuild. # SPA dashboard fires many parallel /api calls on load — keep auth/ML strict, API generous. limit_req_zone $binary_remote_addr zone=docker_auth:10m rate=10r/m; limit_req_zone $binary_remote_addr zone=docker_api:10m rate=600r/m; limit_req_zone $binary_remote_addr zone=docker_ml:10m rate=3r/m; server { listen 80; client_max_body_size 16M; resolver 127.0.0.11 valid=10s ipv6=off; resolver_timeout 5s; add_header Content-Security-Policy "default-src 'self'; img-src 'self' data: https:; media-src 'self' data: https: blob:; connect-src 'self' https://accounts.google.com https://www.youtube.com https://www.googleapis.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://api.fontshare.com; font-src 'self' https://fonts.gstatic.com https://cdn.fontshare.com; script-src 'self' https://www.youtube.com https://s.ytimg.com; frame-src 'self' https://www.youtube.com https://www.youtube-nocookie.com; frame-ancestors 'none'"; location /api/ { limit_req zone=docker_api burst=120 nodelay; set $api_backend api:8000; proxy_pass http://$api_backend; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 300s; } location ~ ^/api/(auth/login|auth/register|auth/forgot-password)$ { limit_req zone=docker_auth burst=5 nodelay; set $api_backend api:8000; proxy_pass http://$api_backend; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } location ~ ^/api/(speaking/evaluate|shadowing/pronunciation/check|vocabulary/pronunciation/check|pronunciation/word|conversation/turn/voice)$ { limit_req zone=docker_ml burst=2 nodelay; set $api_backend api:8000; proxy_pass http://$api_backend; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 300s; } location /uploads/ { set $api_backend api:8000; proxy_pass http://$api_backend$uri$is_args$args; proxy_set_header Host $host; add_header Cache-Control "public, max-age=86400"; } location /audio/ { set $api_backend api:8000; proxy_pass http://$api_backend$uri$is_args$args; } location /images/ { set $api_backend api:8000; proxy_pass http://$api_backend$uri$is_args$args; } # Legacy thumbnail URLs (/api/images/{uuid}) → same handler as /images/ location /api/images/ { set $api_backend api:8000; rewrite ^/api/images/(.*)$ /images/$1 break; proxy_pass http://$api_backend; proxy_set_header Host $host; } location / { set $frontend_backend frontend:80; proxy_pass http://$frontend_backend$uri$is_args$args; proxy_set_header Host $host; } gzip on; gzip_types text/plain application/json application/javascript text/css; }