# Deploy Laravel

> Deploy a Laravel app with Falak on FrankenPHP or PHP-FPM — migrations on the leader, queues, the scheduler, Horizon and zero-downtime releases.

Source: https://falak.sh/docs/deploy/laravel/

This guide deploys a Laravel 11, 12 or 13 app from a Git repository to one or more servers. You end with zero-downtime deployments, migrations that run once, a working scheduler, and optional queue workers or Horizon. Statamic sites use the same flow with the **Statamic** preset.

## Prerequisites

- A connected Git provider: [GitHub App](/docs/guides/connect-github/) or [a token](/docs/guides/connect-git-tokens/).
- An **active** server of type **app** or **web** with PHP installed (FrankenPHP is the default runtime). See [Connect a server](/docs/servers/connect-custom-server/).
- Optional: a database. Create one on an app server with a database engine, or on a **db** server. See [Create databases](/docs/databases/create-databases/).
- Your repository has `composer.json` and `artisan`. If it has a `package.json` with a `build` script (Vite), assets are built too.

## Deploy

1. **Create a database** (skip if your app does not need one). On the canvas: **+ Create → Database**, pick the engine and server. Name the service, for example `shop-db`.

2. **Create the site.** **+ Create → Git repository** → choose your connection, repository and branch. Falak detects Laravel and selects the **Laravel** preset.

3. **Pick servers.** Select one or more servers. The first one is the **leader**: migrations and the scheduler run there.

4. **Choose a domain.** Keep **Generate** for an instant `sslip.io` URL, or enter your own domain. See [Domains](/docs/guides/domains/).

5. **Click Deploy.** The panel opens on the **Deployments** tab and streams the first deployment.

6. **Connect the database.** Open the site's **Variables** tab and add references to the database service:

   ```dotenv title="Site variables"
   DB_CONNECTION=${{ shop-db.DB_CONNECTION }}
   DB_HOST=${{ shop-db.DB_HOST }}
   DB_PORT=${{ shop-db.DB_PORT }}
   DB_DATABASE=${{ shop-db.DB_DATABASE }}
   DB_USERNAME=${{ shop-db.DB_USERNAME }}
   DB_PASSWORD=${{ shop-db.DB_PASSWORD }}
   ```

   Or use one line: `DB_URL=${{ shop-db.DATABASE_URL }}`. References resolve at deploy time. See [Variable references](/docs/guides/variable-references/).

   If the database lives on the **same app server** as the site, the reference gives `DB_HOST=127.0.0.1` for a native site, or the server's own address for a Docker site (engines on app servers serve that server only). That works only while the site runs on that server alone; on other servers the deploy fails with an explanation, so use a database on a `db` server and [allow the app servers in its firewall](/docs/databases/remote-access/).

7. **Redeploy** to apply the variables. Variable changes always take effect on the next deployment.

## What the Laravel preset sets up

| Setting | Value |
|---|---|
| Runtime | `frankenphp` (switch to `php-fpm` if you prefer) |
| Web directory | `public` |
| Shared paths | `storage/` (directory), `.env` (file) — kept across releases in `shared/` |
| Health check | `GET /up` expecting `200` (Laravel's built-in health route) |
| Scheduler | On |
| Horizon, Octane | Off |
| Initial variables | `APP_NAME`, `APP_ENV=production`, `APP_KEY` (generated `base64:` key), `APP_DEBUG=false`, `APP_URL` (the chosen domain), `LOG_CHANNEL=daily` |

### The default deploy script

```bash title="Deploy script (Laravel preset)"
$FALAK_FETCH

cd "$FALAK_RELEASE_DIR"
if [ "$FALAK_IS_LEADER" = "1" ]; then
    $FALAK_PHP artisan migrate --force
fi
$FALAK_PHP artisan optimize
$FALAK_PHP artisan storage:link --force

$FALAK_ACTIVATE
$FALAK_RESTART_PROCS
```

- `$FALAK_FETCH` downloads the built release and links shared paths.
- The `if` block runs **only on the leader**, so migrations run once even with ten servers.
- `$FALAK_PHP` is the PHP CLI of the site's PHP version, for example `php8.4`.
- `$FALAK_ACTIVATE` switches `current` on all servers together.
- `$FALAK_RESTART_PROCS` restarts workers, Horizon and Octane with the new release.

Edit it under **Settings → Deploy**. All macros and variables are in [Deploy scripts](/docs/guides/deploy-scripts/).

Migrations run **before** activation, while the previous release still serves traffic. Avoid dropping or renaming columns the running release uses; do it in a later deployment.

## FrankenPHP or PHP-FPM

  
    The server's FrankenPHP process embeds Caddy and runs PHP in-process. There is no separate PHP-FPM. It is the default for new servers and sites, and it enables [Octane in worker mode](/docs/deploy/laravel-octane/).

    On FrankenPHP, PHP runs as the edge user, which joins each site's group so it can read `.env` and write to `storage/`.
  
  
    A standalone Caddy serves static files and passes `.php` requests to a per-site PHP-FPM pool running as the site user. Choose **PHP-FPM** as the server's PHP runtime when you create the server, and `php-fpm` as the site runtime.

    Octane on PHP-FPM servers uses Swoole or RoadRunner, which you must install yourself.
  

## Queues

Add queue workers under the **Processes** tab:

| Field | Meaning | Range |
|---|---|---|
| Connection | Queue connection (empty = default) | |
| Queue | Comma-separated queue names | |
| Processes | Number of worker processes | 1–64 |
| Timeout, Sleep, Tries, Backoff | Passed to `queue:work` | |
| Max jobs, Max time | Restart a worker after N jobs / seconds | |
| Memory | `--memory` in MB | 32–65536 |
| Command | Replace `queue:work` with your own command | |
| Servers | Run on selected servers only (default: all) | |

The worker command is `php8.4 artisan queue:work <connection> --queue=<queues> --sleep=<s> --tries=<n> --timeout=<s> --memory=<mb> …`. Workers restart on every deployment. See [Processes](/docs/guides/processes/).

## Horizon

Turn on **Horizon** under **Settings → Laravel**. Falak supervises `php artisan horizon` on each server and sends `horizon:terminate` on deploys so running jobs finish (with a 120-second stop timeout). Install `laravel/horizon` in your app first.

## Scheduler

The scheduler is **on** by default. Falak runs `php artisan schedule:run` every minute **on the leader only**, in the current release, as a cron job with a heartbeat. Missed runs show up under [Insights → Heartbeats](/docs/observability/insights/).

Laravel's `withoutOverlapping()` handles overlaps; Falak never skips a minute because a previous run is still going.

## Maintenance mode

Toggle **Maintenance** under **Settings → Laravel**. Falak runs `php artisan down --retry=60` (or `php artisan up`) on every server immediately.

## Logs

New Laravel sites use `LOG_CHANNEL=daily`. The agent tails `shared/storage/logs/*.log`, merges multi-line stack traces into one record, and ships them to Loki with the site's labels. See [Logs](/docs/observability/logs/).

Do not switch to `LOG_CHANNEL=stderr`: under FrankenPHP and PHP-FPM, web requests share the server process's stderr, so those lines cannot be attributed to your site.

## Instrument with APM

Install the Falak APM package to get request timelines, slow queries, N+1 hints, job traces and grouped exceptions:

```bash
composer require falak/apm-laravel
```

No configuration is needed on a Falak server. See [APM for Laravel](/docs/observability/apm-laravel/).

## Run artisan commands

Use **Settings → Commands** to run a one-off command such as `php artisan tinker --execute="…"` or `php artisan cache:clear` on the site's servers, with live output. Commands time out after 600 seconds (`FALAK_SITE_COMMAND_TIMEOUT`).

## Troubleshooting

| Symptom | Cause and fix |
|---|---|
| Health check fails with 500 | Open **View logs**, then the site's **Logs** tab. Usually a missing variable (database, `APP_KEY`) or a failed `artisan optimize`. |
| `Unresolved variable references: …` | A `${{ service.KEY }}` points at a service or key that does not exist in this environment. |
| `Permission denied` writing to `storage/` | `storage/` must be a shared path (default). Redeploy so permissions are fixed. |
| Migrations ran twice | Your script runs migrations outside the `FALAK_IS_LEADER` block. |
| Scheduler never runs | It only runs on the leader, and only after the first successful deployment. |
| Assets missing | Your `package.json` needs a `build` script; `node_modules` is not shipped for PHP sites. |

## Next steps
