Forcing HTTPS can be done at the web server level (Nginx or Apache config) or inside the Laravel application itself via middleware — the server-level approach is generally preferred where you have that access, since it redirects before the request even reaches PHP.
Nginx-level redirect (the generally recommended approach)
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
This redirects every HTTP request before it ever reaches PHP or Laravel at all — more efficient than an application-level redirect, since no framework bootstrapping happens for a request that's just going to be redirected anyway.
Apache-level redirect (.htaccess)
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
Application-level middleware, when server config access isn't available
php artisan make:middleware ForceHttps
class ForceHttps
{
public function handle(Request $request, Closure $next)
{
if (! $request->secure() && app()->environment('production')) {
return redirect()->secure($request->getRequestUri());
}
return $next($request);
}
}
Checking app()->environment('production') avoids forcing HTTPS during local development, where a local dev server typically doesn't have a valid SSL certificate configured at all.
Registering the middleware globally
// bootstrap/app.php (Laravel 11+)
->withMiddleware(function (Middleware $middleware) {
$middleware->prepend(ForceHttps::class);
})
Using prepend() rather than append() runs this check as early as possible in the middleware stack — reasonable given that redirecting away from an insecure connection is something you generally want to happen before most other middleware logic runs.
The simpler URL::forceScheme() alternative for generated URLs specifically
// AppServiceProvider::boot()
if (app()->environment('production')) {
URL::forceScheme('https');
}
This doesn't redirect incoming requests at all — it only ensures that URLs Laravel itself generates (via route(), url(), and similar helpers) always use https://, which matters specifically when the app runs behind a load balancer or proxy that terminates SSL before the request reaches Laravel, making $request->secure() potentially report incorrectly without additional trusted-proxy configuration.
Trusted proxies: a common gotcha behind a load balancer
// bootstrap/app.php (Laravel 11+)
->withMiddleware(function (Middleware $middleware) {
$middleware->trustProxies(at: '*');
})
When Laravel runs behind a load balancer that terminates SSL (common on many cloud hosting platforms), the actual request Laravel receives internally may appear as plain HTTP even though the original client connection was HTTPS — configuring trusted proxies correctly is what lets Laravel's $request->secure() check read the real original scheme from the X-Forwarded-Proto header instead of being fooled by the internal connection.