Laravel hosting on Fly.io: expressive PHP, deployed globally

Deploy your Laravel stack over multiple regions with simplicity, flexibility, and scalability at your fingertips

Launch Now

Ready, Set, Go!

Speedrun Your Laravel App Onto Fly.io

Deploy a Laravel app to Fly.io in a few minutes with Fly.io's command line tool, flyctl.

Get Started
> Install flyctl on GNU/Linux
curl -L https://fly.io/install.sh | sh
 
> Run from the root of a Laravel project
> just like php artisan:serve
fly launch
 
> Scale CPU, memory, instances & regions
fly scale

Not on GNU/Linux? Install flyctl for your platform.

Effortless, Global Deployment

Local to Live, in Minutes!

Transition smoothly from local development to global deployment with flyctl. Launch, deploy, and scale your Laravel apps across 30+ regions in minutes, provisioning memory, storage, and CPU resources with a few commands. Into automated workflows? Deploy your repository changes with Github Actions, directly to your Laravel Fly App.

NEW!

Managed Postgres for Laravel Projects

Focus on features and leave database management to Fly.io. Our Managed Postgres service, gives you:

  • Automatic backups and recovery
  • High availability with automatic failover
  • Performance monitoring and metrics
  • Resource scaling (CPU, RAM, storage)
  • 24/7 support and incident response
  • Automatic encryption of data at rest and in transit
Learn More

Supercharge App Performance

Deploy Your App Where Your Users Are, Literally.

The interactivity of frameworks like Livewire and Filament is only as real-time as the latency of your app. Scale across the globe, closer to wherever your users may be, while using Fly.io's HTTP/2 proxy using Brotli compression to ensure the fastest possible response times.

Load Balancing Mastery

Laravel Load Balancing, Uncomplicated

Launch your Laravel nodes with confidence. Fly.io’s architecture offers seamless load balancing, preparing your application for any traffic spikes without a hitch.

Discover How It Works

Security First

Fortify Your Laravel App

Build on a foundation of security with Fly.io. Encrypted and private networks, automatic SSL/TLS certificates management, and volumes that protect your data, provide the peace of mind that lets you focus on crafting impeccable Laravel applications.

Read More About Our Security Features

Power, Scalability, Flexibility

Artisans Love the Power of Our Machines

With next-level tools like Fly Machines and the Fly Machine API, leverage the flexibility of fast booting VMs, FAAS-possible infrastructure, and VM allocations for your cron and queue workers. Use the fly scale command to provision resources with one command, and scale your Laravel app to new markets while maintaining a snappy user experience.

  • Hassle-Free, Personalized Deployment

    Streamline your deployment with simple flyctl commands or a quick Github action workflow. Customize it with startup scripts and migrations, and tailor your deployment to your app's needs. The cherry on top? You can allocate VM resources for pretty much anything, like your scheduler and queue workers.

  • Create, and Securely Connect, Your Apps, in One Place

    Need database, storage, or cache apps for your Laravel app? You can deploy most kinds of apps at Fly.io, including these! They'll be connected using Fly.io's secure private networks so you can rest assured that your data is protected every step of the way.

  • Observability Tools in Your Dashboard

    Fly.io's also got your app health covered with observability tool access for live logs, error monitoring, and app metrics. Access all these through one place: your Fly.io dashboard.

Deploy a Laravel app to Fly.io: the actual flow

The flow is three commands: install flyctl, run fly launch, then fly deploy for every change after that.

Install the CLI:

curl -L https://fly.io/install.sh | sh

flyctl is the only tool you need locally. Authenticate with fly auth login once.

Kick off your first deploy from your Laravel project root:

cd path/to/your/laravel/app
fly launch

fly launch detects Laravel by reading composer.json and artisan, picks a region close to you, and writes a small set of files to your repo:

  • A Dockerfile that runs nginx in front of php-fpm.
  • A .fly/ directory with nginx and php-fpm config files used by the Dockerfile at boot.
  • A fly.toml with an http_service block that maps your container's PORT to the edge proxy.
  • A .github/workflows/fly-deploy.yml ready for CI/CD, plus a .dockerignore.

The wizard offers to provision a Postgres database. Accept it for now; you can wire up MPG explicitly later (see below). The launcher also generates an APP_KEY with php artisan key:generate and stores it as a Fly secret, so the app boots clean on first deploy.

For every change after the first deploy, run fly deploy. That rebuilds the image, pushes it to our registry, and rolls a new version onto your Machines with no downtime.

The older fly-apps/fly-laravel Composer package is an alternative on-ramp that bundles MySQL, Redis, queue, and scheduler setup behind a few fly-laravel subcommands, but it hasn't seen updates since August 2023. Stick with fly launch for new projects; reach for the package only if you're already using it.

Run migrations on every deploy

Wire migrations into [deploy] in fly.toml so they apply automatically before each new revision takes traffic:

[deploy]
  release_command = "php artisan migrate --force"

release_command runs on a temporary Machine with the production environment, including DATABASE_URL, before any web Machine accepts traffic. If a migration fails, the deploy aborts and your previous version keeps serving requests. The --force flag is required because artisan migrate refuses to run in production without it.

For ad-hoc Artisan commands against the running app (one-off maintenance scripts, cache:clear, debugging), open a shell on a Machine:

fly ssh console -C "php artisan tinker"

That gives you a real Tinker session against your production database, queues, and cache. Be careful what you type; the same access that lets you debug also lets you User::truncate().

Attach Managed Postgres

The recommended path is Fly.io Managed Postgres. MPG handles automatic backups and recovery, high availability with automatic failover, performance monitoring, resource scaling, and encryption of data at rest and in transit. We no longer support the older unmanaged fly postgres path; MPG is the supported way to run Postgres on Fly.io.

Provision a cluster and attach it:

fly mpg create
fly mpg list
fly mpg attach your-cluster-id -a your-app-name

fly mpg create walks through plan, region, and volume size. fly mpg list shows the cluster ID once it's ready. fly mpg attach writes a DATABASE_URL secret onto your app, reboots it, and uses a PGBouncer-pooled URL so connection counts stay sane under load.

Tell Laravel to use the Postgres driver in config/database.php by switching the default connection:

'default' => env('DB_CONNECTION', 'pgsql'),

Set DB_CONNECTION=pgsql in your environment if you want to be explicit; otherwise the default kicks in. Laravel parses the DATABASE_URL secret directly, so no other config changes are needed.

Schedule jobs and run queue workers in their own process groups

The Laravel scheduler and queue workers are both background processes that shouldn't take HTTP traffic, so they belong in separate [processes] groups in fly.toml. Pin http_service to the web group so the proxy doesn't try to send requests to background workers:

[processes]
  web = "/.fly/start.sh"
  scheduler = "supercronic /etc/supercronic/crontab"
  worker = "php artisan queue:work --tries=3 --max-time=3600"

[http_service]
  internal_port = 8080
  force_https = true
  processes = ["web"]

The scheduler needs a crontab that calls php artisan schedule:run once a minute. Use supercronic instead of cron because it logs to stdout (so the output shows up in fly logs) and exits cleanly on SIGTERM:

# /etc/supercronic/crontab
* * * * * cd /var/www/html && php artisan schedule:run >> /dev/stdout 2>&1

Scale each group independently:

fly scale count web=2 scheduler=1 worker=4

The scheduler is a singleton (multiple schedulers would emit duplicate jobs), so always scale it to exactly one Machine. The worker group can scale up and down with your queue volume.

For demand-driven worker autoscaling, the fly-apps/laravel-worker package adds an fly:work Artisan command that boots and stops worker Machines based on jobs-per-Machine. Worth a look if your queue volume swings widely between peak and idle. The package is small and not heavily maintained, so treat it as a starting point you can fork rather than a finished product.

Laravel Octane on Fly Machines

Octane boots the framework once and keeps it warm in memory, so each request skips the cold-bootstrap penalty of a fresh php-fpm worker. It runs fine on Fly.io with Swoole, RoadRunner, or FrankenPHP as the server.

Replace the web process command in fly.toml with the Octane server:

[processes]
  web = "php artisan octane:start --server=swoole --host=0.0.0.0 --port=8080"

Swap --server=swoole for roadrunner or frankenphp depending on which one you've installed. The Octane docs cover the per-server install steps; Fly.io doesn't care which you pick.

The one gotcha is cold starts. If your Machines are configured to auto-stop when idle and auto-start on demand, the first request after a quiet period has to wait for the Machine to boot AND for Octane to warm up. That combined latency can exceed the proxy's default health-check grace period and return a 502.

Two fixes, depending on your traffic shape:

Keep at least one Machine always-running:

[http_service]
  auto_stop_machines = "stop"
  auto_start_machines = true
  min_machines_running = 1

Or extend the health-check grace period:

[[http_service.checks]]
  interval = "30s"
  timeout = "5s"
  grace_period = "30s"
  method = "GET"
  path = "/up"

/up is the standard Laravel 11+ health endpoint; older apps can use / or a route you define. Keeping a Machine warm is the simpler call for production traffic; the grace-period bump is fine for low-traffic apps where the wait is acceptable on the first request.

Manage APP_KEY and secrets

Fly.io secrets are encrypted at rest, injected as environment variables when each Machine boots, and trigger a rolling restart when you change them.

fly launch generates an APP_KEY for you on the first run, so you don't need to call fly secrets set for it. Confirm with:

fly secrets list

For everything else (mail credentials, Stripe keys, third-party API tokens, OAuth client secrets), use fly secrets set:

fly secrets set \
  MAIL_PASSWORD=postmark-token \
  STRIPE_SECRET_KEY=sk_live_... \
  AWS_ACCESS_KEY_ID=...

Read them in code via Laravel's env() helper or, better, through config() so config caching works:

$key = config('services.stripe.secret');

Don't put secrets in fly.toml or a committed .env. The [env] block in fly.toml is for non-sensitive values (APP_ENV, LOG_CHANNEL, feature flags); anything you wouldn't put in a Git-tracked .env file belongs in fly secrets set.

Run Laravel across regions

Laravel apps deploy in one region by default. To run in more, use fly scale count:

fly scale count 3 --region ord,fra,nrt

That places Machines in Chicago, Frankfurt, and Tokyo. Fly.io's proxy routes each incoming request to the nearest healthy Machine, so users in Europe hit Frankfurt, users in Asia hit Tokyo, and so on. Livewire roundtrips, Inertia POSTs, and Filament dashboards all benefit from the shorter latency.

The piece this leaves open is the database. A Managed Postgres cluster lives in one region with a primary and a replica for in-region high availability. There are no cross-region read replicas; reads from Frankfurt back to a Chicago primary add latency you can't hide at the app layer.

The pragmatic move is to keep MPG in the region where most writes originate, run Laravel Machines globally in front of it, and cache hot read paths. Upstash Redis (sessions, rate limiting, queue backend), Laravel's response cache, and view caching all help absorb cross-region latency for read-heavy workloads.