server { listen 80 default_server; server_name _; root /var/www/html/public; index index.php; charset utf-8; # NOTE: client_max_body_size is deliberately NOT set here. # # entrypoint.sh writes it to /etc/nginx/conf.d/upload-limit.conf from # MAX_UPLOAD_MB, together with PHP's upload_max_filesize/post_max_size, so # that one variable governs the whole chain. A value in this server block # would override the generated one and silently reinstate the mismatch this # arrangement exists to prevent — the symptom being a 413 with no body, # returned after zero bytes, which leaves the browser's upload bar frozen # and the page with nothing to report. # Behind Dokploy's Traefik, the real client IP arrives in X-Forwarded-For. real_ip_header X-Forwarded-For; 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; real_ip_recursive on; location / { try_files $uri $uri/ /index.php?$query_string; } location = /favicon.ico { access_log off; log_not_found off; } location = /robots.txt { access_log off; log_not_found off; } # Vite emits content-hashed filenames, so these are safe to cache hard. location /build/ { expires 1y; add_header Cache-Control "public, immutable"; access_log off; try_files $uri =404; } # Uploaded territory PDFs and thumbnails (public/storage -> storage/app/public). # Served directly by nginx rather than round-tripping through PHP. location /storage/ { expires 7d; add_header Cache-Control "public"; try_files $uri =404; } error_page 404 /index.php; location ~ ^/index\.php(/|$) { fastcgi_pass 127.0.0.1:9000; fastcgi_split_path_info ^(.+\.php)(/.*)$; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; fastcgi_param PATH_INFO $fastcgi_path_info; fastcgi_param DOCUMENT_ROOT $realpath_root; fastcgi_hide_header X-Powered-By; # PDF generation and ZIP imports can outrun the default 60s. fastcgi_read_timeout 300; fastcgi_buffers 16 16k; fastcgi_buffer_size 32k; # Set server-side, NOT taken from the client's header: honouring a # client-supplied X-Forwarded-Proto would let an attacker declare the # connection plaintext and skew signed-URL validation and secure-cookie # decisions. The value is written by the entrypoint from APP_URL's # scheme (see /etc/nginx/conf.d/forwarded-proto.conf) — https in # production behind Traefik, http for local plain-HTTP development. fastcgi_param HTTP_X_FORWARDED_PROTO $app_forwarded_proto; # X-Forwarded-Port is deliberately NOT set here. nginx already passes # the incoming header through as HTTP_X_FORWARDED_PORT, and Traefik # sends the real public port; overriding it with $server_port (always # 80 inside the container) would corrupt generated URLs. internal; } # Any other .php file is not a valid entrypoint — Laravel routes everything # through index.php. This blocks execution of uploaded/stray scripts. location ~ \.php$ { return 404; } location ~ /\.(?!well-known).* { deny all; } add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; gzip on; gzip_vary on; gzip_min_length 1024; gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml; }