Skip to content

Web server configuration

Capell runs on a standard Laravel web server configuration. This page covers the parts that are not standard: serving the static HTML cache, and what changes when you run more than one node.

For the standard parts — DNS, TLS, the base server block, trusted proxies, and process supervision — see Going live. The base server block itself lives in frontend server configuration; the rules below are added to it and only apply when capell-app/html-cache is installed.

Confusing these is the most common source of “why is my page stale” tickets, so it is worth being precise:

NameWhere it livesWho serves it
Object cacheYour Laravel cache storeCapell, in PHP — see page cache architecture
Static HTML cachepublic/page-cacheYour web server, before PHP runs
Static artifactsstorage/framework/capell-static-artifactsNobody — export output from capell:generate-html

Only the middle one needs web-server rules. The static HTML cache is provided by the optional capell-app/html-cache package; if you have not installed it, you can skip the rest of this page.

The point of the static HTML cache is to answer anonymous requests from disk without booting PHP. That only happens if the web server checks for a cached file before falling through to index.php.

Cached files are written as public/page-cache/<path>.html.

Add the page-cache lookup to your location / block, ahead of the normal Laravel fallback:

location / {
try_files /page-cache/$uri.html $uri $uri/ /index.php?$query_string;
}

Never serve the cache to authenticated visitors. If your site has logged-in users, bypass the cache when a session cookie is present:

set $cache_bypass "";
if ($http_cookie ~* "laravel_session") {
set $cache_bypass "no-cache";
}
location / {
if ($cache_bypass = "no-cache") {
rewrite ^ /index.php?$query_string last;
}
try_files /page-cache/$uri.html $uri $uri/ /index.php?$query_string;
}

In public/.htaccess, before Laravel’s existing front-controller rules:

<IfModule mod_rewrite.c>
RewriteEngine On
# Skip the cache for signed-in visitors.
RewriteCond %{HTTP_COOKIE} !laravel_session
RewriteCond %{REQUEST_METHOD} GET
RewriteCond %{DOCUMENT_ROOT}/page-cache/%{REQUEST_URI}\.html -f
RewriteRule ^(.*)$ /page-cache/$1.html [L]
</IfModule>

Only ever serve cached HTML for GET. A cached response to a POST is always a bug.

Run this on every node after new code is in place:

Terminal window
php artisan capell:runtime-refresh

It rebuilds the package, view, config, and route caches, warms critical pages, runs Capell Doctor, and exits non-zero if any stage fails — so a broken deploy fails your pipeline rather than your site. Under Octane, restart the workers afterwards.

Capell was designed for single-node hosting, and several subsystems still assume it. Before scaling out, account for these operating constraints:

  • Use a shared cache store. Redis or Memcached reachable by every node. The file and array drivers make each node’s cache — and every lock built on it — private to that node. This is a correctness requirement, not a performance tip. The web installer refuses to start when CAPELL_MULTI_NODE=true and its progress state would use a node-local cache store.
  • The static HTML cache is node-local. public/page-cache lives on each node’s own disk, and publishing a page invalidates only the node that handled the request. Either put it on shared storage, or put a CDN or reverse proxy in front and purge that instead.
  • Run capell:runtime-refresh on every node. The caches it rebuilds are per-node.
  • Build assets once, deploy them everywhere. The admin’s “rebuild frontend assets” action writes to the local public/ of whichever worker ran it; other nodes will 404 the hashed filenames.
  • Run only one upgrade at a time. Current installs enforce this in the shared database through capell_upgrade_locks, independently of cache topology. An installation upgrading from a version before that table existed temporarily falls back to the configured cache lock until its migrations run, so the shared-cache requirement still applies during that first upgrade.
  • Use a shared disk for Livewire temporary uploads. Filament uploads a file in one request and consumes it in a later one. When CAPELL_MULTI_NODE=true, Capell rejects Livewire uploads backed by a node-local filesystem even if the load balancer normally provides session affinity; the storage topology remains correct during failover.