skip to content

When a Laravel Octane app sits behind an Nginx proxy that terminates TLS, why can it generate http:// links, and what do OCTANE_HTTPS and the proxy config fix?

level: middleimportance: should knowfreq 35%

answer

  1. Octane sees plain HTTP from Nginx
  2. octane.https defaults to false
  3. forceScheme('https') per request
  4. try_files $uri, then @octane
  5. proxy_pass to 127.0.0.1:8000

basics

~20 s

Nginx talks plain HTTP to Octane on 127.0.0.1:8000, so Laravel sees an http request and builds http:// URLs. OCTANE_HTTPS=true makes Octane force the https scheme on every request; Nginx serves static files and proxies the rest to Octane.

solid answer

~30 s

In the documented setup, Nginx terminates TLS, serves files from `public/` directly, and proxies everything else to Octane at `http://127.0.0.1:8000`. Octane therefore receives a plain-HTTP request, and `url()`, `route()` and redirects come out as `http://`, which breaks links and can cause redirect loops. Setting `OCTANE_HTTPS=true` (read by `'https' => env('OCTANE_HTTPS', false)` in `config/octane.php`) makes Octane's `EnforceRequestScheme` listener call `forceScheme('https')` on the URL generator and set the request's `HTTPS` server variable to `on` for every request. The Nginx side uses `try_files $uri $uri/ @octane;`, a named `@octane` location with `proxy_http_version 1.1`, the `Host`, `X-Forwarded-For`, `Upgrade` and `Connection` headers, and `proxy_pass http://127.0.0.1:8000$suffix;`.

code

nginx · 20 lines
nginx
location /index.php {
    try_files /not_exists @octane;
}

location / {
    try_files $uri $uri/ @octane;
}

location @octane {
    set $suffix "";
    if ($uri = /index.php) {
        set $suffix ?$query_string;
    }
    proxy_http_version 1.1;
    proxy_set_header Host $http_host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
    proxy_pass http://127.0.0.1:8000$suffix;
}

go deeper

for a junior

Recall that Nginx sits in front for TLS and static files, Octane listens on 127.0.0.1:8000, and OCTANE_HTTPS=true fixes http:// links.

for a middle

Explain why the scheme is lost at the proxy hop, what EnforceRequestScheme does per request, and the try_files and @octane shape of the Nginx config.

for a senior

Show you diagnose mixed content and redirect loops back to the scheme, keep Octane on loopback, and preserve WebSocket and client-address headers.

for a principal

Decide where TLS terminates across the fleet - load balancer, Nginx or FrankenPHP itself - and keep each app's scheme handling consistent with it.

## The topology Octane's docs recommend running an Octane app in production **behind a traditional web server such as Nginx**. The split of duties is: - **Nginx** listens on 80/443, holds the TLS certificate, and serves static assets (images, CSS, built JavaScript) straight from `public/`. - **Octane** listens on `127.0.0.1:8000` - its default host and port - and handles only the requests that are not static files. Binding Octane to the loopback address means nothing outside the machine can reach it directly; all traffic goes through Nginx. ## Why links come out as `http://` TLS ends at Nginx. The hop from Nginx to Octane is plain HTTP, so from Laravel's point of view the request arrived over `http`. Everything that builds an absolute URL from the current request - `url()`, `route()`, `redirect()->to()`, asset URLs, signed URLs, pagination links - uses that scheme. Symptoms: - pages served over HTTPS that reference `http://` assets, which browsers block as mixed content; - redirects to `http://`, which Nginx may redirect back to `https://`, looping; - signed URLs whose signature was computed for one scheme and checked against another. ## What `OCTANE_HTTPS` does `config/octane.php` contains `'https' => env('OCTANE_HTTPS', false)`. Among the listeners Octane runs on **every** `RequestReceived` event is `EnforceRequestScheme`. When `octane.https` is true it: 1. calls `forceScheme('https')` on the request's URL generator, so every generated URL starts with `https://`; 2. sets the request's `HTTPS` server variable to `on`, so the request itself reports that it is secure. A companion listener, `EnsureRequestServerPortMatchesScheme`, fills in `SERVER_PORT` as 443 or 80 when the request carries no port. Because these run per request, they fit Octane's model of one long-lived application serving many requests. `OCTANE_HTTPS` is a blunt switch: it forces HTTPS for every request, so leave it `false` locally when you browse over plain HTTP. The general Laravel alternative - trusting the proxy and reading `X-Forwarded-Proto` - requires the proxy to send that header, which the Nginx example in Octane's docs does not; the Octane-specific fix is the environment variable. ## The Nginx side The configuration in Octane's docs has a recognisable shape: | Piece | Purpose | |---|---| | `root /path/to/app/public;` | static files are looked up in `public/` | | `location / { try_files $uri $uri/ @octane; }` | serve an existing file, otherwise hand the request to Octane | | `location /index.php { try_files /not_exists @octane; }` | a direct hit on `/index.php` is never executed by Nginx; it always goes to Octane | | `location @octane { ... proxy_pass http://127.0.0.1:8000$suffix; }` | the named location that proxies to Octane, re-adding the query string for `/index.php` | | `proxy_http_version 1.1;` with `Upgrade` and `Connection` headers | lets WebSocket upgrade requests pass through | | `Host`, `X-Forwarded-For`, `REMOTE_ADDR`, `SERVER_PORT` headers | pass the original host and client address along | How `try_files`, named locations and `proxy_pass` work in general is Nginx's own subject. What matters for Octane is that no PHP-FPM `fastcgi_pass` appears anywhere: Nginx never runs PHP, it only proxies. ## When you do not need Nginx for TLS FrankenPHP can terminate TLS itself: `octane:start --server=frankenphp --https` enables HTTPS, HTTP/2 and HTTP/3 with automatically generated and renewed certificates, and `--http-redirect` adds HTTP-to-HTTPS redirects. Then the app sees HTTPS requests directly. Behind Nginx or a load balancer, every driver needs the same fix. ## Diagnosing a scheme problem 1. Open a page over HTTPS and inspect generated links and form actions: `http://` means Laravel thinks the request was plain HTTP. 2. Check the effective config value with `php artisan config:show octane` or tinker, remembering that cached config wins over `.env`. 3. After changing the value, reload the workers so they boot with the new configuration. 4. Confirm Nginx still proxies through `@octane` and that no leftover FastCGI location intercepts `.php` requests. ## Checklist - Octane bound to `127.0.0.1` (the default) behind Nginx; - `OCTANE_HTTPS=true` in the production `.env`; - Nginx serving `public/` and proxying the rest to `@octane`; - WebSocket headers kept if the app upgrades connections.

  • Why does the Octane Nginx config route /index.php through try_files /not_exists @octane?
    No PHP-FPM is configured, so Nginx must never try to serve `index.php` as a file. Pointing `try_files` at a path that cannot exist forces the fallback to `@octane`, and the `$suffix` logic re-adds the query string, so a request to `/index.php?page=2` reaches Octane intact.
  • What breaks if OCTANE_HTTPS=true is left on in a local environment served over plain HTTP?
    Octane forces `https` on every generated URL, so redirects, form actions and asset links point to `https://localhost:8000`, which the plain-HTTP server does not answer. Keep it `false` locally and set it only where a proxy or FrankenPHP's `--https` actually serves TLS.

saying these in an interview costs you the question

  • Octane detects TLS at Nginx automatically, so no setting is needed
  • OCTANE_HTTPS makes Octane listen for TLS on port 443
  • Nginx should pass .php requests to PHP-FPM alongside Octane
  • Octane should bind 0.0.0.0 so Nginx on the same host can reach it
  • Every static asset should go through Octane for consistent headers