Back to Blog
Tutorial11 min read2026-07-03

How to Deploy a Laravel App to Production

A production Laravel deploy with PHP-FPM and Nginx in one container, queue workers, scheduler via cronjob, config caching, and a managed MySQL. The PHP-on-modern-cloud playbook.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

Laravel in containers, done right

Laravel deployments have historically meant a VPS, Forge, or shared hosting. Containerizing it is cleaner and portable, but PHP has a quirk: it's not a single long-running server like Node. You run PHP-FPM behind a web server (Nginx) and process the request lifecycle per request. Let's package that properly.

Step 1: A container with Nginx + PHP-FPM

The pragmatic approach is one container running Nginx and PHP-FPM via a process manager, or the official PHP-FPM image with a sidecar. For a single-container deploy, use a base that bundles both (FrankenPHP is increasingly popular and simplifies this dramatically):

FROM dunglas/frankenphp:1-php8.3

WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --optimize-autoloader --no-interaction
COPY . .
RUN composer dump-autoload --optimize

# Cache config/routes/views at build time
RUN php artisan config:cache && php artisan route:cache && php artisan view:cache

ENV SERVER_NAME=:8080
EXPOSE 8080
CMD ["frankenphp", "run", "--config", "/etc/caddy/Caddyfile"]

FrankenPHP serves on a port you control — bind it to the platform's port (here :8080). Caching config, routes, and views at build time is the single biggest production performance win in Laravel.

Step 2: Environment and the APP_KEY

Laravel needs APP_KEY set, plus the usual config. Set these as env vars in the dashboard — never commit .env:

APP_ENV=production
APP_DEBUG=false
APP_KEY=base64:...   # php artisan key:generate --show
DB_CONNECTION=mysql

APP_DEBUG=false in production is a security requirement — debug mode leaks stack traces and env values.

Step 3: Wire a managed MySQL

Provision a managed MySQL (5.7 or 8.x) on PandaStack. Laravel reads either discrete DB_* vars or a URL. PandaStack injects a connection string for the attached database; map it to Laravel's DB_* or use DATABASE_URL parsing:

// config/database.php (Laravel supports a url key)
'mysql' => [
    'url' => env('DATABASE_URL'),
    'driver' => 'mysql',
    // ...
],

Run migrations as a release step:

php artisan migrate --force

The --force flag is required to run migrations non-interactively in production.

Step 4: Queue workers

Laravel queues need a long-running worker process. Deploy a second container app with the worker as its start command:

php artisan queue:work --tries=3 --max-time=3600

Use --max-time so workers recycle periodically (PHP isn't great at long-lived memory). Back the queue with the managed Redis for reliability:

QUEUE_CONNECTION=redis
REDIS_URL=redis://...

Step 5: The scheduler as a cronjob

Laravel's scheduler is meant to be invoked every minute. Instead of a cron *inside* the container, use a PandaStack cronjob running every minute:

# Cronjob schedule: * * * * *
php artisan schedule:run

This is cleaner than baking cron into the app image and gives you separate logs for scheduled runs.

Step 6: Deploy

Connect the repo and push. The build runs in a rootless BuildKit K8s Job pod, the image lands in Artifact Registry, and Helm deploys it with live logs:

git push origin main

Add your domain for automatic SSL. You now have the web app, a queue worker, and a scheduler cronjob all sharing one managed MySQL and Redis.

Step 7: Storage and sessions

Containers are ephemeral — don't write user uploads to local disk. Use an object store (S3-compatible) via Laravel's filesystem config, and put sessions in Redis or the database so they survive redeploys:

SESSION_DRIVER=redis
FILESYSTEM_DISK=s3

Production checklist

  • [ ] config:cache, route:cache, view:cache at build
  • [ ] APP_DEBUG=false, APP_KEY set
  • [ ] Migrations run with --force as a release step
  • [ ] Queue worker as a separate long-running app
  • [ ] Scheduler as a per-minute cronjob
  • [ ] Sessions + cache in Redis, uploads in object storage

References

  • [Laravel — Deployment](https://laravel.com/docs/12.x/deployment)
  • [FrankenPHP documentation](https://frankenphp.dev/docs/)
  • [Laravel — Queues](https://laravel.com/docs/12.x/queues)
  • [Laravel — Task Scheduling](https://laravel.com/docs/12.x/scheduling)

---

Laravel maps neatly onto a modern container platform once you split web, worker, and scheduler. Run all three plus a managed MySQL on PandaStack's [free tier](https://dashboard.pandastack.io) and skip the VPS maintenance entirely.

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also