Laravel is a joy to develop with and a little fiddly to deploy — PHP-FPM, an Nginx front end, queue workers, the scheduler, and config caching all need attention. This guide covers a clean, containerized production deployment backed by a managed MySQL database.
The Laravel runtime model
A Laravel app in production has more moving parts than a single process:
- PHP-FPM runs your application code.
- A web server (Nginx) serves static assets and proxies PHP requests to FPM.
- A queue worker processes jobs (
php artisan queue:work). - A scheduler runs
php artisan schedule:runevery minute (via cron).
In a container world you can run these as separate services or combine the web tier into one image. Let's build the web image first.
A production Dockerfile
FROM php:8.3-fpm-alpine
RUN apk add --no-cache nginx supervisor \
&& docker-php-ext-install pdo pdo_mysql bcmath opcache
WORKDIR /var/www/html
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
COPY composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader --no-scripts
COPY . .
RUN composer dump-autoload --optimize \
&& php artisan config:cache \
&& php artisan route:cache \
&& php artisan view:cache
EXPOSE 8080
CMD ["sh", "-c", "php-fpm -D && nginx -g 'daemon off;'"]The --no-dev --optimize-autoloader flags strip dev dependencies and build a classmap for faster autoloading. The config:cache, route:cache, and view:cache commands compile your config into single fast-loading files — a meaningful production speedup. Important: once config is cached, env() only works inside config files, so make sure all runtime config reads from config().
Enable OPcache in production — it caches compiled PHP bytecode and is one of the biggest single performance wins for any PHP app.
Environment and the app key
Laravel needs APP_KEY set or it can't decrypt sessions and cookies. Generate it once and store it as a secret:
php artisan key:generate --showSet these in production:
APP_ENV=production
APP_DEBUG=false
APP_KEY=base64:...
DB_CONNECTION=mysql
LOG_CHANNEL=stderrAPP_DEBUG=false is non-negotiable — debug mode leaks stack traces and environment data. LOG_CHANNEL=stderr sends logs to stdout/stderr so your platform's log aggregation captures them.
Managed MySQL
Use a managed MySQL instance and feed Laravel a DATABASE_URL or the individual DB_* vars. Run migrations as a deploy step:
php artisan migrate --forceThe --force flag skips the interactive confirmation Laravel shows in production. Use the expand/contract pattern for zero-downtime schema changes.
Queues and the scheduler
Queue workers and the scheduler run as separate processes from your web tier.
Queue worker (a separate container/service):
php artisan queue:work --tries=3 --max-time=3600--max-time recycles the worker periodically to avoid memory creep — a known issue with long-lived PHP workers.
Scheduler — Laravel's scheduler expects a cron entry hitting it every minute:
* * * * * php /var/www/html/artisan schedule:runOn a platform with managed cronjobs you can run php artisan schedule:run on a one-minute schedule instead of maintaining your own crontab.
Deploying on PandaStack
- 1Create a MySQL database (5.7 or 8.x). Connection details are injected as env vars.
- 2Connect your repo as a container app; PandaStack detects the Dockerfile and builds it via rootless BuildKit.
- 3Set
APP_KEY,APP_ENV=production,APP_DEBUG=false, and DB vars in the dashboard. - 4Add
php artisan migrate --forceas a release command. - 5Create a cronjob running
php artisan schedule:runevery minute, and a second service forphp artisan queue:work.
You get automatic SSL, live build logs, rollbacks, and deploy history.
| Component | How it runs |
|---|---|
| Web (PHP-FPM + Nginx) | Container app |
| Database | Managed MySQL |
| Queue worker | Separate container service |
| Scheduler | Cronjob, every minute |
| Migrations | Release command |
Common pitfalls
APP_DEBUG=truein production — leaks sensitive data.- Forgetting
--forceon migrate — the deploy hangs on a confirmation prompt. - Caching config but reading
env()at runtime — returns null; read fromconfig()instead. - No queue worker — queued jobs silently never run.
- Writable
storage/andbootstrap/cache/— make sure the runtime user can write these.
References
- Laravel deployment docs: https://laravel.com/docs/12.x/deployment
- Laravel queues: https://laravel.com/docs/12.x/queues
- Laravel task scheduling: https://laravel.com/docs/12.x/scheduling
- PHP OPcache configuration: https://www.php.net/manual/en/book.opcache.php
- Official PHP Docker images: https://hub.docker.com/_/php
---
PandaStack's free tier includes container apps, a managed MySQL database, and cronjobs — everything a Laravel app needs, with automatic SSL and live logs. Connect your repo and deploy at https://dashboard.pandastack.io