Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Building Web Apps with Laravel  Module 15 – Deployment and operations

Deploying Laravel to a Linux server

Serve Laravel 13 with nginx and PHP-FPM from public/ only, cache and migrate in a deploy script, and release with an atomic symlink switch and rollback.

  • Intermediate
  • 10 minutes
  • Examples run with PHP 8.5.11, Composer 2.10.3, Laravel 13.34.0 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 10 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Configure nginx with PHP-FPM to serve a Laravel app from the public directory only
  • Write a deploy script that caches configuration, events, routes and views, then migrates once
  • Set storage and bootstrap/cache writable and keep every other file read-only
  • Release a new version with an atomic symlink switch, and roll back the same way
  • Check a new release with the /up health route before traffic reaches it

Before you start

What you’ll learn · Before you start

What you will learn

  • Configure nginx with PHP-FPM to serve a Laravel app from the public directory only
  • Write a deploy script that caches configuration, events, routes and views, then migrates once
  • Set storage and bootstrap/cache writable and keep every other file read-only
  • Release a new version with an atomic symlink switch, and roll back the same way
  • Check a new release with the /up health route before traffic reaches it

Before you start

On this page

A Laravel app whose web server points at the project folder hands /.env, with its database password and application key, to anyone who asks. The cure is the same on every Linux server, and this lesson runs all of it on Laravel 13.34.0: put only public/ in front of the world, cache the framework, migrate the database once, and switch releases without a moment of downtime.

You serve Pathshala, a coaching-centre app, with nginx in front of PHP-FPM and watch the deny rule refuse /.env while /up answers 200. Then you run the commands a deploy needs, set the permissions, check a new release before any visitor reaches it, and make it live with an atomic symlink switch you can undo. Workers and the scheduler run as services, which the queues lesson and the scheduling lesson set up; here they are the processes a deploy must restart.

A request reaches Laravel only through the public folder

Every request to a Laravel site, whether for a page, an API route or the health check route: A cheap URL that answers 200 when the app boots and an error when it cannot. Laravel registers /up for it; load balancers, uptime monitors and deploy scripts call it to tell a healthy server from a broken one., enters through one file: public/index.php, the front controller: The one PHP file that every web request enters through, public/index.php in Laravel. It loads Composer's autoloader, builds the application and hands the request to the framework. More in “Project structure, configuration and Artisan”. The server’s document root: The one folder a web server serves files from. For a Laravel app it is public/, so the files above it, such as .env, config/ and vendor/, can never be fetched by URL. is public/, and nothing above it — not .env, not config/, not vendor/ — can be fetched. The server needs PHP 8.3 or newer with the ctype, cURL, DOM, fileinfo, filter, hash, mbstring, OpenSSL, PCRE, PDO, session, tokenizer and XML extensions, the framework’s documented minimum. Follow one request, and one for a dotfile, through the server:

Interactive · stage by stage One request, from the browser to Laravel Follow a request for /up, and a request for .env, through nginx and PHP-FPM.
One request, from the browser to Laravel, step 6 of 6. Showing Browser, nginx, PHP-FPM, Laravel, Response. In focus: Response.dotfileBrowserasks for /upnginxroot is public/PHP-FPMruns index.phpLaravelrouterResponse200 for /up

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. A request arrives. The browser, or an uptime monitor, asks the server for /up. The web server is nginx, listening on the public port.
  2. nginx serves public/ only. nginx roots the site at public/, the one folder the browser may reach. A real file there is sent back as it is.
  3. A dotfile is refused here. A request for .env or .git matches the deny rule, so nginx answers 403 itself and never reaches PHP. The secrets stay unread.
  4. Other paths reach index.php. Any path that is not a real file goes to public/index.php through the FastCGI socket, which PHP-FPM runs.
  5. Laravel decides the answer. index.php builds the app and hands the request to the router and the middleware, which produce the response.
  6. The response goes back. For /up the health route returns 200, which nginx passes back to the browser. A broken boot would return 500 instead.

The nginx server block is what enforces that path. Laravel’s deployment guide gives the first block below as a starting point, here adapted to Pathshala; the second is the deploy’s own check:

/etc/nginx/sites-available/pathshala CONF · nginx-pathshala.conf
# The public site: visitors reach Pathshala only through this server, and only public/ is on the web.
server {
    listen 80;
    listen [::]:80;
    server_name pathshala.example.in;
    root /srv/pathshala/current/public;

    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";

    index index.php;
    charset utf-8;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    error_page 404 /index.php;

    location ~ ^/index\.php(/|$) {
        fastcgi_pass unix:/run/php/php8.5-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
        fastcgi_hide_header X-Powered-By;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}

# The deploy's own check: the same app on the loopback address only, so nothing outside this machine reaches it.
# It serves the release the next symlink points at, so deploy.sh can ask it for /up before the switch.
server {
    listen 127.0.0.1:8080;
    root /srv/pathshala/next/public;

    index index.php;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ ^/index\.php(/|$) {
        fastcgi_pass unix:/run/php/php8.5-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
        fastcgi_hide_header X-Powered-By;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}

Five parts of the first block matter. root ends in /public, so only that folder is on the web. try_files sends any path that is not a real file to index.php, which is how one PHP file answers every route. The location ~ ^/index\.php(/|$) block passes that file to PHP-FPM over a socket and hides the X-Powered-By header that would otherwise print the PHP version. Inside it, SCRIPT_FILENAME is built from $realpath_root, the root with every symbolic link resolved, which is what lets a release switch reach the very next request (the section on releases shows why). The last block, location ~ /\., denies any path that starts with a dot, except /.well-known, so a stray .env or .git folder in public/ is refused. The second server block listens on 127.0.0.1 only, so nothing outside the machine reaches it, and serves the release a deploy is about to make live.

Rooting the server at the project folder

If the document root is the project folder instead of public/, a request for /.env returns your secrets. Laravel’s guide says to send every request to public/index.php and never to move that file to the project root.

Checkpoint

Check: Why the root is public/

  1. Question 1 of 2 A new server serves Pathshala from the project folder, not public/. Which request now returns a file it should not?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: GET /.env

    public/ is the only folder that belongs on the web. Rooting the server at the project folder puts .env, config/ and vendor/ under the web root, so GET /.env returns the secrets.

  2. Question 2 of 2 What is the job of the nginx block location ~ /\.(?!well-known).* { deny all; }?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Refuse any path that starts with a dot, except under /.well-known

    The rule denies dotted paths so a file such as .env or a .git folder that ends up inside public/ is still refused, while the /.well-known exception keeps certificate validation working.

nginx and PHP-FPM, with the deny rule proven

nginx does not run PHP. It passes requests for index.php over a FastCGI: A protocol a web server uses to hand a request to a separate, long-running program and read the response back. nginx speaks it over a socket to PHP-FPM, because nginx cannot run PHP itself. socket to PHP-FPM: PHP's FastCGI Process Manager: a service that keeps a pool of PHP processes running, takes each request from the web server over FastCGI, runs the script and returns the response., a pool of PHP processes that stays running and returns the response. To show the public server block does what it claims, here it serves Pathshala on a local address: nginx -t checks the configuration first, the test to run before every start or reload, then four requests reach nginx.

nginx in front of PHP-FPM, answering four requests Python · serving_session.py
"""Serves Pathshala with nginx in front of PHP-FPM on 127.0.0.1 and sends four requests, to show that the server block
answers /up, keeps the security headers, hides X-Powered-By and refuses dotfiles such as .env. stack() runs nginx -t
first, as you would before every start or reload, then starts PHP-FPM 8.5.11 and nginx 1.30.6 with the public server
block of the Laravel deployment documentation, adapted to a local TCP FastCGI socket. No masks of our own are needed."""

from session import stack

stack("""
curl -sS -o /dev/null -w 'GET /up            -> %{http_code}\\n' http://127.0.0.1:8156/up
curl -sS -D - -o /dev/null http://127.0.0.1:8156/ | grep -iE '^(HTTP/|Server:|X-Frame-Options|X-Content-Type-Options|X-Powered-By)'
curl -sS -o /dev/null -w 'GET /.env          -> %{http_code}\\n' http://127.0.0.1:8156/.env
curl -sS -o /dev/null -w 'GET /.git/config   -> %{http_code}\\n' http://127.0.0.1:8156/.git/config
""")

Output

# Check the configuration before nginx starts, as before every reload:
$ nginx -t
nginx: the configuration file stack/nginx.conf syntax is ok
nginx: configuration file stack/nginx.conf test is successful
# PHP-FPM 8.5.11 and nginx are running on 127.0.0.1; send requests:
$ curl -sS -o /dev/null -w 'GET /up            -> %{http_code}\n' http://127.0.0.1:8156/up
GET /up            -> 200
$ curl -sS -D - -o /dev/null http://127.0.0.1:8156/ | grep -iE '^(HTTP/|Server:|X-Frame-Options|X-Content-Type-Options|X-Powered-By)'
HTTP/1.1 200 OK
Server: nginx/1.30.6
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
$ curl -sS -o /dev/null -w 'GET /.env          -> %{http_code}\n' http://127.0.0.1:8156/.env
GET /.env          -> 403
$ curl -sS -o /dev/null -w 'GET /.git/config   -> %{http_code}\n' http://127.0.0.1:8156/.git/config
GET /.git/config   -> 403

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 serving_session.py

/up answered 200, so the app booted. The response carried X-Frame-Options and X-Content-Type-Options, the two headers the server block adds, and no X-Powered-By, because fastcgi_hide_header removed it. Both /.env and /.git/config answered 403: the deny rule refused them before PHP ever saw the request. FrankenPHP, a PHP application server written in Go, can serve the same app with frankenphp php-server -r public/ and is the other server the guide documents; its -r public/ keeps the first rule, only public/ on the web.

Key idea

Put only public/ in front of the world and deny dotted paths, and the files that hold your secrets cannot be fetched, whichever server runs PHP.

Checkpoint

Check: nginx, PHP-FPM and the deny rule

  1. Question 1 of 2 In the recorded run, GET / returned X-Frame-Options and X-Content-Type-Options but no X-Powered-By. Why is X-Powered-By absent?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: fastcgi_hide_header X-Powered-By removed it

    fastcgi_hide_header X-Powered-By drops the header PHP adds, so the response does not name the PHP version, while the two security headers the block adds are still sent.

  2. Question 2 of 2 What does nginx do with a request for a Blade-rendered page such as /dashboard?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: try_files finds no such file, so it passes /index.php to PHP-FPM over the FastCGI socket

    try_files $uri $uri/ /index.php sends any path that is not a real file to index.php, which nginx runs through PHP-FPM, so Laravel's router answers /dashboard.

A deploy script installs for production, caches and migrates once

A deploy does its work in a fixed order: install the packages for production, cache the framework, back up and migrate the database once, check the new release, and only then make it live. The install is composer install --no-dev --optimize-autoloader: it brings in the 76 packages the app runs on, leaves out the 33 development packages, such as PHPUnit and Pint, and dumps a class map, so the autoloader finds each known class in one lookup instead of searching the source folders. The next two commands run on a clone of Pathshala:

Cache the framework, then migrate without a prompt Python · deploy_steps_session.py
"""The commands a deploy script runs after it fetches the new code: cache the configuration, events, routes and views
in one command, then run any new migrations with `--force`, which skips the confirmation a production run would
otherwise ask for. Run on a clone of Pathshala; the database file is recreated first so the migrations actually run,
and the caches are cleared at the end so the clone is left as it was found."""

from session import run

run("""
php artisan optimize
rm database/database.sqlite
touch database/database.sqlite
php artisan migrate --force
php artisan optimize:clear
""", app=True)

Output

$ php artisan optimize
   INFO  Caching framework bootstrap, configuration, and metadata.

  config ................................................................ DONE
  events ................................................................ DONE
  routes ................................................................ DONE
  views ................................................................. DONE
$ rm database/database.sqlite
$ touch database/database.sqlite
$ php artisan migrate --force
   INFO  Preparing database.

  Creating migration table .............................................. DONE

   INFO  Running migrations.

  0001_01_01_000000_create_users_table .................................. DONE
  0001_01_01_000001_create_cache_table .................................. DONE
  0001_01_01_000002_create_jobs_table ................................... DONE
$ php artisan optimize:clear
   INFO  Clearing cached bootstrap files.

  config ................................................................ DONE
  cache ................................................................. DONE
  compiled .............................................................. DONE
  events ................................................................ DONE
  routes ................................................................ DONE
  views ................................................................. DONE

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 deploy_steps_session.py

php artisan optimize caches the configuration, the event-to-listener map, the routes and the compiled views in one step, so each request skips that work. Once the configuration is cached, the .env file is no longer read, so a call to env() outside the config files returns null for a .env variable, which the configuration lesson covers. migrate --force runs pending migrations without the confirmation a production environment would otherwise ask for. The script below runs optimize first, so a configuration that does not load stops the deploy before anything touches the database, and it takes a mysqldump copy just before the migration. Around these steps it fetches the code, links the shared state, builds the assets and checks the release:

deploy.sh: build one release, check it, switch to it Shell · deploy/deploy.sh
#!/usr/bin/env bash
# Deploy Pathshala to /srv/pathshala: build a new release directory, check it, then switch to it in one step.
# Run on the server by the deploy user. nginx serves current/ to visitors and next/ on 127.0.0.1:8080 only,
# for the check in step 6 (both server blocks are in nginx-pathshala.conf).
set -euo pipefail

APP=/srv/pathshala
SHARED="$APP/shared"
RELEASE="$APP/releases/$(date -u +%Y%m%d%H%M%S)"

# 1. Fetch the new code into its own release directory (git archive, rsync or a CI artefact).
mkdir -p "$RELEASE"
git --work-tree="$RELEASE" --git-dir="$APP/repo.git" checkout -f main

# 2. Link the state that survives a release: the environment file and the storage directory.
ln -sfn "$SHARED/.env" "$RELEASE/.env"
rm -rf "$RELEASE/storage"
ln -sfn "$SHARED/storage" "$RELEASE/storage"

# 3. Install for production and build the front-end assets (or copy them from CI). One command per line: set -e
#    does not stop the script when a command before the last && of a list fails.
cd "$RELEASE"
composer install --no-dev --optimize-autoloader --no-interaction
npm ci
npm run build

# 4. Cache configuration, events, routes and views. A configuration that does not load stops the deploy here,
#    before anything touches the database.
php artisan optimize

# 5. Back up the database, then migrate once. Pathshala's database is MySQL, and the dump goes to the shared
#    folder, outside every release. --single-transaction copies the InnoDB tables in one consistent state without
#    blocking the app; --no-tablespaces spares the backup user the PROCESS privilege. The backup user's password
#    is in an option file that only the deploy user can read.
mkdir -p "$SHARED/backups"
mysqldump --defaults-extra-file="$SHARED/backup.cnf" --single-transaction --no-tablespaces pathshala \
    > "$SHARED/backups/$(basename "$RELEASE").sql"
php artisan migrate --force

# 6. Check the release before any visitor reaches it: point next at it and fetch /up from the server that
#    listens on 127.0.0.1:8080. An error status such as 500, or no answer within 10 seconds, stops the deploy
#    here, and current keeps serving the release that works.
ln -sfn "$RELEASE" "$APP/next"
curl -fsS --max-time 10 http://127.0.0.1:8080/up > /dev/null

# 7. Switch the live symlink in one atomic step, so no request sees a half-built release.
ln -sfn "$RELEASE" "$APP/current.tmp"
mv -Tf "$APP/current.tmp" "$APP/current"

# 8. Restart the long-running processes (the queue workers, and Reverb or Octane where they run) on the new code.
php artisan reload

# 9. Keep the five most recent releases; older ones can be removed.
ls -1dt "$APP/releases"/*/ | tail -n +6 | xargs -r rm -rf
echo "Deployed $RELEASE"

Migrating from every server

Run migrations in one deploy job, not on each app server as it starts. Several servers migrating the same database at once race each other, and a migration is not written to run twice at the same time.

Checkpoint

Check: Install, cache and migrate

  1. Question 1 of 2 Which caches does php artisan optimize build? Choose all that apply.

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • Routes
    • Compiled Blade views
    • Configuration

    The recorded run shows config, events, routes and views each reported DONE. optimize does not touch query results or application data.

  2. Question 2 of 2 Why does a deploy run php artisan migrate --force in one job rather than on every app server as it boots?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Several servers migrating the same database at once race each other, and a migration is not made to run twice together

    App servers share one database, so migrations run once per deploy. Two servers applying the same change at once can race, because a migration is written to run a single time.

Only two folders need to be writable

While it runs, Laravel writes to exactly two places: storage/, which holds logs, compiled views, sessions and file caches, and bootstrap/cache/, which holds the cached configuration and routes. Every other file can be read-only to the web server, which is both safer and the reason a release directory, once built, never has to change. The owner of the writable folders is the user PHP-FPM runs as:

Path Who writes it On the server
storage/ the app, while it runs writable by the PHP-FPM user
bootstrap/cache/ php artisan optimize and the app writable by the PHP-FPM user
app/, config/, routes/, vendor/ the deploy only read-only to the web server
.env set once per server, by hand or by a secret store read-only to the web server

APP_DEBUG belongs in that last row: it must be false in production, because a debug page prints configuration and stack traces to the visitor, which the security lesson treats in full.

chmod -R 777 storage

Making storage/ world-writable to fix a permission error lets any user on the box, including a compromised process, rewrite your logs and cached views. Give the two folders to the PHP-FPM user instead: chown -R www-data:www-data storage bootstrap/cache. Where storage/ is a link to a shared folder, as in the releases below, run it on the shared folder itself, because chown -R does not follow a link it is given.

Checkpoint

Check: Which folders are writable

  1. Question 1 of 2 Which directories must the PHP-FPM user be able to write while the app runs? Choose all that apply.

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • bootstrap/cache/
    • storage/

    Laravel writes only to storage/ and bootstrap/cache/ while it runs, so only those two belong to the PHP-FPM user; the rest stays read-only.

  2. Question 2 of 2 Why must APP_DEBUG be false in production?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A debug error page prints configuration values and stack traces to the visitor

    With APP_DEBUG true, an unhandled exception renders a page full of configuration and stack traces to the user, so production keeps it false.

A release is checked first, then made live by an atomic switch

A zero-downtime deploy: A release method in which the new version is built and prepared beside the running one and made live in one step, so no request ever meets a half-installed app or a maintenance page. release never serves a half-built app. Each version is built in its own release directory: A folder that holds one complete, built version of the app beside the earlier ones. Settings and uploads are linked in from a shared folder, so moving between versions keeps them., with .env and storage/ linked from one shared folder so settings and uploads survive. Before any visitor reaches the new release, the deploy points a second symlink, next, at it and fetches /up from the server block that listens on 127.0.0.1:8080 only. An error status or no answer stops the script there, with current still on the release that works. Only a 200 lets the deploy make the release live by an atomic symlink switch: Making a release live by replacing the symbolic link the web server serves, such as current, with one rename, so each request sees either the old release or the new one and never a mix.: ln -sfn writes a temporary link, then mv -Tf renames it over current in one step, so no request is ever served from a folder that is mid-build.

Interactive · step by step A release is checked, then made live by an atomic switch Watch release 2 answer /up through next, then current move to it in one step, then the rollback.
A release is checked, then made live by an atomic switch, step 6 of 6. Showing current, release 1, shared, release 2, next (/up answered 200). In focus: release 1.currentrelease 1sharedrelease 2next/up answered 200

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The live symlink. current is a symlink that the public server block roots at current/public. Today it points at release 1.
  2. Shared state. Both releases link .env and the storage folder from one shared folder, so settings, logs and uploads survive a release.
  3. Build the next release. The deploy unpacks release 2 in its own folder and runs composer install, optimize and migrate there. Live traffic still uses release 1.
  4. Check it before it is live. The deploy points next at release 2 and asks the loopback-only server, which serves next, for /up. A 500 would stop the deploy here, with current still on release 1.
  5. Switch in one step. Only after that 200 does ln then mv replace current in one atomic step, so no request sees a half-built folder. current now points at release 2.
  6. Roll back the same way. If release 2 misbehaves once it is live, point current back at release 1, which is untouched. A rollback is one more atomic switch.

A rollback is the same switch in reverse: point current back at the previous release, which is still on disk. Neither switch needs an nginx reload, and the next request already runs the release current names, because the public block builds SCRIPT_FILENAME from $realpath_root: nginx resolves the symlink for every request, so PHP-FPM, and the OPcache: The PHP extension that keeps compiled scripts in shared memory, so that later requests run the compiled code instead of compiling the same files again. More in “Profiling and optimizing a Laravel app” inside it, are handed a path inside the release itself. With $document_root they would get the path through current, and PHP’s realpath cache, which keeps a resolved path for 120 seconds by default, could go on mapping current to the old release for up to two minutes.

Long-running processes do not reload themselves: a queue worker: A long-running process, started with php artisan queue:work, that takes jobs from one or more queues, runs each job's handle() method and then deletes, releases or fails the job. More in “Queues and jobs: work after the response”, Reverb or Octane holds the code it booted with, so php artisan reload must tell them to restart after the switch. It runs each service’s restart command, queue:restart among them; a worker stops once the job in hand is done, and a process supervisor: A program that starts long-running processes, such as queue workers, watches them and starts them again when they exit. Supervisor and systemd are two common ones on Linux servers. More in “Batches, chains, workers and Horizon” such as Supervisor or systemd starts it again on the new code. The /up check proves that the web path boots, and watching that route from outside, all day, is where monitoring begins.

Key idea

A release is checked through next before current moves, the switch is one atomic step and its reverse is a rollback; the state that must outlive a release, .env, storage/ and the database, lives outside the release directory.

Checkpoint

Check: Check, switch and roll back

  1. Question 1 of 3 current points at release 2. The deploy points next at release 3, and /up on 127.0.0.1:8080 answers 500. Which release do visitors get at that moment?

    Choose one answer.

    Show the answer: Question 1 of 3

    Answer: Release 2, through current, which the deploy has not moved

    The check is a request to the loopback-only server that serves next. When it fails, set -e stops deploy.sh before the switch in step 7, so current keeps pointing at release 2 and no visitor ever reaches release 3.

  2. Question 2 of 3 Why build the new release in its own directory and switch a symlink, rather than overwriting the live files in place?

    Choose one answer.

    Show the answer: Question 2 of 3

    Answer: During an in-place copy, requests would hit a folder that is half-updated; a symlink switch is one atomic step

    Copying over the live files serves half-updated code during the copy. Building a new release directory and moving the symlink in one step means requests only ever see a complete release, and rollback is one more switch.

  3. Question 3 of 3 After a symlink switch the website shows new code at once, but queue workers keep running the old code. Why, and what fixes it?

    Choose one answer.

    Show the answer: Question 3 of 3

    Answer: A worker is a long-running process that holds its booted code; php artisan reload tells it to restart

    nginx resolves current for every request and hands PHP-FPM a path inside the new release, so the web path runs the new code at once; a worker boots once and keeps that code, so php artisan reload must restart it and a process monitor brings it back.

Practise

Try these on a Pathshala project, on a spare Linux box or a local virtual machine of your own, then take the self-check, which starts with the idea you have shown least.

  1. Write an nginx server block whose root is public/, with the deny-dotfiles rule. Run nginx -t, start nginx and PHP-FPM, and confirm /up is 200 and /.env is 403.
  2. Add the second server block, on 127.0.0.1:8080 and rooted at next/public, then write a deploy.sh that installs with --no-dev --optimize-autoloader, runs php artisan optimize and migrate --force, checks /up through next and only then switches current. Deploy twice, roll current back to the first release, and confirm it answers again.
  3. Break the health check on purpose: in AppServiceProvider::boot(), listen for Illuminate\Foundation\Events\DiagnosingHealth and throw an exception. Deploy again. Composer, optimize and migrate all succeed, because no Artisan command fires that event, but /up on the check server answers 500, so the deploy stops and current stays on the release that works. Remove the listener and deploy once more.
  4. Link storage/ from the shared folder, give the shared storage/ and each release’s bootstrap/cache/ to the PHP-FPM user, and leave the rest read-only. Confirm the app still writes its log.

Practice that adapts to you

Practise this lesson

11 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 11 What is the document root of a correctly deployed Laravel app?

    Choose one answer.

    Show the answer: Question 1 of 11

    Answer: The public/ directory

    The web root is public/, so the files that hold secrets, which live above it, cannot be fetched.

  2. Question 2 of 11 A teammate asks why Laravel does not just put index.php in the project root to simplify the server config. What is the strongest reason not to?

    Choose one answer.

    Show the answer: Question 2 of 11

    Answer: It would place .env and config/ beside index.php, under the web root, where they could be fetched

    Keeping index.php in public/ keeps the web root at public/, so .env and config stay unreachable. That separation is the whole point of the public/ folder.

  3. Question 3 of 11 How does nginx run the PHP for a Laravel request?

    Choose one answer.

    Show the answer: Question 3 of 11

    Answer: It passes the request to PHP-FPM over a FastCGI socket

    nginx forwards index.php to PHP-FPM over a FastCGI socket, and PHP-FPM runs the application and returns the response.

  4. Question 4 of 11 A load balancer requests /up and gets 500. What does that tell it?

    Choose one answer.

    Show the answer: Question 4 of 11

    Answer: The app on that instance failed to boot, or one of its health checks threw, so it should not receive traffic

    /up answers 200 on a clean boot and 500 when the boot throws, so a 500 tells the balancer to route around that instance.

  5. Question 5 of 11 Why does the deploy cache, migrate and check the new release before it switches the symlink, not after?

    Choose one answer.

    Show the answer: Question 5 of 11

    Answer: So visitors reach the new release only once it is cached, migrated and has answered /up with 200

    Caching, migrating and checking the new release before the switch means traffic only ever reaches a release that is complete and boots, so there is no window of half-ready code.

  6. Question 6 of 11 What does the --no-dev flag on composer install do during a deploy?

    Choose one answer.

    Show the answer: Question 6 of 11

    Answer: It installs only the packages the app needs to run, leaving out development tools

    --no-dev installs the runtime packages and leaves the development tools out, which is why a production install is smaller than a development one.

  7. Question 7 of 11 Why does the deploy symlink storage/ to a shared folder instead of letting each release keep its own?

    Choose one answer.

    Show the answer: Question 7 of 11

    Answer: So logs, sessions and uploaded files survive a release and a rollback

    storage/ holds uploads, sessions and logs that must persist across releases, so it is linked from a shared folder rather than rebuilt empty in each release directory.

  8. Question 8 of 11 After a deploy, Laravel cannot write storage/logs/laravel.log because permission is denied. Which fix keeps the server safe?

    Choose one answer.

    Show the answer: Question 8 of 11

    Answer: Give the shared storage/ folder to the PHP-FPM user with chown -R

    Laravel writes only to storage/ and bootstrap/cache/, so the fix is ownership of those folders by the PHP-FPM user, never a wider mode. In the release layout storage/ is a link, so the chown goes to the shared folder it points at.

  9. Question 9 of 11 A release answered /up with 200 and went live, but its fee page now answers 500. What is the fastest safe way back?

    Choose one answer.

    Show the answer: Question 9 of 11

    Answer: Point the current symlink at the previous release, which is still on disk

    /up proves the release boots, not that every page works. Because each release stays in its own directory, a rollback is one more atomic symlink switch to the previous, known-good release.

  10. Question 10 of 11 Why does the very next request after the switch run the new release, with no nginx reload?

    Choose one answer.

    Show the answer: Question 10 of 11

    Answer: The server block passes $realpath_root, so nginx resolves current for every request

    $realpath_root is the root with every symbolic link resolved, worked out for the current request. With it, PHP receives the new release's real path at once; with $document_root, PHP's realpath cache could keep mapping current to the old release for up to two minutes.

  11. Question 11 of 11 A colleague drops the next symlink and its server block, fetches /up right after the switch instead, and switches back when it fails. What do visitors lose?

    Choose one answer.

    Show the answer: Question 11 of 11

    Answer: Their requests in the seconds between the switch and the switch back reach the broken release

    A check after the switch can only limit the damage: for the seconds the request and the switch back take, visitors reach the broken release. The check through next runs before current moves, so a release that fails it never serves a visitor.

Interview questions

Warm-up (fresher to 2–5 years): why must a Laravel app’s web server root point at public/, and not at the project folder?

Because everything outside public/ is meant to stay off the web. The project folder holds .env with the application key and database password, the config/ files, composer.json, the vendor/ packages and your source. If the document root is the project folder, a request for /.env is answered with that file, so anyone can read your secrets. public/ holds only index.php and the files a browser may fetch, such as built CSS and images. public/index.php is the one entry point: the web server sends every path that is not a real file in public/ to it, and Laravel decides the response. The deployment guide adds a second guard inside public/: a location block that denies any dotted path except /.well-known, so a stray public/.env or .git is refused with 403 as well.

Warm-up (fresher to 2–5 years): what does Laravel’s /up route return, and what is it for?

/up is a health route the skeleton registers in bootstrap/app.php. A request to it returns HTTP 200 when the application boots without throwing, and 500 when the boot fails, so an uptime monitor, a load balancer or an orchestrator can tell a healthy instance from a broken one with one cheap request. When the route runs, Laravel also dispatches a DiagnosingHealth event, so you can add your own checks in a listener: query the database, ping the cache, and throw from the listener if something is wrong, which turns the 200 into a 500. Keep those checks light, because the route is hit often. You can rename the URL from /up in bootstrap/app.php. A deploy script can fetch /up from a new release before the switch, so a release that boots broken is caught before traffic reaches it.

Two questions an interviewer asks next, about what a symlink switch does not do on its own for the database and the workers:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    Pathshala's deploy script runs migrate --force before it switches the current symlink. One release renames the batches.title column to name. During that deploy, every batch page fails with a database error for about fifteen seconds, although the new code is correct. Explain the failure and change the release so that no request fails.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A release changes the wording of Pathshala's fee reminder, which a queued job sends. The deploy switched the symlink and ran php artisan reload, whose queue task printed DONE. The web pages show the new wording at once, yet reminders sent an hour later still carry the old text. What do you check, and what do you change?

    Model answer, Pro. First step and follow-ups

Flashcards · 8 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. Why must the web server's document root be the public folder?

    Every request should enter through public/index.php, and nothing above public/ can then be fetched. If the root is the project folder, a request for /.env hands out the database password and the application key.

  2. Which parts of the nginx server block in Laravel's deployment guide matter most?

    root ending in /public; try_files sending any path that is not a real file to index.php; a location for index.php that passes it to PHP-FPM and hides X-Powered-By; SCRIPT_FILENAME built from $realpath_root; and a rule that denies dotted paths such as /.env and /.git.

  3. What did four requests to the local nginx and PHP-FPM setup show?

    /up answered 200 with the block's X-Frame-Options and X-Content-Type-Options headers and no X-Powered-By, while /.env and /.git/config answered 403: the deny rule refused them before PHP saw the request. Run nginx -t before every start or reload.

  4. What does composer install --no-dev --optimize-autoloader do on a production server?

    It installs the 76 packages the app runs on, leaves out the 33 development packages such as PHPUnit and Pint, and dumps a class map, so the autoloader finds each known class in one lookup.

  5. Why do migrations run in one deploy job and not on each app server as it starts?

    Several servers migrating the same database at once race each other, and a migration is not written to run twice at the same time. migrate --force runs them without the production prompt, after optimize has proved the configuration loads.

  6. Which two folders does Laravel need to write to, and who should own them?

    storage/ (logs, compiled views, sessions, file caches) and bootstrap/cache/ (cached config and routes). Give them to the PHP-FPM user, for example with chown -R www-data:www-data, never make them world-writable; app, config, routes, vendor and .env stay read-only.

  7. How does a zero-downtime release go live, and how is it undone?

    It is built in its own release directory, with .env and storage linked from a shared folder. The deploy points a next symlink at it and checks /up on a loopback-only server block before the switch; only a 200 moves current, in one atomic step. A rollback points current back at the previous release.

  8. Why must php artisan reload run after the release switch?

    Queue workers, Reverb and Octane keep the code they booted with. reload runs each service's restart command, queue:restart among them, so each worker exits after its job in hand and Supervisor or systemd starts it on the new code.

Key takeaways

  • A Laravel server roots the site at public/ and denies dotted paths, so .env and other files above public/ are never reachable; nginx passes PHP to PHP-FPM over a FastCGI socket.
  • A deploy installs with --no-dev --optimize-autoloader, caches the framework with php artisan optimize, backs up the database and runs migrate --force once, not on every server.
  • Laravel writes only to storage/ and bootstrap/cache/; those two folders belong to the PHP-FPM user, every other file is read-only, and APP_DEBUG is false in production.
  • A release is built in its own directory with shared .env and storage/, checked through next on a server only the machine itself can reach, made live by an atomic symlink switch, and rolled back by switching the symlink to the previous release.
  • nginx resolves current for every request through $realpath_root, so the web path runs a new release at once, while long-running workers keep their old code until php artisan reload restarts them under a process monitor.

Terms in this lesson

atomic symlink switch
Making a release live by replacing the symbolic link the web server serves, such as current, with one rename, so each request sees either the old release or the new one and never a mix.
document root
The one folder a web server serves files from. For a Laravel app it is public/, so the files above it, such as .env, config/ and vendor/, can never be fetched by URL.
FastCGI
A protocol a web server uses to hand a request to a separate, long-running program and read the response back. nginx speaks it over a socket to PHP-FPM, because nginx cannot run PHP itself.
health check route
A cheap URL that answers 200 when the app boots and an error when it cannot. Laravel registers /up for it; load balancers, uptime monitors and deploy scripts call it to tell a healthy server from a broken one.
PHP-FPM
PHP's FastCGI Process Manager: a service that keeps a pool of PHP processes running, takes each request from the web server over FastCGI, runs the script and returns the response.
release directory
A folder that holds one complete, built version of the app beside the earlier ones. Settings and uploads are linked in from a shared folder, so moving between versions keeps them.
zero-downtime deploy
A release method in which the new version is built and prepared beside the running one and made live in one step, so no request ever meets a half-installed app or a maintenance page.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.