server { #listen 80; index index.html index.php; ## Begin - Server Info root /home/USER/www/html; server_name localhost; ## End - Server Info ## Begin - Index # for subfolders, simply adjust: # `location /subfolder {` # and the rewrite to use `/subfolder/index.php` location / { try_files $uri $uri/ /index.php?$query_string; } ## End - Index ## Begin - Security # deny hidden files and directories at any depth, except ACME challenges location ~ (^|/)\.(?!well-known(?:/|$)) { return 403; } # deny all direct access for these folders location ~* ^/(\.git|cache|bin|logs|backup|tmp|webserver-configs|tests)/.*$ { return 403; } # deny all direct access to these sensitive user folders, whatever the file type location ~* ^/user/(env/[^/]+/)?config/.*$ { return 403; } # allow avatar images under user/accounts to be served directly, whether stored # at user/accounts/avatars/ (flatfile accounts) or user/accounts// # (Flex folder storage); this must come before the user/accounts deny so it # wins the first-match. SVG is intentionally excluded as a stored-XSS vector. location ~* ^/user/(env/[^/]+/)?accounts/[^/]+/[^/]+\.(jpe?g|png|gif|webp|avif|bmp|ico)$ { try_files $uri =404; } # deny everything else under user/accounts, whatever the file type location ~* ^/user/(env/[^/]+/)?accounts/.*$ { return 403; } # allow public media uploads under user/data (e.g. Flex Object images) to be # served directly; this must come before the user/data deny so it wins the # first-match. SVG is intentionally excluded as a stored-XSS vector. location ~* ^/user/(env/[^/]+/)?data/.*\.(jpe?g|png|gif|webp|avif|bmp|ico|mp4|webm|ogg|ogv|mov|mp3|wav|m4a|flac|pdf)$ { try_files $uri =404; } # deny everything else under user/data, whatever the file type location ~* ^/user/(env/[^/]+/)?data/.*$ { return 403; } # Serve page media only through the page route, so plugins can apply the # page's `access` rules to it. DO NOT uncomment without first setting # `pages.media_route_urls: true` in user/config/system.yaml, or every media # URL on the site becomes a 403. # location ~* ^/user/pages/.*$ { return 403; } # deny running scripts in the public cache folders (image derivatives and combined assets) location ~* ^/(images|assets)/.*\.(php|php2|php3|php4|php5|php7|php8|phar|phtml|pht|phtm|phps|pl|py|cgi|sh|bat)$ { return 403; } # deny running scripts inside core system folders location ~* ^/(system|vendor)/.*\.(txt|xml|md|html|htm|shtml|shtm|json|yaml|yml|php|php2|php3|php4|php5|phar|phtml|pl|py|cgi|twig|sh|bat)$ { return 403; } # deny running scripts inside user folder location ~* ^/user/.*\.(txt|md|json|yaml|yml|php|php2|php3|php4|php5|phar|phtml|pl|py|cgi|twig|sh|bat)$ { return 403; } # deny access to specific files in the root folder location ~* ^/(LICENSE\.txt|composer\.lock|composer\.json|nginx\.conf|web\.config|htaccess\.txt|\.htaccess) { return 403; } # deny access to .env environment files (expression purposefully not anchored) location ~ /\.env(\.|$) { return 403; } # deny Markdown source files such as README.md and CHANGELOG.md; a page route # ending in .md is not a file, so it still reaches Grav's Markdown output location ~* \.md$ { if (-f $request_filename) { return 403; } try_files $uri /index.php?$query_string; } ## End - Security ## Begin - Static asset caching # MUST stay below the security block. nginx picks the FIRST matching regex # location, so a caching rule placed above would answer for paths the denies # above are meant to refuse. For the same reason these use regex (`~*`) and # never `^~`: a `^~` prefix match wins over every regex location regardless # of order, which would bypass the user/accounts and user/data rules. # Content-hashed bundles shipped by SvelteKit-based plugins and themes (the # Admin panel among them). The filename changes whenever the contents do, so # these can be cached permanently. `=404` keeps a missing chunk from booting # PHP just to render a 404 — and, on any host that serves an SPA from a # static index.html fallback, stops a deleted chunk being answered with the # HTML shell at 200, which a browser cannot parse as a module. location ~* /_app/immutable/.*\.(js|css|woff2?)$ { try_files $uri =404; access_log off; add_header Cache-Control "public, max-age=31536000, immutable" always; } # Everything else static gets a moderate TTL and still revalidates via the # ETag/Last-Modified nginx already sends. Deliberately NOT `immutable`: # these filenames are stable across edits, so a long hard cache would pin # stale media until the name changed. # The try_files fallback is load-bearing, not boilerplate: Grav serves some # media through index.php (cached image derivatives, plugin media proxies), # so a bare static handler would hard-404 assets that PHP would have # produced on demand. Adding caching must not change WHAT is served. location ~* \.(jpe?g|png|gif|webp|avif|bmp|ico|svg|mp4|webm|ogg|ogv|mov|mp3|wav|m4a|flac|css|js|woff2?|ttf|otf|eot)$ { try_files $uri $uri/ /index.php?$query_string; expires 30d; access_log off; } ## End - Static asset caching ## Begin - PHP location ~ \.php$ { # Choose either a socket or TCP/IP address fastcgi_pass unix:/var/run/php/php-fpm.sock; # fastcgi_pass unix:/var/run/php5-fpm.sock; #legacy # fastcgi_pass 127.0.0.1:9000; fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root/$fastcgi_script_name; } ## End - PHP }