{ # The panel is the only thing served, and it is behind a login: no reason # for the server to announce its version to anybody scanning. frankenphp order php_server before file_server auto_https off servers { trusted_proxies static private_ranges } } {$SERVER_NAME:-:80} { root * /app/public encode zstd gzip # Uploaded images. `public/storage` is a symlink into the storage volume, # so this is what actually serves what the collective uploads — and it is # the one path an SSR frontend fetches on every page view, so it is cached # hard and never touched by PHP. @media path /storage/* header @media Cache-Control "public, max-age=31536000, immutable" # Uploaded SVGs are sanitized on the way in (MediaController). This is the # second line: opened directly rather than through an , an SVG is a # document, and nothing stored here has any business running script or # loading anything from elsewhere. # # Two matchers, which Caddy ANDs, rather than `path /storage/*.svg`: a `*` # in the middle of a path does not cross a `/`, so that pattern only ever # matched files directly under /storage — and every upload is a directory # below it. @svg { path /storage/* path_regexp (?i)\.svg$ } header @svg Content-Security-Policy "default-src 'none'; style-src 'unsafe-inline'; img-src data:" # The frontend is a separate application on another origin. @api path /api/* header @api { X-Content-Type-Options nosniff } header { X-Content-Type-Options nosniff X-Frame-Options SAMEORIGIN Referrer-Policy strict-origin-when-cross-origin -Server } php_server { # Laravel's own health route, so the container can be probed without # touching the database. try_files {path} /index.php } # The health probe runs every 30 seconds and would otherwise be most of # the access log. `log_skip` is the directive for this — `exclude` inside # `log` is not valid in Caddy 2. @health path /up log_skip @health log { output stdout format json } }