Deployment

Laravel Intermediate/Advanced
Course 2 ยท Chapter 8 ยท Deployment

๐Ÿš€ Deployment

Everything built across both Laravel courses needs to run somewhere real. This final chapter covers the traditional PHP deployment model, Artisan's production commands, and a full circle back to .env from Course 1's very first chapter โ€” the same closing structure the Django course used.

Nginx + PHP-FPM: A Different Shape From Gunicorn

Django's Course 2 covered Gunicorn as a long-running WSGI application server with worker processes. PHP's traditional model is genuinely different: PHP-FPM (FastCGI Process Manager) spins up a fresh PHP process (or reuses one from a pool) to handle each individual request, then discards its state โ€” PHP has traditionally never been a long-running application process the way a Gunicorn worker is:

Traditional Model: Nginx + PHP-FPM

Nginx serves static files and proxies dynamic requests to PHP-FPM, which boots the Laravel application fresh (or from a pool) for each request โ€” no in-memory state persists between requests by default.

Laravel Octane: The Newer Alternative

Keeps the application booted in memory across requests (via Swoole or RoadRunner), closer to how Gunicorn or Node actually works โ€” a real performance option, but not the traditional default.

โš™๏ธ Artisan's Production Commands

Chapter 8 of Course 1 previewed config:cache and route:cache โ€” here's the full picture, plus one command that bundles them:

php artisan config:cache   # combine config/*.php into one cached file
php artisan route:cache    # cache the compiled route table
php artisan view:cache     # pre-compile every Blade template to plain PHP

# Or, all at once:
php artisan optimize

Django's Checklist vs Laravel's Action Command

Django: check --deploy

A diagnostic tool โ€” it reports warnings about misconfiguration (DEBUG left on, missing security settings) but doesn't change anything itself.

Laravel: optimize

An action tool โ€” it directly performs the caching optimizations (config, routes, views) rather than just warning you they're missing.

Genuinely different philosophies solving adjacent problems: Django tells you what's wrong and lets you fix it; Laravel's optimize just does the optimization work directly, no separate "now go fix it" step.

Full Circle: .env Revisited

Back to Course 1, Chapter 1 โ€” the same two settings, the same risk, in production this time:

# .env โ€” production values
APP_ENV=production
APP_DEBUG=false        # same risk as Django's DEBUG โ€” see this chapter's gotcha
APP_KEY=base64:...     # must be set โ€” Chapter 1's key:generate gotcha, still true in production

DB_CONNECTION=mysql
DB_HOST=your-production-db-host
QUEUE_CONNECTION=redis # NOT sync โ€” Chapter 5's gotcha, still true here

๐Ÿ‘ท Queue Workers Need a Process Supervisor

php artisan queue:work from Chapter 5 is a long-running process โ€” the same requirement Django's Gunicorn setup has, just for a different piece:

# /etc/supervisor/conf.d/laravel-worker.conf
[program:laravel-worker]
command=php /var/www/example-app/artisan queue:work --sleep=3 --tries=3
autostart=true
autorestart=true
numprocs=2

Supervisor (or systemd) keeps the worker running and restarts it automatically if it crashes โ€” directly parallel to how Django's Gunicorn process needs the same kind of process supervision to survive a server reboot or a crash.

Maintenance Mode: artisan down/up

A genuinely nice built-in feature worth introducing fresh here โ€” putting the whole site into a maintenance page is one command:

php artisan down --render="errors::503"
# serves a maintenance page immediately, for every visitor

# ... run migrations, deploy new code ...

php artisan up
# site is live again

A Full Deployment Sequence

Putting It All Together

php artisan down --render="errors::503"

git pull origin main
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan optimize

php artisan queue:restart   # see this chapter's gotcha โ€” critical, easy to forget

php artisan up

๐Ÿ’ป Coding Challenges

Challenge 1: Write Production .env Values

Given a .env with APP_DEBUG=true, QUEUE_CONNECTION=sync, and no APP_KEY, write the corrected production values and the command needed to generate a missing key.

Goal: Practice recognizing every environment-config risk covered across both courses in one place.

โ†’ Solution

Challenge 2: Write a Deployment Script

Write the ordered shell commands for deploying an update: enabling maintenance mode, pulling code, installing dependencies, migrating, optimizing, restarting queue workers, then disabling maintenance mode.

Goal: Practice the full deployment sequence and reasoning about the correct order.

โ†’ Solution

Challenge 3: Diagnose Stale Queue Worker Code

A deploy changes a Job's handle() method, but jobs processed after the deploy still run the OLD logic. Explain why, and what command fixes it.

Goal: Practice recognizing a deployment gotcha specific to long-running queue workers.

โ†’ Solution

โš ๏ธ Gotcha: Forgetting queue:restart After a Deploy

This one is specific to queue systems and has no real Django parallel, since Django doesn't run persistent worker processes tied to a single deploy command the way Laravel's queue workers do. A queue:work process loads your application code (including every Job class) into memory once, when it starts, and keeps running indefinitely โ€” deploying new code to disk does not make a running worker pick it up. A worker that's been running since before the deploy keeps executing the old version of handle() for every job it processes, silently, with no error indicating the code is stale. php artisan queue:restart signals every worker to finish its current job and exit gracefully โ€” your process supervisor (Supervisor/systemd) then restarts them fresh, picking up the new code. This is exactly as easy to forget as APP_DEBUG=false, just far less obvious when it happens, since everything appears to work โ€” it's just running last week's logic.

๐ŸŽ“ Course Complete

That closes Laravel Intermediate/Advanced โ€” and with it, the full Laravel project (16 chapters across both courses). Course 1 built the batteries-included fundamentals against FastAPI, Express, and Django; Course 2 layered on everything a real application needs beyond CRUD: authentication built on ownable, editable scaffolding rather than a closed framework box, a real API layer split cleanly between input validation and output shaping, a test suite whose safety net you have to opt into rather than inherit for free, decoupled events with a genuinely elegant path to background processing, a built-in queue system that solved the Django course's Celery gotcha automatically, object-level authorization by default, and finally a production deployment with its own genuinely PHP-specific traps. Across both courses, Laravel's own personality came through clearly: batteries included, but handed to you as code you're expected to read, own, and edit โ€” not a black box.