Horizon

Share a Laravel app

Share a Laravel app from php artisan serve with a Horizon tunnel, with HTTPS links, trusted proxies and APP_URL set.

Share your Laravel app from php artisan serve with a public HTTPS URL, with generated links pointing at the tunnel.

Before you begin

  • Node.js 18 or later
  • A Horizon account and the CLI (see Getting started)
  • A reserved subdomain for -s. Reserve one on the Subdomains page.
  • A Laravel 11 or later app. The bootstrap/app.php code below uses its middleware API.

Start the dev server

php artisan serve listens on port 8000 by default.

php artisan serve

Start a tunnel

Use -s with a subdomain you reserved. Without it, the subdomain is random and changes every run. Reserved subdomains are a paid feature, see Pricing.

hrzn tunnel http://localhost:8000 -s my-app
Output
HORIZON: Tunnel connected
  URL          https://my-app.hrzn.run (reserved)
  Forwarding   http://localhost:8000
  Request log  https://hrzn.run/dashboard/tunnels/my-app

Your public URL is https://my-app.hrzn.run. Keep this terminal open.

Trust the forwarded host

During a web request, url() and route() use the scheme and host of the current request. The CLI sets Host to the local address, so Laravel builds links with that address. The public host arrives in X-Forwarded-Host.

Tell Laravel to trust that header with the TrustProxies middleware. Trust the host header only. Leave X-Forwarded-Proto and X-Forwarded-Port out, because the values Horizon sends don't describe the public https address.

bootstrap/app.php
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Http\Request;

->withMiddleware(function (Middleware $middleware): void {
    $middleware->trustProxies(
        at: '*',
        headers: Request::HEADER_X_FORWARDED_HOST,
    );
})

at: '*' trusts every sender. That's fine on your laptop, where only the Horizon CLI connects. Remove it before you deploy.

Set the https scheme and APP_URL

Tell Laravel the public scheme explicitly. URL::forceScheme('https') makes url(), route() and asset() generate https links.

app/Providers/AppServiceProvider.php
use Illuminate\Support\Facades\URL;

public function boot(): void
{
    if ($this->app->environment('local')) {
        URL::forceScheme('https');
    }
}

APP_URL is the URL Laravel uses where no request exists, such as Artisan commands and queued jobs. Set it to the public URL so links in queued emails and notifications point at the tunnel.

.env
APP_URL=https://my-app.hrzn.run

Run php artisan config:clear if you cached your configuration, then restart php artisan serve.

Check it works

Add a route that prints the URL Laravel generates.

routes/web.php
use Illuminate\Support\Facades\Route;

Route::get('/where', fn () => url('/'));

Open https://my-app.hrzn.run/where. On the first visit Horizon shows a Before you continue page. Select Continue to site. The page prints https://my-app.hrzn.run. The Horizon terminal prints:

Output
  GET     200  /where

Troubleshooting

Laravel isn't trusting X-Forwarded-Host. Check trustProxies in bootstrap/app.php and that the headers include Request::HEADER_X_FORWARDED_HOST.

Check that URL::forceScheme('https') runs. It's inside an environment('local') check, so APP_ENV must be local.

Scripts and styles don't load

If you run npm run dev, Vite serves assets from your own machine, which a visitor can't reach. Stop it and run npm run build instead. Laravel then serves the built files through the tunnel.

The URL changed after a restart

You started the tunnel without -s, so Horizon gave you a new random subdomain. Restart with -s my-app and update APP_URL. -s needs a subdomain you reserved, see Pricing.

Nothing reaches your app

  • Check that the Horizon terminal is still running. If its last line is Connection lost. Reconnecting…, wait for Reconnected.
  • Check that php artisan serve listens on port 8000, the port in your hrzn tunnel command.

Next steps

On this page