<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: HoudaifaDevBS</title>
    <description>The latest articles on Forem by HoudaifaDevBS (@houdaifadev).</description>
    <link>https://forem.com/houdaifadev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F895123%2F3d056a2b-6622-4d3d-91eb-6080fc616282.jpg</url>
      <title>Forem: HoudaifaDevBS</title>
      <link>https://forem.com/houdaifadev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/houdaifadev"/>
    <language>en</language>
    <item>
      <title>From Slow Requests to Scalable Background Jobs with Laravel Queues &amp; Horizon</title>
      <dc:creator>HoudaifaDevBS</dc:creator>
      <pubDate>Fri, 10 Apr 2026 13:55:16 +0000</pubDate>
      <link>https://forem.com/houdaifadev/from-slow-requests-to-scalable-background-jobs-with-laravel-queues-horizon-2b8</link>
      <guid>https://forem.com/houdaifadev/from-slow-requests-to-scalable-background-jobs-with-laravel-queues-horizon-2b8</guid>
      <description>&lt;p&gt;Almost anyone has experienced a scenario where you click a "Register / Submit" button, then stare at the screen waiting seconds — if not minutes — for a response, only to see a success message or, even worse, some errors popping up.&lt;/p&gt;

&lt;p&gt;I know, it feels frustrating. It gives a bad user experience, and honestly? Everyone will just go find another faster solution.&lt;/p&gt;

&lt;p&gt;In this article, we'll explain what's actually happening behind that loading spinner — and how you can better architect your solution to fix this once and for all.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F53gj9dgmi3z79e1x7r26.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F53gj9dgmi3z79e1x7r26.webp" alt="Queues &amp;amp; Horizon"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 1 — The Problem (The Real One)
&lt;/h2&gt;

&lt;p&gt;Let's make it concrete. Here's what a typical registration endpoint looks like in most Laravel apps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;validated&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

    &lt;span class="c1"&gt;// Send welcome email&lt;/span&gt;
    &lt;span class="nc"&gt;Mail&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WelcomeEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="c1"&gt;// Generate and store a welcome PDF&lt;/span&gt;
    &lt;span class="nv"&gt;$pdf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Pdf&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;loadView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'pdfs.welcome'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;compact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'user'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nc"&gt;Storage&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"welcome/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.pdf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$pdf&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;output&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'message'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Registration successful!'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks familiar, right?&lt;/p&gt;

&lt;p&gt;The problem is that everything here runs sequentially, inside the same request. Laravel won't return that success response until every single line finishes executing — the email, the Slack call, the PDF, all of it.&lt;/p&gt;

&lt;p&gt;So if your mail provider takes 2 seconds and the PDF takes another second, your user just waited 3+ seconds to see "Registration successful". And that's on a good day, with no timeouts or failures.&lt;/p&gt;

&lt;p&gt;This pattern shows up everywhere:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sending emails or SMS after an action&lt;/li&gt;
&lt;li&gt;Calling third-party APIs (payment gateways, webhooks, analytics)&lt;/li&gt;
&lt;li&gt;Generating reports or exports&lt;/li&gt;
&lt;li&gt;Processing uploaded files or images&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of these have one thing in common — &lt;strong&gt;the user doesn't need to wait for them&lt;/strong&gt;. They just need to know their action was received. The rest can happen in the background.&lt;/p&gt;

&lt;p&gt;That's exactly what we're going to fix.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 2 — How Laravel Queues Work
&lt;/h2&gt;

&lt;p&gt;Before jumping into code, let's build a quick mental model — I promise this is the only "theory" section.&lt;/p&gt;

&lt;p&gt;When a user hits your endpoint, Laravel normally does everything in that same request lifecycle — sends the email, calls the API, generates the PDF — and only then returns a response. The user waits for all of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Queues flip that completely.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of doing the heavy work immediately, you push a job onto a queue — think of it as a to-do list — and return the response right away. A separate process called a &lt;strong&gt;worker&lt;/strong&gt; is running in the background, picking jobs off that list and executing them one by one, completely outside the user's request.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fko3hojo1dk2sljdjhyqe.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fko3hojo1dk2sljdjhyqe.webp" alt="Queue Diagram"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three actors to keep in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Job&lt;/strong&gt; — the class that contains the actual work (&lt;code&gt;SendWelcomeEmail&lt;/code&gt;, &lt;code&gt;GenerateInvoice&lt;/code&gt;...)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queue&lt;/strong&gt; — the list where jobs wait (we'll use Redis, more on that in a second)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Worker&lt;/strong&gt; — the background process that pulls jobs from the queue and runs them (&lt;code&gt;php artisan queue:work&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's really it. The rest is just configuration and knowing which scenarios to apply this to — which is exactly what we'll cover next.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Why Redis and not the database driver?&lt;/strong&gt; Laravel supports multiple queue drivers — &lt;code&gt;database&lt;/code&gt;, &lt;code&gt;Redis&lt;/code&gt;, &lt;code&gt;SQS&lt;/code&gt;, and others. The database driver works fine for getting started, but Redis is faster and lighter on your DB, and it's what you'll realistically use in production. So we'll go with Redis from the start and skip the detour.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Part 3 — Setting Up Redis &amp;amp; Queue Config
&lt;/h2&gt;

&lt;p&gt;Alright, enough theory — let's get our hands dirty.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install Redis
&lt;/h3&gt;

&lt;p&gt;On Ubuntu / your VPS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;redis-server &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;redis-server
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start redis-server

redis-cli ping        &lt;span class="c"&gt;# If you got PONG — you're good.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Find below the installation instructions for each platform:&lt;br&gt;
&lt;a href="https://redis.io/docs/latest/operate/oss_and_stack/install/archive/install-redis/" rel="noopener noreferrer"&gt;https://redis.io/docs/latest/operate/oss_and_stack/install/archive/install-redis/&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Install the Laravel Redis Package
&lt;/h3&gt;

&lt;p&gt;Laravel needs one extra package to talk to Redis:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require predis/predis
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Configure Your &lt;code&gt;.env&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Two changes only in &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;QUEUE_CONNECTION=redis           # switch to the redis driver

REDIS_CLIENT=predis              # use the predis package installed
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, Laravel uses &lt;code&gt;sync&lt;/code&gt; as the queue driver — meaning jobs run immediately, synchronously, defeating the whole purpose. Switching to &lt;code&gt;redis&lt;/code&gt; is what actually enables the background behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  Quick Sanity Check
&lt;/h3&gt;

&lt;p&gt;Before writing a single job, let's confirm everything is wired correctly. Run your worker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan queue:work
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INFO  Processing jobs from the [default] queue.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No errors? Perfect. Leave that terminal open — that's your worker listening for jobs. Open a second terminal for the next steps.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Heads up for production:&lt;/strong&gt; Running &lt;code&gt;queue:work&lt;/code&gt; manually is fine for local development, but on your server you need a process manager to keep it alive — if it crashes, your jobs just pile up with nobody processing them. We'll cover that with Supervisor in the Horizon section.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Part 4 — Your First Real Job
&lt;/h2&gt;

&lt;p&gt;Remember that messy controller from Part 1? Let's start fixing it — one job at a time.&lt;/p&gt;

&lt;p&gt;We'll tackle the welcome email first since it's the most common and the cleanest example to learn the pattern with.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan make:job SendWelcomeEmail        &lt;span class="c"&gt;# Create the Job&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates &lt;code&gt;app/Jobs/SendWelcomeEmail.php&lt;/code&gt;. Open it up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SendWelcomeEmail&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ShouldQueue&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Dispatchable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;InteractsWithQueue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Queueable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;SerializesModels&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;User&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Mail&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WelcomeEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things to notice here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ShouldQueue&lt;/code&gt;&lt;/strong&gt; — This interface is what tells Laravel "don't run this now, push it to the queue."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;SerializesModels&lt;/code&gt;&lt;/strong&gt; — handles serializing and deserializing your Eloquent models automatically so that you can pass &lt;code&gt;$user&lt;/code&gt; directly without worrying about it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Dispatch It From Your Controller
&lt;/h3&gt;

&lt;p&gt;Now go back to your controller and replace the &lt;code&gt;Mail::to(...)&lt;/code&gt; line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before&lt;/span&gt;
&lt;span class="nc"&gt;Mail&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WelcomeEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// After&lt;/span&gt;
&lt;span class="nc"&gt;SendWelcomeEmail&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it — one line. Your controller doesn't care when or how the email gets sent anymore — it just says "handle this" and moves on.&lt;/p&gt;

&lt;p&gt;Your registration endpoint now looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;validated&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="nc"&gt;SendWelcomeEmail&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'message'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Registration successful!'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response comes back instantly. The email is sent in the background by your worker.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Want to delay a job?&lt;/strong&gt; You can dispatch a job with a delay super easily:&lt;/p&gt;


&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;SendWelcomeEmail&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Useful for things like "send a follow-up email 10 minutes after registration".&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's the core pattern — everything else you'll do with queues is just a variation of this. Create a job, move the logic into &lt;code&gt;handle()&lt;/code&gt;, and dispatch it. Let's now apply this to three real-world scenarios you'll actually run into.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 5 — Real World Scenarios
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Scenario A — Calling a Third-Party API (Slack, SMS, Webhooks)
&lt;/h3&gt;

&lt;p&gt;Third-party APIs are the #1 culprit for slow responses. You have zero control over their response time — and they can fail.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan make:job NotifyAdminOnSlack   &lt;span class="c"&gt;# create the job&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NotifyAdminOnSlack&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ShouldQueue&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Dispatchable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;InteractsWithQueue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Queueable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;SerializesModels&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$tries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$backoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;User&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Http&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'services.slack.webhook'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'text'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"New user registered: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice &lt;code&gt;$tries = 3&lt;/code&gt; and &lt;code&gt;$backoff = 10&lt;/code&gt; — if Slack is down or slow, Laravel will automatically retry the job 3 times, waiting 10 seconds between each attempt. Your user never sees any of this.&lt;/p&gt;

&lt;p&gt;Dispatch both jobs from your controller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;SendWelcomeEmail&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;NotifyAdminOnSlack&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'message'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Registration successful!'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two background jobs, zero waiting.&lt;/p&gt;




&lt;h3&gt;
  
  
  Scenario B — Generating a PDF or Report on Demand
&lt;/h3&gt;

&lt;p&gt;This one is slightly different — the user actually needs the result, they just don't need to wait for it right now.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan make:job GenerateWelcomePdf       &lt;span class="c"&gt;# create the job&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GenerateWelcomePdf&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ShouldQueue&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Dispatchable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;InteractsWithQueue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Queueable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;SerializesModels&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;User&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$pdf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Pdf&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;loadView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'pdfs.welcome'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'user'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;
        &lt;span class="nc"&gt;Storage&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s2"&gt;"welcome/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.pdf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;$pdf&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;output&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// Notify the user it's ready&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PdfReadyNotification&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pattern here is: &lt;strong&gt;generate → store → notify&lt;/strong&gt;. The user gets an instant response on registration, and a notification (email, in-app, whatever you prefer) once their PDF is actually ready. Clean and professional.&lt;/p&gt;




&lt;h3&gt;
  
  
  Scenario C — Processing a Bulk CSV Import
&lt;/h3&gt;

&lt;p&gt;This is where queues really shine. Importing 5,000 rows in a single request is a recipe for timeouts and misery.&lt;/p&gt;

&lt;p&gt;Instead of one giant job, chunk your data into smaller jobs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan make:job ImportUserRow            &lt;span class="c"&gt;# create the job&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ImportUserRow&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ShouldQueue&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Dispatchable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;InteractsWithQueue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Queueable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;SerializesModels&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;updateOrCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'email'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="s1"&gt;'phone'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'phone'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, in your import controller, loop through the CSV and dispatch one job per row (or per chunk):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'str_getcsv'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'csv'&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$rows&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;ImportUserRow&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="s1"&gt;'message'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Import started! We will notify you when it\'s done.'&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;5,000 rows? 50,000 rows? Doesn't matter — your endpoint returns in milliseconds, and your workers chew through the data in the background at their own pace.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Need all jobs to finish before doing something?&lt;/strong&gt; Laravel has &lt;code&gt;Bus::batch()&lt;/code&gt; for exactly this — group jobs together, track their progress, and run a callback when they all complete. Worth a separate deep-dive on its own.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Three scenarios, one pattern. Create the job, handle the work, dispatch and forget. Next up — what happens when things go wrong?&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 6 — Handling Failures Like a Pro
&lt;/h2&gt;

&lt;p&gt;Background jobs fail. Mail providers go down, APIs timeout, PDFs throw exceptions — it happens. The difference between a solid implementation and a fragile one is how you handle it when things go wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  Set Up the Failed Jobs Table
&lt;/h3&gt;

&lt;p&gt;First, make sure you have the failed jobs table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan queue:failed-table             &lt;span class="c"&gt;# create the migration file&lt;/span&gt;
php artisan migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Laravel will now store any failed job in this table instead of silently dropping it — including the exception, the payload, and when it failed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Retries, Timeouts &amp;amp; Backoff
&lt;/h3&gt;

&lt;p&gt;You can control failure behavior directly on the job class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SendWelcomeEmail&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ShouldQueue&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Dispatchable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;InteractsWithQueue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Queueable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;SerializesModels&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$tries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;// retry up to 3 times&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;// kill the job if it runs longer than 30s&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$backoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;// wait 15 seconds between retries&lt;/span&gt;

    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or if you want exponential backoff — waiting longer after each failed attempt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;backoff&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="c1"&gt;// 10s after 1st fail, 30s after 2nd, 60s after 3rd&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is great for flaky third-party APIs — instead of hammering them every 10 seconds, you give them room to recover.&lt;/p&gt;

&lt;h3&gt;
  
  
  The &lt;code&gt;failed()&lt;/code&gt; Method
&lt;/h3&gt;

&lt;p&gt;When a job exhausts all its retries, Laravel calls the &lt;code&gt;failed()&lt;/code&gt; method on it — if you define one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;failed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;\Throwable&lt;/span&gt; &lt;span class="nv"&gt;$exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Notify yourself, log it, alert the user...&lt;/span&gt;
    &lt;span class="nc"&gt;Log&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"SendWelcomeEmail failed for user &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'error'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$exception&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;EmailFailedNotification&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Never leave this empty on jobs that matter. Silently failing jobs are worse than crashing — at least a crash is loud.&lt;/p&gt;

&lt;h3&gt;
  
  
  Managing Failed Jobs via CLI
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan queue:failed       &lt;span class="c"&gt;# See all failed jobs&lt;/span&gt;
php artisan queue:retry 5      &lt;span class="c"&gt;# Retry a specific failed job by its ID&lt;/span&gt;
php artisan queue:retry all    &lt;span class="c"&gt;# Retry all failed jobs at once&lt;/span&gt;
php artisan queue:forget 5     &lt;span class="c"&gt;# Delete a specific failed job&lt;/span&gt;
php artisan queue:flush        &lt;span class="c"&gt;# Clear the entire failed jobs table&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;ShouldBeUnique&lt;/code&gt; — Prevent Duplicate Jobs
&lt;/h3&gt;

&lt;p&gt;Sometimes the same job gets dispatched multiple times — user double-clicks, a webhook fires twice, whatever. For jobs where duplicates are a real problem, implement &lt;code&gt;ShouldBeUnique&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GenerateWelcomePdf&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ShouldQueue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ShouldBeUnique&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$uniqueFor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// lock for 1 hour&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;uniqueId&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// one job per user at a time&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Laravel will skip dispatching if a job with the same &lt;code&gt;uniqueId()&lt;/code&gt; is already in the queue. Clean solution, zero extra code on the dispatch side.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Golden rule:&lt;/strong&gt; always define &lt;code&gt;$tries&lt;/code&gt;, &lt;code&gt;$timeout&lt;/code&gt;, and &lt;code&gt;failed()&lt;/code&gt; on any job that touches a third-party service or generates critical data. The 2 minutes it takes to add them will save you hours of debugging in production.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Part 7 — Laravel Horizon
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;queue:work&lt;/code&gt; gets the job done locally, but in production, you need visibility — which jobs are running, how long they're taking, what's failing, and whether your workers are keeping up. That's exactly what Horizon gives you.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install Horizon
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require laravel/horizon
php artisan horizon:install            &lt;span class="c"&gt;# install and create migration files&lt;/span&gt;
php artisan migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This publishes a config file at &lt;code&gt;config/horizon.php&lt;/code&gt; and sets up the dashboard.&lt;/p&gt;

&lt;h3&gt;
  
  
  Basic Configuration
&lt;/h3&gt;

&lt;p&gt;Open &lt;code&gt;config/horizon.php&lt;/code&gt;. The part you care about most is &lt;code&gt;environments&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'environments'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'production'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'supervisor-1'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'maxProcesses'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'balanceMaxShift'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'balanceCooldown'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="s1"&gt;'local'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'supervisor-1'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'maxProcesses'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Horizon uses supervisors to manage your workers internally — don't confuse these with the system-level Supervisor we'll set up in a moment. These are Horizon's own worker groups.&lt;/p&gt;

&lt;p&gt;You can also assign specific jobs to specific queues and control priority:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'supervisor-1'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'connection'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'redis'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'queue'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'critical'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'default'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'low'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="s1"&gt;'balance'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'auto'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'maxProcesses'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Jobs on the &lt;code&gt;critical&lt;/code&gt; queue get processed before &lt;code&gt;default&lt;/code&gt;, which gets processed before &lt;code&gt;low&lt;/code&gt;. Useful when you want invoice processing to always beat bulk imports.&lt;/p&gt;

&lt;p&gt;Dispatch to a specific queue like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;GenerateInvoice&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;onQueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'critical'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;ImportUserRow&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;onQueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'low'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Protect the Dashboard
&lt;/h3&gt;

&lt;p&gt;The Horizon dashboard runs at &lt;code&gt;/horizon&lt;/code&gt; and exposes sensitive data — failed jobs, job payloads, throughput. Lock it down:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/Providers/HorizonServiceProvider.php&lt;/span&gt;
&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Gate&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'viewHorizon'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;in_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'you@yourdomain.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only whitelisted emails can access the dashboard in production. Simple, effective.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reading the Dashboard
&lt;/h3&gt;

&lt;p&gt;Once Horizon is running (&lt;code&gt;php artisan horizon&lt;/code&gt;), head to &lt;code&gt;/horizon&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Throughput&lt;/strong&gt; — how many jobs per minute your workers are processing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtime&lt;/strong&gt; — average execution time per job class — if something spikes here, that's your bottleneck&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wait time&lt;/strong&gt; — how long jobs sit in the queue before a worker picks them up — if this grows, you need more workers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failed jobs&lt;/strong&gt; — everything that broke, with the full exception and payload right there in the UI — no more digging through logs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This alone is worth installing Horizon for.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep Horizon Running with Supervisor
&lt;/h3&gt;

&lt;p&gt;On your server, you need Supervisor to keep Horizon alive. If it crashes or the server restarts, Supervisor brings it back automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;supervisor &lt;span class="nt"&gt;-y&lt;/span&gt;                   &lt;span class="c"&gt;# Install Supervisor&lt;/span&gt;

&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/supervisor/conf.d/horizon.conf    &lt;span class="c"&gt;# Create a config file&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# horizon.conf
&lt;/span&gt;&lt;span class="nn"&gt;[program:horizon]&lt;/span&gt;
&lt;span class="py"&gt;process_name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;%(program_name)s&lt;/span&gt;
&lt;span class="py"&gt;command&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;php /var/www/yourapp/artisan horizon&lt;/span&gt;
&lt;span class="py"&gt;autostart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;autorestart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;user&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;www-data&lt;/span&gt;
&lt;span class="py"&gt;redirect_stderr&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;stdout_logfile&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/var/www/yourapp/storage/logs/horizon.log&lt;/span&gt;
&lt;span class="py"&gt;stopwaitsecs&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;3600&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply and start:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;supervisorctl reread                  &lt;span class="c"&gt;# reload the config&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;supervisorctl update
&lt;span class="nb"&gt;sudo &lt;/span&gt;supervisorctl start horizon
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One last thing — whenever you deploy new code, restart Horizon gracefully so it picks up the changes without dropping running jobs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan horizon:terminate        &lt;span class="c"&gt;# run manually or put in CI/CD pipeline&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Part 8 — Production Checklist
&lt;/h2&gt;

&lt;p&gt;Before you ship, run through this quickly:&lt;/p&gt;

&lt;h3&gt;
  
  
  Redis
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Redis is installed and running on your server (&lt;code&gt;redis-cli ping&lt;/code&gt; returns &lt;code&gt;PONG&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;QUEUE_CONNECTION=redis&lt;/code&gt; in your production &lt;code&gt;.env&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Redis password is set if your server is exposed&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Jobs
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Every job that touches a third-party service has &lt;code&gt;$tries&lt;/code&gt;, &lt;code&gt;$timeout&lt;/code&gt;, and &lt;code&gt;backoff()&lt;/code&gt; defined&lt;/li&gt;
&lt;li&gt;[ ] Critical jobs implement &lt;code&gt;failed()&lt;/code&gt; and notify you when they break&lt;/li&gt;
&lt;li&gt;[ ] Duplicate-sensitive jobs implement &lt;code&gt;ShouldBeUnique&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Failed jobs table migrated (&lt;code&gt;queue:failed-table&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Horizon
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Dashboard protected with &lt;code&gt;gate()&lt;/code&gt; — not open to the public&lt;/li&gt;
&lt;li&gt;[ ] Queue priorities configured (&lt;code&gt;critical&lt;/code&gt;, &lt;code&gt;default&lt;/code&gt;, &lt;code&gt;low&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;maxProcesses&lt;/code&gt; tuned to your server's capacity&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Supervisor
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Horizon running under Supervisor (&lt;code&gt;supervisorctl status horizon&lt;/code&gt; shows &lt;code&gt;RUNNING&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;horizon:terminate&lt;/code&gt; added to your deploy script&lt;/li&gt;
&lt;li&gt;[ ] Horizon logs are accessible at &lt;code&gt;storage/logs/horizon.log&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Sanity Check
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Trigger a job locally and confirm the worker picks it up&lt;/li&gt;
&lt;li&gt;[ ] Intentionally fail a job and confirm it shows up in &lt;code&gt;/horizon&lt;/code&gt; and &lt;code&gt;queue:failed&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Check wait times in Horizon after your first real traffic — scale workers if needed&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Part 9 — Conclusion
&lt;/h2&gt;

&lt;p&gt;Let's go back to where we started — a user clicking "Register" and staring at a spinner for 8 seconds.&lt;/p&gt;

&lt;p&gt;With everything we've set up, here's what that same flow looks like now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User clicks Register
    → Controller creates the user
    → Dispatches 3 jobs to Redis (takes ~2ms)
    → Returns "Registration successful" instantly ✓

Meanwhile, in the background:
    → Worker picks up SendWelcomeEmail   → email sent
    → Worker picks up NotifyAdminOnSlack → Slack notified
    → Worker picks up GenerateWelcomePdf → PDF stored, user notified
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The user is already on the dashboard while your workers are still doing the heavy lifting. That's the difference.&lt;/p&gt;

&lt;h3&gt;
  
  
  To recap what we covered:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Why synchronous code kills user experience and where it hides in typical Laravel apps&lt;/li&gt;
&lt;li&gt;How the queue / job / worker model works under the hood&lt;/li&gt;
&lt;li&gt;Setting up Redis and wiring it to Laravel in minutes&lt;/li&gt;
&lt;li&gt;Converting slow controller logic into clean, dispatchable jobs&lt;/li&gt;
&lt;li&gt;Three real-world scenarios — API calls, PDF generation, bulk imports&lt;/li&gt;
&lt;li&gt;Handling failures properly with retries, backoff, and &lt;code&gt;failed()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Monitoring everything in production with Laravel Horizon and Supervisor&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Queues aren't an advanced topic — they're a fundamental tool, and once you get comfortable with the pattern, you'll start seeing opportunities to use them everywhere.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;If you want to go deeper, here's where to go from here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Job Batching with &lt;code&gt;Bus::batch()&lt;/code&gt;&lt;/strong&gt; — group related jobs, track progress, run callbacks on completion&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Broadcasting job progress&lt;/strong&gt; — combine queues with Laravel Reverb to show a real-time progress bar in your UI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Horizontal scaling&lt;/strong&gt; — running multiple workers across multiple servers with Redis as the shared backbone&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🔗 Stay Connected
&lt;/h2&gt;

&lt;p&gt;Follow me for more Laravel tutorials, dev tips, deployment workflows and solving real-world production headaches.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Follow me on &lt;a href="https://www.linkedin.com/in/houdaifaboucenna/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Follow me here on &lt;a href="https://medium.com/@houdaifaboucenna" rel="noopener noreferrer"&gt;Medium&lt;/a&gt; and &lt;a href="https://medium.com/@houdaifaboucenna/subscribe" rel="noopener noreferrer"&gt;join my mailing list&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Follow me here on &lt;a href="https://dev.to/houdaifa360"&gt;Dev.to&lt;/a&gt; for more in-depth content and tutorials!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Found this article useful?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
🙏 Show your support by clapping 👏, subscribing 🔔, sharing to social networks&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>redis</category>
      <category>php</category>
      <category>backend</category>
    </item>
    <item>
      <title>From Polling to Real-Time: Building a Laravel 12 Chat with WebSockets &amp; Reverb</title>
      <dc:creator>HoudaifaDevBS</dc:creator>
      <pubDate>Wed, 25 Feb 2026 17:40:35 +0000</pubDate>
      <link>https://forem.com/houdaifadev/from-polling-to-real-time-building-a-laravel-12-chat-with-websockets-reverb-3k9j</link>
      <guid>https://forem.com/houdaifadev/from-polling-to-real-time-building-a-laravel-12-chat-with-websockets-reverb-3k9j</guid>
      <description>&lt;p&gt;Have you ever wondered how chat applications deliver messages instantly without overwhelming the server?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsuqtj6m2xus9ql3z3rzh.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsuqtj6m2xus9ql3z3rzh.jpeg" alt="polling to real-time" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I recently worked on a legacy Laravel application that included a built-in chat system. At first glance, everything seemed functional — messages were sent and received, and the feature "worked."&lt;/p&gt;

&lt;p&gt;But the experience felt off.&lt;/p&gt;

&lt;p&gt;Messages were delayed. The interface became slower after a few minutes. Opening multiple tabs made things worse. Something clearly wasn't right.&lt;/p&gt;

&lt;p&gt;After digging into the code, I discovered the issue: the application was repeatedly requesting new messages every few seconds. It was using a technique known as &lt;strong&gt;polling&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In other words, every connected user was continuously asking the server:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Do you have new messages now?"&lt;br&gt;
"What about now?"&lt;br&gt;
"Now?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This approach might work for small internal tools, but it doesn't scale. With enough users, the number of HTTP requests explodes — consuming server resources while still delivering a delayed "real-time" experience.&lt;/p&gt;

&lt;p&gt;That's when I decided to rebuild the entire chat system using a proper WebSocket-based architecture with &lt;strong&gt;Laravel Reverb&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The difference was dramatic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Latency dropped significantly&lt;/li&gt;
&lt;li&gt;HTTP request volume decreased drastically&lt;/li&gt;
&lt;li&gt;The UI became truly real-time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this article, I'll walk you through how I built a production-ready WebSocket solution using Laravel 12, Reverb, and Nginx.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Why Polling Fails at Scale
&lt;/h2&gt;

&lt;p&gt;Polling seems simple. The client sends an HTTP request every few seconds asking: "Do you have new messages?"&lt;/p&gt;

&lt;p&gt;If there are new messages, the server responds with them. If not, it returns an empty response.&lt;/p&gt;

&lt;p&gt;At small scale, this works. But let's look at the math.&lt;/p&gt;

&lt;p&gt;If one user polls every 3 seconds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;20 requests per minute&lt;/li&gt;
&lt;li&gt;1,200 requests per hour&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now imagine 100 concurrent users:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;2,000 requests per minute&lt;/li&gt;
&lt;li&gt;120,000 requests per hour&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And most of those requests return… nothing.&lt;/p&gt;

&lt;p&gt;This creates three major problems:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Massive Waste of Resources&lt;/strong&gt; — Every single HTTP request requires the server to accept the connection, bootstrap the Laravel framework, hit the routing layer, check authentication, and likely query the database — just to return an empty JSON array. This eats up CPU and RAM for zero actual payload.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Artificial Latency&lt;/strong&gt; — If a user polls every 3 seconds, and a message is sent right after a poll finishes, the receiving user won't see it for almost 3 seconds. In a modern chat application, a 3-second delay feels like an eternity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Client-Side Degradation&lt;/strong&gt; — Opening multiple tabs means multiplying those requests. Browsers limit the number of concurrent connections to the same domain. If your polling clogs up those connections, the rest of your application (like loading images or saving form data) starts to crawl.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The WebSocket Solution: From Asking to Listening
&lt;/h2&gt;

&lt;p&gt;Instead of the client repeatedly asking the server for updates, what if the server just &lt;em&gt;told&lt;/em&gt; the client when something happened?&lt;/p&gt;

&lt;p&gt;Enter &lt;strong&gt;WebSockets&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A WebSocket creates a persistent, bi-directional, full-duplex connection between the client and the server.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Connect Once&lt;/strong&gt; — The client makes a single HTTP request to establish the connection, which is then "upgraded" to a WebSocket.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Listen Continuously&lt;/strong&gt; — The connection stays open with minimal overhead. The client simply waits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Push Instantly&lt;/strong&gt; — When User A sends a message, the server instantly pushes that payload through the open connection directly to User B.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Zero wasted requests. Zero artificial delay. True real-time communication.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiab9zsiug8kknx20rxcp.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiab9zsiug8kknx20rxcp.jpeg" alt="polling vs websockets" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Laravel Real-Time Options
&lt;/h2&gt;

&lt;p&gt;Historically, achieving this in Laravel meant making a tough choice.&lt;/p&gt;

&lt;p&gt;For years, the standard approach was relying on third-party SaaS platforms like &lt;strong&gt;Pusher&lt;/strong&gt;. While incredibly easy to set up, third-party services can become prohibitively expensive as your concurrent connections and daily message limits scale up.&lt;/p&gt;

&lt;p&gt;If you wanted to self-host to save costs, you had to venture outside the PHP ecosystem. You might have used &lt;strong&gt;Laravel WebSockets&lt;/strong&gt; (which required running a long-lived PHP process that wasn't natively optimized for it and is now mostly abandoned).&lt;/p&gt;

&lt;p&gt;Then came &lt;strong&gt;Laravel Reverb&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Introduced as a first-party package, Reverb is a blazing-fast, highly scalable WebSocket server written entirely in PHP using ReactPHP. It integrates perfectly with Laravel's existing broadcasting system.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9vupp2zw1z0fkc5pxldw.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9vupp2zw1z0fkc5pxldw.jpeg" alt="pusher vs reverb" width="800" height="411"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Setting Up Reverb and Broadcasting
&lt;/h2&gt;

&lt;p&gt;Since we are using Laravel 12, installing and configuring Reverb is incredibly streamlined. Laravel 12 doesn't enable broadcasting by default, but it provides a single, powerful Artisan command to scaffold everything we need.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install Broadcasting + Reverb + Node dependencies (Pusher, Echo)&lt;/span&gt;
php artisan &lt;span class="nb"&gt;install&lt;/span&gt;:broadcasting
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INFO  Published 'broadcasting' configuration file.
INFO  Published 'channels' route file.

┌ Which broadcasting driver would you like to use? ────────────┐
│ Laravel Reverb                                               │
└──────────────────────────────────────────────────────────────┘

┌ Would you like to install Laravel Reverb? ───────────────────┐
│ Yes                                                          │
└──────────────────────────────────────────────────────────────┘

┌ Would you like to install and build the Node dependencies required for broadcasting? ┐
│ Yes                                                                                   │
└───────────────────────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you check your &lt;code&gt;.env&lt;/code&gt; file, you'll see Laravel has automatically generated the keys for your local Reverb server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BROADCAST_CONNECTION=reverb

REVERB_APP_ID=your_generated_id
REVERB_APP_KEY=your_generated_key
REVERB_APP_SECRET=your_generated_secret
REVERB_HOST="localhost"
REVERB_PORT=8080
REVERB_SCHEME=http

VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST="${REVERB_HOST}"
VITE_REVERB_PORT="${REVERB_PORT}"
VITE_REVERB_SCHEME="${REVERB_SCHEME}"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now spin up the actual WebSocket server. Open a new terminal tab and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan reverb:start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🔥 You now have a blazing-fast, self-hosted WebSocket server running on port 8080.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Building the Chat System &amp;amp; Firing Events
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Data Layer
&lt;/h3&gt;

&lt;p&gt;First, set up a simple &lt;code&gt;Message&lt;/code&gt; model linked to our &lt;code&gt;User&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan make:model Message &lt;span class="nt"&gt;-m&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In your migration, keep it lean — just the essentials:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'messages'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Blueprint&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;foreignId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sender_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;constrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'users'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;foreignId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'receiver_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;constrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'users'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'body'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Broadcast Event
&lt;/h3&gt;

&lt;p&gt;Generate an event that Laravel will automatically push to Reverb.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan make:event MessageSent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Implement the &lt;code&gt;ShouldBroadcastNow&lt;/code&gt; interface to tell Laravel to skip the queue and push the payload to Reverb immediately.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MessageSent&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ShouldBroadcastNow&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Dispatchable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;SerializesModels&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;Message&lt;/span&gt; &lt;span class="nv"&gt;$message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="c1"&gt;// Securing the channel so only the receiver user can listen&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;broadcastOn&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;Channel&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PrivateChannel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'chat.'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;receiver_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ In high traffic, use the &lt;code&gt;ShouldBroadcast&lt;/code&gt; interface instead — this lets queue workers handle broadcasting so the server can respond immediately.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Securing Private Channels
&lt;/h3&gt;

&lt;p&gt;Open &lt;code&gt;routes/channels.php&lt;/code&gt; and add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Support\Facades\Broadcast&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;Broadcast&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'chat.{userId}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$userId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only the authenticated user can listen to their own private chat channel&lt;/li&gt;
&lt;li&gt;No user can subscribe to another user's channel&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without this, Laravel will reject the WebSocket subscription.&lt;/p&gt;

&lt;h3&gt;
  
  
  Firing the Event
&lt;/h3&gt;

&lt;p&gt;When a user submits a new message via your controller, save it to the database and fire the event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Message&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="s1"&gt;'sender_id'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'receiver_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;receiver_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'body'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="c1"&gt;// This sends the pulse ONLY to the receiver's private channel&lt;/span&gt;
    &lt;span class="nf"&gt;broadcast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MessageSent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$message&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toOthers&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it for the backend — Reverb is now catching that event and blasting it out to the connected WebSocket client on the &lt;code&gt;chat.{receiverId}&lt;/code&gt; channel.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Catching the Event &amp;amp; Updating the UI in Real-Time
&lt;/h2&gt;

&lt;p&gt;The backend is now broadcasting events through Reverb. But broadcasting alone does nothing unless the frontend is actively listening. This is where &lt;strong&gt;Laravel Echo&lt;/strong&gt; comes into play.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring Echo
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// resources/js/bootstrap.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Echo&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;laravel-echo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Pusher&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pusher-js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Pusher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Pusher&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Echo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Echo&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;broadcaster&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reverb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VITE_REVERB_APP_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;wsHost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VITE_REVERB_HOST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;wsPort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VITE_REVERB_PORT&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;wssPort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VITE_REVERB_PORT&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;forceTLS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VITE_REVERB_SCHEME&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;enabledTransports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ws&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wss&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;key&lt;/code&gt; → Must match your &lt;code&gt;REVERB_APP_KEY&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;wsHost&lt;/code&gt; and &lt;code&gt;wsPort&lt;/code&gt; → Point to your Reverb server&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;forceTLS&lt;/code&gt; → Automatically switches to &lt;code&gt;wss://&lt;/code&gt; in production&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At this point, the browser will establish a persistent WebSocket connection to the Reverb server as soon as your app loads.&lt;/p&gt;

&lt;h3&gt;
  
  
  Listening to the Private Channel
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Echo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;private&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`chat.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MessageSent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;New message received:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;appendMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No polling. No intervals. No repeated API calls. The client is now simply &lt;strong&gt;listening&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Updating the UI
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;appendMessageToUI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chatBox&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chat-box&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;messageElement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;div&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;messageElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;messageElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;chatBox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messageElement&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The moment &lt;code&gt;MessageSent&lt;/code&gt; is broadcast:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reverb pushes it to the browser&lt;/li&gt;
&lt;li&gt;Echo catches it&lt;/li&gt;
&lt;li&gt;Your callback executes&lt;/li&gt;
&lt;li&gt;The DOM updates instantly&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2w9zy415zmmtov3pqtqj.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2w9zy415zmmtov3pqtqj.jpeg" alt="network inspect socket request" width="800" height="345"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Real Metrics: Before vs After
&lt;/h2&gt;

&lt;p&gt;When migrating from polling to WebSockets, the improvement isn't theoretical — it's measurable. The comparison below is based on 80 concurrent users.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Polling&lt;/th&gt;
&lt;th&gt;WebSockets (Reverb)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HTTP Requests/min&lt;/td&gt;
&lt;td&gt;~1,600&lt;/td&gt;
&lt;td&gt;~0 (persistent connection)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Message Latency&lt;/td&gt;
&lt;td&gt;Up to 3 seconds&lt;/td&gt;
&lt;td&gt;Near-instant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server Load&lt;/td&gt;
&lt;td&gt;High (constant requests)&lt;/td&gt;
&lt;td&gt;Low (event-driven)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-tab Behavior&lt;/td&gt;
&lt;td&gt;Degrades quickly&lt;/td&gt;
&lt;td&gt;Stable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  8. Real-World Use Cases Beyond Chat
&lt;/h2&gt;

&lt;p&gt;This architecture is not limited to messaging. Once WebSockets are in place, you unlock powerful capabilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔔 &lt;strong&gt;Live Notifications&lt;/strong&gt; — Instant alerts without refreshing the page&lt;/li&gt;
&lt;li&gt;📊 &lt;strong&gt;Real-Time Dashboards&lt;/strong&gt; — Admin panels that update metrics instantly&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;Order Tracking&lt;/strong&gt; — Delivery status updates pushed in real-time&lt;/li&gt;
&lt;li&gt;🧠 &lt;strong&gt;Live Analytics&lt;/strong&gt; — Active user counters, sales metrics, traffic monitoring&lt;/li&gt;
&lt;li&gt;🏭 &lt;strong&gt;IoT Monitoring&lt;/strong&gt; — Devices pushing sensor data instantly to dashboards&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Any system where "something happens" and users must see it immediately is a perfect candidate.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Rebuilding the chat system was only half the journey.&lt;/p&gt;

&lt;p&gt;Moving from polling to WebSockets solved the performance and latency problems — but running WebSockets in production introduces new challenges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reverse proxy configuration&lt;/li&gt;
&lt;li&gt;SSL termination (&lt;code&gt;wss://&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Process supervision&lt;/li&gt;
&lt;li&gt;Firewall rules&lt;/li&gt;
&lt;li&gt;Scaling strategy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In &lt;strong&gt;Part 2&lt;/strong&gt;, I'll walk through how to deploy Laravel Reverb in production behind Nginx — including the exact configuration, common pitfalls, and issues you may encounter along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔗 Stay Connected
&lt;/h2&gt;

&lt;p&gt;Follow me for more Laravel tutorials, dev tips, deployment workflows and solving real-world production headaches.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Follow me on &lt;a href="https://www.linkedin.com/in/houdaifaboucenna/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Follow me here on &lt;a href="https://medium.com/@houdaifaboucenna" rel="noopener noreferrer"&gt;Medium&lt;/a&gt; and &lt;a href="https://medium.com/@houdaifaboucenna/subscribe" rel="noopener noreferrer"&gt;join my mailing list&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Follow me here on &lt;a href="https://dev.to/houdaifa360"&gt;Dev.to&lt;/a&gt; for more in-depth content and tutorials!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Found this article useful?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
🙏 Show your support by clapping 👏, subscribing 🔔, sharing to social networks&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>websocket</category>
      <category>reverb</category>
    </item>
    <item>
      <title>DeepTrack CLI: Gamify Your Learning with AI 🎯 | GitHub Copilot CLI Challenge</title>
      <dc:creator>HoudaifaDevBS</dc:creator>
      <pubDate>Sat, 14 Feb 2026 14:45:43 +0000</pubDate>
      <link>https://forem.com/houdaifadev/deeptrack-cli-gamify-your-learning-with-ai-github-copilot-cli-challenge-25d3</link>
      <guid>https://forem.com/houdaifadev/deeptrack-cli-gamify-your-learning-with-ai-github-copilot-cli-challenge-25d3</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/github-2026-01-21"&gt;GitHub Copilot CLI Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;I built DeepTrack CLI, a minimal AI-inspired learning tracker that helps me turn my learning into daily quizzes and track progress with gamification elements like XP, levels, streaks, and badges.&lt;/p&gt;

&lt;p&gt;As a developer, I consume a lot of articles, videos, and tutorials, but I realized that reading alone isn’t enough — I needed a system to reinforce knowledge and make learning fun. DeepTrack CLI is my solution: a terminal-first tool that generates quizzes, tracks my performance, and highlights my weak topics, giving me a clear path to focus each day.&lt;/p&gt;

&lt;p&gt;This project is especially meaningful because it reflects how AI-assisted development can accelerate building useful tools while keeping workflows minimal and practical.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;GitHub Repository: &lt;a href="https://github.com/houdaifaboucenna/deeptrack-CLI" rel="noopener noreferrer"&gt;houdaifaboucenna/deeptrack-CLI&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is a look at DeepTrack in action:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A robust CLI interface to manage your learning journey: The CLI comes packed with commands to learn new topics, take a daily challenge, or check your leaderboard.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbdjg01j2n1a7i8sdpryo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbdjg01j2n1a7i8sdpryo.png" alt="DeepTrack CLI options" width="800" height="262"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Interactive, AI-Generated Quizzes: Taking a quiz on Laravel Queues. The CLI instantly grades your answers, updates your stats, and awards badges based on your performance.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsv7ib893kyynedcql25d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsv7ib893kyynedcql25d.png" alt="Quizz Playing" width="800" height="741"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Web Dashboard: Because staring at the terminal isn't always what we want, DeepTrack can generate a clean dashboard to visualize your overall statistics, track your top topics, and review your mastery levels.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftb82ofwpp8h8mfq4qc00.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftb82ofwpp8h8mfq4qc00.png" alt="Web Dashboard to visualize progress" width="800" height="456"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  My Experience with GitHub Copilot CLI
&lt;/h2&gt;

&lt;p&gt;I relied heavily on GitHub Copilot CLI throughout development:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generating project scaffolding:&lt;/strong&gt; I asked Copilot to create the folder structure, CLI commands, and basic service files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Writing core logic:&lt;/strong&gt; Copilot suggested code for quiz generation, gamification calculations, and weakest-topic analysis, which I refined manually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Refactoring &amp;amp; explanations:&lt;/strong&gt; I used Copilot to review functions and suggest improvements, ensuring clean, readable, and maintainable code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workflow acceleration:&lt;/strong&gt; Tasks that usually take hours — like creating the CLI commands or service skeletons — were completed in minutes, letting me focus on design and usability instead of boilerplate code.&lt;/p&gt;

&lt;p&gt;This submission showcases how Copilot CLI can act as a true pair programmer, especially in small, high-impact projects&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>githubchallenge</category>
      <category>cli</category>
      <category>githubcopilot</category>
    </item>
    <item>
      <title>I wrote a step-by-step tutorial, sharing my solution for ZERO-DOWNTIME deployment of a Laravel app in a VPS / EC2 environment with Github Actions

https://dev.to/houdaifa360/deploy-laravel-to-vps-with-github-actions-zero-downtime-cicd-f3</title>
      <dc:creator>HoudaifaDevBS</dc:creator>
      <pubDate>Tue, 10 Feb 2026 21:13:37 +0000</pubDate>
      <link>https://forem.com/houdaifadev/i-wrote-a-step-by-step-tutorial-sharing-my-solution-for-zero-downtime-deployment-of-a-laravel-app-5co5</link>
      <guid>https://forem.com/houdaifadev/i-wrote-a-step-by-step-tutorial-sharing-my-solution-for-zero-downtime-deployment-of-a-laravel-app-5co5</guid>
      <description>&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://dev.to/houdaifadev/deploy-laravel-to-vps-with-github-actions-zero-downtime-cicd-f3" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjiykoz0rmqvfpxhdar50.png" height="400" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://dev.to/houdaifadev/deploy-laravel-to-vps-with-github-actions-zero-downtime-cicd-f3" rel="noopener noreferrer" class="c-link"&gt;
            Deploy Laravel to VPS with GitHub Actions (Zero Downtime CI/CD) - DEV Community
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Deploying a Laravel application to a freshly created VPS can quickly become overwhelming. There are...
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8j7kvp660rqzt99zui8e.png" width="300" height="299"&gt;
          dev.to
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


</description>
      <category>cicd</category>
      <category>github</category>
      <category>laravel</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Deploy Laravel to VPS with GitHub Actions (Zero Downtime CI/CD)</title>
      <dc:creator>HoudaifaDevBS</dc:creator>
      <pubDate>Tue, 10 Feb 2026 16:15:10 +0000</pubDate>
      <link>https://forem.com/houdaifadev/deploy-laravel-to-vps-with-github-actions-zero-downtime-cicd-f3</link>
      <guid>https://forem.com/houdaifadev/deploy-laravel-to-vps-with-github-actions-zero-downtime-cicd-f3</guid>
      <description>&lt;p&gt;Deploying a Laravel application to a freshly created VPS can quickly become overwhelming. There are many ways to put a web application online, and it's easy to lose focus trying to build the "perfect" deployment workflow — only to end up with something hard to maintain.&lt;/p&gt;

&lt;p&gt;In real projects, what matters most is a reliable, repeatable, and safe deployment process that lets you focus on writing code rather than manually pushing changes to servers.&lt;/p&gt;

&lt;p&gt;In this article, I'll walk through a &lt;strong&gt;minimal yet production-ready CI/CD approach&lt;/strong&gt; for deploying Laravel applications to a VPS (or EC2 instance) using &lt;strong&gt;GitHub Actions and SSH&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;⚠️ The goal is not to cover every possible optimization, but to present a workflow that works well for real client projects and personal products.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 Not on a VPS yet?&lt;/strong&gt; This guide focuses on automated deployment for servers where you have full root privileges. If you are stuck with &lt;strong&gt;Shared Hosting (cPanel)&lt;/strong&gt; and don't have SSH access, check out my alternative guide: 👉 &lt;a href="https://medium.com/@houdaifaboucenna/deploying-laravel-on-shared-hosting-no-ssh-required-34b409efc7ae" rel="noopener noreferrer"&gt;&lt;strong&gt;How to Deploy Laravel on Shared Hosting (No SSH Required)&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Stack Used
&lt;/h2&gt;

&lt;p&gt;To keep things concrete, this guide assumes the following setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ubuntu Server (20.04+)&lt;/li&gt;
&lt;li&gt;Laravel 12 with PHP 8.3&lt;/li&gt;
&lt;li&gt;Nginx + PHP-FPM&lt;/li&gt;
&lt;li&gt;MySQL&lt;/li&gt;
&lt;li&gt;GitHub Actions for CI/CD&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This guide assumes basic familiarity with Linux and Laravel.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F93eyniwgb5lvhzuvj1bq.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F93eyniwgb5lvhzuvj1bq.webp" alt="deploy laravel to vps with github actions" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 — Installing Server Requirements
&lt;/h2&gt;

&lt;p&gt;After connecting to your server via SSH, it's recommended &lt;strong&gt;not to work as root&lt;/strong&gt;, but as a sudo-enabled user.&lt;/p&gt;

&lt;p&gt;The key idea here is that &lt;strong&gt;this setup is done once&lt;/strong&gt;. After the initial server preparation, deployments should not require installing system dependencies again.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Update system list and upgrade packages  &lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt upgrade &lt;span class="nt"&gt;-y&lt;/span&gt;  

&lt;span class="c"&gt;# Install Nginx  &lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;nginx &lt;span class="nt"&gt;-y&lt;/span&gt;  

&lt;span class="c"&gt;# Install Mysql Server  &lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;mysql-server &lt;span class="nt"&gt;-y&lt;/span&gt;  

&lt;span class="c"&gt;# Install PHP and required Packages  &lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;php-cli php-fpm php-mysql php-xml php-mbstring php-curl unzip &lt;span class="nt"&gt;-y&lt;/span&gt;  

&lt;span class="c"&gt;# Install Composer   &lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; https://getcomposer.org/installer | php  
&lt;span class="nb"&gt;sudo mv &lt;/span&gt;composer.phar /usr/local/bin/composer  

&lt;span class="c"&gt;# Install Node.js and NPM  &lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://deb.nodesource.com/setup_lts.x | &lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; bash -  
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; nodejs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠️ Be careful to install a PHP version that matches your Laravel version requirements. Mismatches here are a common source of production issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — Configuring Nginx for Laravel
&lt;/h2&gt;

&lt;p&gt;We need to configure Nginx to point to the &lt;code&gt;current/public&lt;/code&gt; directory (which we will create later via our CI/CD pipeline). Let's create configs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo touch&lt;/span&gt; /etc/nginx/sites-available/laravel-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;your-domain.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  

    &lt;span class="c1"&gt;# POINT TO 'current/public' FOR ATOMIC DEPLOYMENTS  &lt;/span&gt;
    &lt;span class="kn"&gt;root&lt;/span&gt; &lt;span class="n"&gt;/var/www/laravel-app/current/public&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  

    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;X-Frame-Options&lt;/span&gt; &lt;span class="s"&gt;"SAMEORIGIN"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;X-Content-Type-Options&lt;/span&gt; &lt;span class="s"&gt;"nosniff"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  

    &lt;span class="kn"&gt;index&lt;/span&gt; &lt;span class="s"&gt;index.php&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
    &lt;span class="kn"&gt;charset&lt;/span&gt; &lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
        &lt;span class="kn"&gt;try_files&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt;&lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="n"&gt;/index.php?&lt;/span&gt;&lt;span class="nv"&gt;$query_string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
    &lt;span class="p"&gt;}&lt;/span&gt;  

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;/favicon.ico&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kn"&gt;access_log&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kn"&gt;log_not_found&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;/robots.txt&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kn"&gt;access_log&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kn"&gt;log_not_found&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  

    &lt;span class="kn"&gt;error_page&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt; &lt;span class="n"&gt;/index.php&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt; &lt;span class="sr"&gt;\.php$&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
        &lt;span class="kn"&gt;fastcgi_pass&lt;/span&gt; &lt;span class="s"&gt;unix:/var/run/php/php8.3-fpm.sock&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
        &lt;span class="kn"&gt;fastcgi_index&lt;/span&gt; &lt;span class="s"&gt;index.php&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
        &lt;span class="kn"&gt;fastcgi_param&lt;/span&gt; &lt;span class="s"&gt;SCRIPT_FILENAME&lt;/span&gt; &lt;span class="nv"&gt;$realpath_root$fastcgi_script_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
        &lt;span class="kn"&gt;include&lt;/span&gt; &lt;span class="s"&gt;fastcgi_params&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
    &lt;span class="p"&gt;}&lt;/span&gt;  

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt; &lt;span class="sr"&gt;/\.(?!well-known).*&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
        &lt;span class="kn"&gt;deny&lt;/span&gt; &lt;span class="s"&gt;all&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
    &lt;span class="p"&gt;}&lt;/span&gt;  
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, we enable the configuration and ensure it is working:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /etc/nginx/sites-available/laravel-app /etc/nginx/sites-enabled/  
&lt;span class="nb"&gt;sudo unlink&lt;/span&gt; /etc/nginx/sites-enabled/default  
&lt;span class="nb"&gt;sudo &lt;/span&gt;nginx &lt;span class="nt"&gt;-t&lt;/span&gt;  
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠️ Replace &lt;code&gt;your-domain.com&lt;/code&gt; with your actual domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 — Set up MySQL server and create a database
&lt;/h2&gt;

&lt;p&gt;Let's execute the MySQL security script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Secure MySQL (set root password, remove anonymous users)  &lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;mysql_secure_installation  

&lt;span class="c"&gt;# Log in to MySQL  &lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;mysql &lt;span class="nt"&gt;-u&lt;/span&gt; root &lt;span class="nt"&gt;-p&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, we create the application database that we will use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;laravel_db&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="s1"&gt;'laravel_user'&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="s1"&gt;'localhost'&lt;/span&gt; &lt;span class="n"&gt;IDENTIFIED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="s1"&gt;'your-strong-password'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;PRIVILEGES&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;laravel_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'laravel_user'&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="s1"&gt;'localhost'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
&lt;span class="n"&gt;FLUSH&lt;/span&gt; &lt;span class="k"&gt;PRIVILEGES&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
&lt;span class="n"&gt;EXIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once created, we should add the database access in the production &lt;code&gt;.env&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;⚠️ The &lt;code&gt;.env&lt;/code&gt; is never committed to Git and is created only once on the server. CI/CD pipelines should never modify it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 — Initializing the Application on the Server
&lt;/h2&gt;

&lt;p&gt;Before automation, we need to set up the folder structure. We will create a "&lt;strong&gt;shared&lt;/strong&gt;" structure so that logs and uploads persist between deployments.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Create the main directory  &lt;/span&gt;
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /var/www/laravel-app  
&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="nv"&gt;$USER&lt;/span&gt;:www-data /var/www/laravel-app  

&lt;span class="c"&gt;# 2. Setup the persistent folders  &lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; /var/www/laravel-app  
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; shared/storage/framework/&lt;span class="o"&gt;{&lt;/span&gt;cache/data,sessions,views&lt;span class="o"&gt;}&lt;/span&gt;  

&lt;span class="c"&gt;# 3. Create the .env file  &lt;/span&gt;
nano shared/.env  
&lt;span class="c"&gt;# (Paste your production env content here)  &lt;/span&gt;

&lt;span class="c"&gt;# 4. Set permissions ONE TIME  &lt;/span&gt;
&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="nv"&gt;$USER&lt;/span&gt;:www-data /var/www/laravel-app  
&lt;span class="nb"&gt;sudo chmod&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; 775 shared/storage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 5 — CI/CD Strategy with GitHub Actions
&lt;/h2&gt;

&lt;p&gt;Instead of running Composer and NPM commands directly on the server, the deployment workflow follows a &lt;strong&gt;build-then-deploy&lt;/strong&gt; strategy:&lt;/p&gt;

&lt;h3&gt;
  
  
  Why build in CI instead of the server?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Faster deployments&lt;/li&gt;
&lt;li&gt;More predictable results&lt;/li&gt;
&lt;li&gt;Fewer production dependencies&lt;/li&gt;
&lt;li&gt;Easier debugging&lt;/li&gt;
&lt;li&gt;Reduced server load&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;In this approach:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;GitHub Actions installs PHP and Node&lt;/li&gt;
&lt;li&gt;Composer dependencies are installed&lt;/li&gt;
&lt;li&gt;Frontend assets are built&lt;/li&gt;
&lt;li&gt;A clean release artifact is generated&lt;/li&gt;
&lt;li&gt;The artifact is deployed to the server via SSH&lt;/li&gt;
&lt;li&gt;A new release directory is created&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;current&lt;/code&gt; symlink is updated atomically&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The server becomes a &lt;strong&gt;stable runtime environment&lt;/strong&gt;, not a build machine.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;.github/workflows/deploy.yml&lt;/code&gt; in your Laravel project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy Laravel to VPS&lt;/span&gt;  

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;main&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;  

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;  

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
    &lt;span class="c1"&gt;# 1️⃣ Checkout Code  &lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout repository&lt;/span&gt;  
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;  

    &lt;span class="c1"&gt;# 2️⃣ Setup PHP  &lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup PHP&lt;/span&gt;  
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shivammathur/setup-php@v2&lt;/span&gt;  
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
        &lt;span class="na"&gt;php-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;8.3&lt;/span&gt;  
        &lt;span class="na"&gt;extensions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mbstring, xml, ctype, iconv, intl, pdo_mysql&lt;/span&gt;  

    &lt;span class="c1"&gt;# 3️⃣ Install Backend Dependencies  &lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Composer dependencies&lt;/span&gt;  
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;  
        &lt;span class="s"&gt;composer install --no-dev --prefer-dist --optimize-autoloader  &lt;/span&gt;

    &lt;span class="c1"&gt;# 4️⃣ Build Frontend Assets  &lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup Node &amp;amp; Build&lt;/span&gt;  
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;  
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
        &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;20&lt;/span&gt;  
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;  
        &lt;span class="s"&gt;npm ci  &lt;/span&gt;
        &lt;span class="s"&gt;npm run build  &lt;/span&gt;

    &lt;span class="c1"&gt;# 5️⃣ Prepare Files for Transfer  &lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Archive application&lt;/span&gt;  
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;  
        &lt;span class="s"&gt;tar --exclude='./storage' \  &lt;/span&gt;
            &lt;span class="s"&gt;--exclude='./.git' \  &lt;/span&gt;
            &lt;span class="s"&gt;--exclude='./node_modules' \  &lt;/span&gt;
            &lt;span class="s"&gt;--exclude='./tests' \  &lt;/span&gt;
            &lt;span class="s"&gt;-czf /tmp/release.tar.gz .  &lt;/span&gt;

        &lt;span class="s"&gt;# 2. Move it back to the workspace  &lt;/span&gt;
        &lt;span class="s"&gt;mv /tmp/release.tar.gz .  &lt;/span&gt;

    &lt;span class="c1"&gt;# 6️⃣ Upload to Server  &lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload artifact via SCP&lt;/span&gt;  
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appleboy/scp-action@master&lt;/span&gt;  
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
        &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.VPS_HOST }}&lt;/span&gt;  
        &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.VPS_USERNAME }}&lt;/span&gt;  
        &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.VPS_SSH_KEY }}&lt;/span&gt;  
        &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.VPS_PORT || 22 }}&lt;/span&gt;  
        &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;release.tar.gz"&lt;/span&gt;  
        &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/var/www/laravel-app"&lt;/span&gt;  

    &lt;span class="c1"&gt;# 7️⃣ Deploy on Server  &lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Execute Remote SSH Commands&lt;/span&gt;  
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appleboy/ssh-action@master&lt;/span&gt;  
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
        &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.VPS_HOST }}&lt;/span&gt;  
        &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.VPS_USERNAME }}&lt;/span&gt;  
        &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.VPS_SSH_KEY }}&lt;/span&gt;  
        &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.VPS_PORT || 22 }}&lt;/span&gt;  
        &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;  
          &lt;span class="s"&gt;set -e  &lt;/span&gt;

          &lt;span class="s"&gt;APP_DIR="/var/www/laravel-app"  &lt;/span&gt;
          &lt;span class="s"&gt;RELEASE_ID=$(date +%Y%m%d%H%M%S)  &lt;/span&gt;
          &lt;span class="s"&gt;RELEASE_PATH="$APP_DIR/releases/$RELEASE_ID"  &lt;/span&gt;

          &lt;span class="s"&gt;# 1. Create new release directory  &lt;/span&gt;
          &lt;span class="s"&gt;mkdir -p $RELEASE_PATH  &lt;/span&gt;

          &lt;span class="s"&gt;# 2. Extract files  &lt;/span&gt;
          &lt;span class="s"&gt;tar -xzf $APP_DIR/release.tar.gz -C $RELEASE_PATH  &lt;/span&gt;
          &lt;span class="s"&gt;rm $APP_DIR/release.tar.gz  &lt;/span&gt;

          &lt;span class="s"&gt;# 3. Link Shared Resources  &lt;/span&gt;
          &lt;span class="s"&gt;ln -sfn $APP_DIR/shared/.env $RELEASE_PATH/.env  &lt;/span&gt;
          &lt;span class="s"&gt;ln -sfn $APP_DIR/shared/storage $RELEASE_PATH/storage  &lt;/span&gt;

          &lt;span class="s"&gt;# 4. Set Permissions  &lt;/span&gt;
          &lt;span class="s"&gt;sudo chown -R $USER:www-data $RELEASE_PATH  &lt;/span&gt;

          &lt;span class="s"&gt;# Ensure group write access for cache (so webserver can write to it)  &lt;/span&gt;
          &lt;span class="s"&gt;sudo chmod -R 775 $RELEASE_PATH/bootstrap/cache  &lt;/span&gt;

          &lt;span class="s"&gt;# 5. Run Migrations &amp;amp; Storage Link &amp;amp; Optimize &amp;amp; Reload&lt;/span&gt;
          &lt;span class="s"&gt;cd $RELEASE_PATH&lt;/span&gt;
          &lt;span class="s"&gt;php artisan migrate --force&lt;/span&gt;
          &lt;span class="s"&gt;php artisan storage:link&lt;/span&gt;
          &lt;span class="s"&gt;php artisan optimize&lt;/span&gt;
          &lt;span class="s"&gt;php artisan reload&lt;/span&gt;

          &lt;span class="s"&gt;# 6. Atomic Switch (Zero Downtime)  &lt;/span&gt;
          &lt;span class="s"&gt;ln -sfn $RELEASE_PATH $APP_DIR/current  &lt;/span&gt;

          &lt;span class="s"&gt;# 7. Reload PHP-FPM (Ensure this matches your server version!)  &lt;/span&gt;
          &lt;span class="s"&gt;sudo systemctl reload php8.3-fpm  &lt;/span&gt;

          &lt;span class="s"&gt;# 8. Cleanup old releases (keep latest 5)   &lt;/span&gt;
          &lt;span class="s"&gt;cd $APP_DIR/releases  &lt;/span&gt;
          &lt;span class="s"&gt;ls -t | tail -n +6 | xargs -r rm -rf  &lt;/span&gt;

          &lt;span class="s"&gt;echo "🚀 Deployment $RELEASE_ID success!"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In your GitHub repository:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to Settings → Secrets and variables → Actions&lt;/li&gt;
&lt;li&gt;Add these secrets:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;VPS_HOST&lt;/code&gt;: Your VPS IP address&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;VPS_USERNAME&lt;/code&gt;: SSH username&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;VPS_SSH_KEY&lt;/code&gt;: Private SSH key&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;VPS_PORT&lt;/code&gt;: SSH port (default: 22)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 6 — Release-Based Deployment Flow
&lt;/h2&gt;

&lt;p&gt;Each deployment creates a new release directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/var/www/laravel-app/releases/20260210151258
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shared directories such as storage and bootstrap/cache are symlinked into each release.&lt;/p&gt;

&lt;p&gt;Once everything is ready:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Database migrations are executed with &lt;code&gt;--force&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Caches are optimized&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;current&lt;/code&gt; symlink is switched to the new release&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;⚡ Because the symlink update is atomic, &lt;strong&gt;users rarely notice downtime&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting Common Issues
&lt;/h2&gt;

&lt;h3&gt;
  
  
  500 Internal Server Error
&lt;/h3&gt;

&lt;p&gt;Most commonly caused by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Incorrect file permissions&lt;/li&gt;
&lt;li&gt;Missing PHP extensions&lt;/li&gt;
&lt;li&gt;Wrong PHP-FPM socket version&lt;/li&gt;
&lt;li&gt;Broken symlink paths&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check that &lt;code&gt;storage/&lt;/code&gt; and &lt;code&gt;bootstrap/cache/&lt;/code&gt; are writable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Blank Page After Deployment
&lt;/h3&gt;

&lt;p&gt;Often caused by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Incorrect Nginx root path&lt;/li&gt;
&lt;li&gt;Missing &lt;code&gt;.env&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Application key not generated&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Make sure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;APP_ENV=production  
APP_DEBUG=false
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  SSH Action Fails
&lt;/h3&gt;

&lt;p&gt;Usually due to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Incorrect private key format&lt;/li&gt;
&lt;li&gt;Wrong SSH username&lt;/li&gt;
&lt;li&gt;Server firewall rules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Test SSH access manually before debugging CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;This setup gives you a professional, automated pipeline. You push to &lt;code&gt;main&lt;/code&gt;, and GitHub Actions handles the rest — running tests, building assets, and switching the live site without dropping connections.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Happy Deploying! 🚀&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🔗 Stay Connected
&lt;/h2&gt;

&lt;p&gt;Follow me for more Laravel tutorials, dev tips, deployment workflows and solving real-world production headaches.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Follow me on &lt;a href="https://www.linkedin.com/in/houdaifaboucenna/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Follow me here on &lt;a href="https://medium.com/@houdaifaboucenna" rel="noopener noreferrer"&gt;Medium&lt;/a&gt; and &lt;a href="https://medium.com/@houdaifaboucenna/subscribe" rel="noopener noreferrer"&gt;join my mailing list&lt;/a&gt; for more in-depth content and tutorials!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Found this article useful?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
🙏 Show your support by clapping 👏, subscribing 🔔, sharing to social networks&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; Laravel, VPS, GitHub Actions, CI/CD, Nginx&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1 — Installing Server Requirements
&lt;/h2&gt;

&lt;p&gt;After connecting to your server via SSH, it's recommended &lt;strong&gt;not to work as root&lt;/strong&gt;, but as a sudo-enabled user.&lt;/p&gt;

&lt;p&gt;The key idea here is that &lt;strong&gt;this setup is done once&lt;/strong&gt;. After the initial server preparation, deployments should not require installing system dependencies again.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Update system list and upgrade packages  &lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt upgrade &lt;span class="nt"&gt;-y&lt;/span&gt;  

&lt;span class="c"&gt;# Install Nginx  &lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;nginx &lt;span class="nt"&gt;-y&lt;/span&gt;  

&lt;span class="c"&gt;# Install Mysql Server  &lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;mysql-server &lt;span class="nt"&gt;-y&lt;/span&gt;  

&lt;span class="c"&gt;# Install PHP and required Packages  &lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;php-cli php-fpm php-mysql php-xml php-mbstring php-curl unzip &lt;span class="nt"&gt;-y&lt;/span&gt;  

&lt;span class="c"&gt;# Install Composer   &lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; https://getcomposer.org/installer | php  
&lt;span class="nb"&gt;sudo mv &lt;/span&gt;composer.phar /usr/local/bin/composer  

&lt;span class="c"&gt;# Install Node.js and NPM  &lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://deb.nodesource.com/setup_lts.x | &lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; bash -  
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; nodejs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠️ Be careful to install a PHP version that matches your Laravel version requirements. Mismatches here are a common source of production issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — Configuring Nginx for Laravel
&lt;/h2&gt;

&lt;p&gt;We need to configure Nginx to point to the &lt;code&gt;current/public&lt;/code&gt; directory (which we will create later via our CI/CD pipeline). Let's create configs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo touch&lt;/span&gt; /etc/nginx/sites-available/laravel-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;your-domain.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  

    &lt;span class="c1"&gt;# POINT TO 'current/public' FOR ATOMIC DEPLOYMENTS  &lt;/span&gt;
    &lt;span class="kn"&gt;root&lt;/span&gt; &lt;span class="n"&gt;/var/www/laravel-app/current/public&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  

    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;X-Frame-Options&lt;/span&gt; &lt;span class="s"&gt;"SAMEORIGIN"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;X-Content-Type-Options&lt;/span&gt; &lt;span class="s"&gt;"nosniff"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  

    &lt;span class="kn"&gt;index&lt;/span&gt; &lt;span class="s"&gt;index.php&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
    &lt;span class="kn"&gt;charset&lt;/span&gt; &lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
        &lt;span class="kn"&gt;try_files&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt;&lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="n"&gt;/index.php?&lt;/span&gt;&lt;span class="nv"&gt;$query_string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
    &lt;span class="p"&gt;}&lt;/span&gt;  

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;/favicon.ico&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kn"&gt;access_log&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kn"&gt;log_not_found&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;/robots.txt&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kn"&gt;access_log&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kn"&gt;log_not_found&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  

    &lt;span class="kn"&gt;error_page&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt; &lt;span class="n"&gt;/index.php&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt; &lt;span class="sr"&gt;\.php$&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
        &lt;span class="kn"&gt;fastcgi_pass&lt;/span&gt; &lt;span class="s"&gt;unix:/var/run/php/php8.3-fpm.sock&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
        &lt;span class="kn"&gt;fastcgi_index&lt;/span&gt; &lt;span class="s"&gt;index.php&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
        &lt;span class="kn"&gt;fastcgi_param&lt;/span&gt; &lt;span class="s"&gt;SCRIPT_FILENAME&lt;/span&gt; &lt;span class="nv"&gt;$realpath_root$fastcgi_script_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
        &lt;span class="kn"&gt;include&lt;/span&gt; &lt;span class="s"&gt;fastcgi_params&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
    &lt;span class="p"&gt;}&lt;/span&gt;  

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt; &lt;span class="sr"&gt;/\.(?!well-known).*&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
        &lt;span class="kn"&gt;deny&lt;/span&gt; &lt;span class="s"&gt;all&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
    &lt;span class="p"&gt;}&lt;/span&gt;  
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, we enable the configuration and ensure it is working:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /etc/nginx/sites-available/laravel-app /etc/nginx/sites-enabled/  
&lt;span class="nb"&gt;sudo unlink&lt;/span&gt; /etc/nginx/sites-enabled/default  
&lt;span class="nb"&gt;sudo &lt;/span&gt;nginx &lt;span class="nt"&gt;-t&lt;/span&gt;  
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠️ Replace &lt;code&gt;your-domain.com&lt;/code&gt; with your actual domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 — Set up MySQL server and create a database
&lt;/h2&gt;

&lt;p&gt;Let's execute the MySQL security script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Secure MySQL (set root password, remove anonymous users)  &lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;mysql_secure_installation  

&lt;span class="c"&gt;# Log in to MySQL  &lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;mysql &lt;span class="nt"&gt;-u&lt;/span&gt; root &lt;span class="nt"&gt;-p&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, we create the application database that we will use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;laravel_db&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="s1"&gt;'laravel_user'&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="s1"&gt;'localhost'&lt;/span&gt; &lt;span class="n"&gt;IDENTIFIED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="s1"&gt;'your-strong-password'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;PRIVILEGES&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;laravel_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'laravel_user'&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="s1"&gt;'localhost'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
&lt;span class="n"&gt;FLUSH&lt;/span&gt; &lt;span class="k"&gt;PRIVILEGES&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
&lt;span class="n"&gt;EXIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once created, we should add the database access in the production &lt;code&gt;.env&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;⚠️ The &lt;code&gt;.env&lt;/code&gt; is never committed to Git and is created only once on the server. CI/CD pipelines should never modify it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 — Initializing the Application on the Server
&lt;/h2&gt;

&lt;p&gt;Before automation, we need to set up the folder structure. We will create a "&lt;strong&gt;shared&lt;/strong&gt;" structure so that logs and uploads persist between deployments.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Create the main directory  &lt;/span&gt;
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /var/www/laravel-app  
&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="nv"&gt;$USER&lt;/span&gt;:www-data /var/www/laravel-app  

&lt;span class="c"&gt;# 2. Setup the persistent folders  &lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; /var/www/laravel-app  
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; shared/storage/framework/&lt;span class="o"&gt;{&lt;/span&gt;cache/data,sessions,views&lt;span class="o"&gt;}&lt;/span&gt;  

&lt;span class="c"&gt;# 3. Create the .env file  &lt;/span&gt;
nano shared/.env  
&lt;span class="c"&gt;# (Paste your production env content here)  &lt;/span&gt;

&lt;span class="c"&gt;# 4. Set permissions ONE TIME  &lt;/span&gt;
&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="nv"&gt;$USER&lt;/span&gt;:www-data /var/www/laravel-app  
&lt;span class="nb"&gt;sudo chmod&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; 775 shared/storage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 5 — CI/CD Strategy with GitHub Actions
&lt;/h2&gt;

&lt;p&gt;Instead of running Composer and NPM commands directly on the server, the deployment workflow follows a &lt;strong&gt;build-then-deploy&lt;/strong&gt; strategy:&lt;/p&gt;

&lt;h3&gt;
  
  
  Why build in CI instead of the server?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Faster deployments&lt;/li&gt;
&lt;li&gt;More predictable results&lt;/li&gt;
&lt;li&gt;Fewer production dependencies&lt;/li&gt;
&lt;li&gt;Easier debugging&lt;/li&gt;
&lt;li&gt;Reduced server load&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;In this approach:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;GitHub Actions installs PHP and Node&lt;/li&gt;
&lt;li&gt;Composer dependencies are installed&lt;/li&gt;
&lt;li&gt;Frontend assets are built&lt;/li&gt;
&lt;li&gt;A clean release artifact is generated&lt;/li&gt;
&lt;li&gt;The artifact is deployed to the server via SSH&lt;/li&gt;
&lt;li&gt;A new release directory is created&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;current&lt;/code&gt; symlink is updated atomically&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The server becomes a &lt;strong&gt;stable runtime environment&lt;/strong&gt;, not a build machine.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;.github/workflows/deploy.yml&lt;/code&gt; in your Laravel project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy Laravel to VPS&lt;/span&gt;  

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;main&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;  

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;  

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
    &lt;span class="c1"&gt;# 1️⃣ Checkout Code  &lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout repository&lt;/span&gt;  
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;  

    &lt;span class="c1"&gt;# 2️⃣ Setup PHP  &lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup PHP&lt;/span&gt;  
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shivammathur/setup-php@v2&lt;/span&gt;  
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
        &lt;span class="na"&gt;php-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;8.3&lt;/span&gt;  
        &lt;span class="na"&gt;extensions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mbstring, xml, ctype, iconv, intl, pdo_mysql&lt;/span&gt;  

    &lt;span class="c1"&gt;# 3️⃣ Install Backend Dependencies  &lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Composer dependencies&lt;/span&gt;  
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;  
        &lt;span class="s"&gt;composer install --no-dev --prefer-dist --optimize-autoloader  &lt;/span&gt;

    &lt;span class="c1"&gt;# 4️⃣ Build Frontend Assets  &lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup Node &amp;amp; Build&lt;/span&gt;  
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;  
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
        &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;20&lt;/span&gt;  
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;  
        &lt;span class="s"&gt;npm ci  &lt;/span&gt;
        &lt;span class="s"&gt;npm run build  &lt;/span&gt;

    &lt;span class="c1"&gt;# 5️⃣ Prepare Files for Transfer  &lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Archive application&lt;/span&gt;  
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;  
        &lt;span class="s"&gt;tar --exclude='./storage' \  &lt;/span&gt;
            &lt;span class="s"&gt;--exclude='./.git' \  &lt;/span&gt;
            &lt;span class="s"&gt;--exclude='./node_modules' \  &lt;/span&gt;
            &lt;span class="s"&gt;--exclude='./tests' \  &lt;/span&gt;
            &lt;span class="s"&gt;-czf /tmp/release.tar.gz .  &lt;/span&gt;

        &lt;span class="s"&gt;# 2. Move it back to the workspace  &lt;/span&gt;
        &lt;span class="s"&gt;mv /tmp/release.tar.gz .  &lt;/span&gt;

    &lt;span class="c1"&gt;# 6️⃣ Upload to Server  &lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload artifact via SCP&lt;/span&gt;  
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appleboy/scp-action@master&lt;/span&gt;  
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
        &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.VPS_HOST }}&lt;/span&gt;  
        &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.VPS_USERNAME }}&lt;/span&gt;  
        &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.VPS_SSH_KEY }}&lt;/span&gt;  
        &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.VPS_PORT || 22 }}&lt;/span&gt;  
        &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;release.tar.gz"&lt;/span&gt;  
        &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/var/www/laravel-app"&lt;/span&gt;  

    &lt;span class="c1"&gt;# 7️⃣ Deploy on Server  &lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Execute Remote SSH Commands&lt;/span&gt;  
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appleboy/ssh-action@master&lt;/span&gt;  
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
        &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.VPS_HOST }}&lt;/span&gt;  
        &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.VPS_USERNAME }}&lt;/span&gt;  
        &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.VPS_SSH_KEY }}&lt;/span&gt;  
        &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.VPS_PORT || 22 }}&lt;/span&gt;  
        &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;  
          &lt;span class="s"&gt;set -e  &lt;/span&gt;

          &lt;span class="s"&gt;APP_DIR="/var/www/laravel-app"  &lt;/span&gt;
          &lt;span class="s"&gt;RELEASE_ID=$(date +%Y%m%d%H%M%S)  &lt;/span&gt;
          &lt;span class="s"&gt;RELEASE_PATH="$APP_DIR/releases/$RELEASE_ID"  &lt;/span&gt;

          &lt;span class="s"&gt;# 1. Create new release directory  &lt;/span&gt;
          &lt;span class="s"&gt;mkdir -p $RELEASE_PATH  &lt;/span&gt;

          &lt;span class="s"&gt;# 2. Extract files  &lt;/span&gt;
          &lt;span class="s"&gt;tar -xzf $APP_DIR/release.tar.gz -C $RELEASE_PATH  &lt;/span&gt;
          &lt;span class="s"&gt;rm $APP_DIR/release.tar.gz  &lt;/span&gt;

          &lt;span class="s"&gt;# 3. Link Shared Resources  &lt;/span&gt;
          &lt;span class="s"&gt;ln -sfn $APP_DIR/shared/.env $RELEASE_PATH/.env  &lt;/span&gt;
          &lt;span class="s"&gt;ln -sfn $APP_DIR/shared/storage $RELEASE_PATH/storage  &lt;/span&gt;

          &lt;span class="s"&gt;# 4. Set Permissions  &lt;/span&gt;
          &lt;span class="s"&gt;sudo chown -R $USER:www-data $RELEASE_PATH  &lt;/span&gt;

          &lt;span class="s"&gt;# Ensure group write access for cache (so webserver can write to it)  &lt;/span&gt;
          &lt;span class="s"&gt;sudo chmod -R 775 $RELEASE_PATH/bootstrap/cache  &lt;/span&gt;

          &lt;span class="s"&gt;# 5. Run Migrations &amp;amp; Optimize  &lt;/span&gt;
          &lt;span class="s"&gt;cd $RELEASE_PATH  &lt;/span&gt;
          &lt;span class="s"&gt;php artisan migrate --force  &lt;/span&gt;
          &lt;span class="s"&gt;php artisan optimize  &lt;/span&gt;

          &lt;span class="s"&gt;# 6. Atomic Switch (Zero Downtime)  &lt;/span&gt;
          &lt;span class="s"&gt;ln -sfn $RELEASE_PATH $APP_DIR/current  &lt;/span&gt;

          &lt;span class="s"&gt;# 7. Reload PHP-FPM (Ensure this matches your server version!)  &lt;/span&gt;
          &lt;span class="s"&gt;sudo systemctl reload php8.3-fpm  &lt;/span&gt;

          &lt;span class="s"&gt;# 8. Cleanup old releases (keep latest 5)   &lt;/span&gt;
          &lt;span class="s"&gt;cd $APP_DIR/releases  &lt;/span&gt;
          &lt;span class="s"&gt;ls -t | tail -n +6 | xargs -r rm -rf  &lt;/span&gt;

          &lt;span class="s"&gt;echo "🚀 Deployment $RELEASE_ID success!"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In your GitHub repository:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to Settings → Secrets and variables → Actions&lt;/li&gt;
&lt;li&gt;Add these secrets:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;VPS_HOST&lt;/code&gt;: Your VPS IP address&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;VPS_USERNAME&lt;/code&gt;: SSH username&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;VPS_SSH_KEY&lt;/code&gt;: Private SSH key&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;VPS_PORT&lt;/code&gt;: SSH port (default: 22)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 6 — Release-Based Deployment Flow
&lt;/h2&gt;

&lt;p&gt;Each deployment creates a new release directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/var/www/laravel-app/releases/20260210151258
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shared directories such as storage and bootstrap/cache are symlinked into each release.&lt;/p&gt;

&lt;p&gt;Once everything is ready:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Database migrations are executed with &lt;code&gt;--force&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Caches are optimized&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;current&lt;/code&gt; symlink is switched to the new release&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;⚡ Because the symlink update is atomic, &lt;strong&gt;users rarely notice downtime&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting Common Issues
&lt;/h2&gt;

&lt;h3&gt;
  
  
  500 Internal Server Error
&lt;/h3&gt;

&lt;p&gt;Most commonly caused by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Incorrect file permissions&lt;/li&gt;
&lt;li&gt;Missing PHP extensions&lt;/li&gt;
&lt;li&gt;Wrong PHP-FPM socket version&lt;/li&gt;
&lt;li&gt;Broken symlink paths&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check that &lt;code&gt;storage/&lt;/code&gt; and &lt;code&gt;bootstrap/cache/&lt;/code&gt; are writable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Blank Page After Deployment
&lt;/h3&gt;

&lt;p&gt;Often caused by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Incorrect Nginx root path&lt;/li&gt;
&lt;li&gt;Missing &lt;code&gt;.env&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Application key not generated&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Make sure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;APP_ENV=production  
APP_DEBUG=false
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  SSH Action Fails
&lt;/h3&gt;

&lt;p&gt;Usually due to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Incorrect private key format&lt;/li&gt;
&lt;li&gt;Wrong SSH username&lt;/li&gt;
&lt;li&gt;Server firewall rules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Test SSH access manually before debugging CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;This setup gives you a professional, automated pipeline. You push to &lt;code&gt;main&lt;/code&gt;, and GitHub Actions handles the rest — running tests, building assets, and switching the live site without dropping connections.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Happy Deploying! 🚀&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🔗 Stay Connected
&lt;/h2&gt;

&lt;p&gt;Follow me for more Laravel tutorials, dev tips, deployment workflows and solving real-world production headaches.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Follow me on &lt;a href="https://www.linkedin.com/in/houdaifaboucenna/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Follow me here on &lt;a href="https://medium.com/@houdaifaboucenna" rel="noopener noreferrer"&gt;Medium&lt;/a&gt; and &lt;a href="https://medium.com/@houdaifaboucenna/subscribe" rel="noopener noreferrer"&gt;join my mailing list&lt;/a&gt; for more in-depth content and tutorials!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Found this article useful?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
🙏 Show your support by clapping 👏, subscribing 🔔, sharing to social networks&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; Laravel, VPS, GitHub Actions, CI/CD, Nginx&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1 — Installing Server Requirements
&lt;/h2&gt;

&lt;p&gt;After connecting to your server via SSH, it's recommended &lt;strong&gt;not to work as root&lt;/strong&gt;, but as a sudo-enabled user.&lt;/p&gt;

&lt;p&gt;The key idea here is that &lt;strong&gt;this setup is done once&lt;/strong&gt;. After the initial server preparation, deployments should not require installing system dependencies again.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Update system list and upgrade packages  &lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt upgrade &lt;span class="nt"&gt;-y&lt;/span&gt;  

&lt;span class="c"&gt;# Install Nginx  &lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;nginx &lt;span class="nt"&gt;-y&lt;/span&gt;  

&lt;span class="c"&gt;# Install Mysql Server  &lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;mysql-server &lt;span class="nt"&gt;-y&lt;/span&gt;  

&lt;span class="c"&gt;# Install PHP and required Packages  &lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;php-cli php-fpm php-mysql php-xml php-mbstring php-curl unzip &lt;span class="nt"&gt;-y&lt;/span&gt;  

&lt;span class="c"&gt;# Install Composer   &lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; https://getcomposer.org/installer | php  
&lt;span class="nb"&gt;sudo mv &lt;/span&gt;composer.phar /usr/local/bin/composer  

&lt;span class="c"&gt;# Install Node.js and NPM  &lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://deb.nodesource.com/setup_lts.x | &lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; bash -  
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; nodejs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠️ Be careful to install a PHP version that matches your Laravel version requirements. Mismatches here are a common source of production issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — Configuring Nginx for Laravel
&lt;/h2&gt;

&lt;p&gt;We need to configure Nginx to point to the &lt;code&gt;current/public&lt;/code&gt; directory (which we will create later via our CI/CD pipeline). Let's create configs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo touch&lt;/span&gt; /etc/nginx/sites-available/laravel-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;your-domain.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  

    &lt;span class="c1"&gt;# POINT TO 'current/public' FOR ATOMIC DEPLOYMENTS  &lt;/span&gt;
    &lt;span class="kn"&gt;root&lt;/span&gt; &lt;span class="n"&gt;/var/www/laravel-app/current/public&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  

    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;X-Frame-Options&lt;/span&gt; &lt;span class="s"&gt;"SAMEORIGIN"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;X-Content-Type-Options&lt;/span&gt; &lt;span class="s"&gt;"nosniff"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  

    &lt;span class="kn"&gt;index&lt;/span&gt; &lt;span class="s"&gt;index.php&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
    &lt;span class="kn"&gt;charset&lt;/span&gt; &lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
        &lt;span class="kn"&gt;try_files&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt;&lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="n"&gt;/index.php?&lt;/span&gt;&lt;span class="nv"&gt;$query_string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
    &lt;span class="p"&gt;}&lt;/span&gt;  

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;/favicon.ico&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kn"&gt;access_log&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kn"&gt;log_not_found&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;/robots.txt&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kn"&gt;access_log&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kn"&gt;log_not_found&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  

    &lt;span class="kn"&gt;error_page&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt; &lt;span class="n"&gt;/index.php&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt; &lt;span class="sr"&gt;\.php$&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
        &lt;span class="kn"&gt;fastcgi_pass&lt;/span&gt; &lt;span class="s"&gt;unix:/var/run/php/php8.3-fpm.sock&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
        &lt;span class="kn"&gt;fastcgi_index&lt;/span&gt; &lt;span class="s"&gt;index.php&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
        &lt;span class="kn"&gt;fastcgi_param&lt;/span&gt; &lt;span class="s"&gt;SCRIPT_FILENAME&lt;/span&gt; &lt;span class="nv"&gt;$realpath_root$fastcgi_script_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
        &lt;span class="kn"&gt;include&lt;/span&gt; &lt;span class="s"&gt;fastcgi_params&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
    &lt;span class="p"&gt;}&lt;/span&gt;  

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt; &lt;span class="sr"&gt;/\.(?!well-known).*&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
        &lt;span class="kn"&gt;deny&lt;/span&gt; &lt;span class="s"&gt;all&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
    &lt;span class="p"&gt;}&lt;/span&gt;  
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, we enable the configuration and ensure it is working:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /etc/nginx/sites-available/laravel-app /etc/nginx/sites-enabled/  
&lt;span class="nb"&gt;sudo unlink&lt;/span&gt; /etc/nginx/sites-enabled/default  
&lt;span class="nb"&gt;sudo &lt;/span&gt;nginx &lt;span class="nt"&gt;-t&lt;/span&gt;  
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠️ Replace &lt;code&gt;your-domain.com&lt;/code&gt; with your actual domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 — Set up MySQL server and create a database
&lt;/h2&gt;

&lt;p&gt;Let's execute the MySQL security script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Secure MySQL (set root password, remove anonymous users)  &lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;mysql_secure_installation  

&lt;span class="c"&gt;# Log in to MySQL  &lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;mysql &lt;span class="nt"&gt;-u&lt;/span&gt; root &lt;span class="nt"&gt;-p&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, we create the application database that we will use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;laravel_db&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="s1"&gt;'laravel_user'&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="s1"&gt;'localhost'&lt;/span&gt; &lt;span class="n"&gt;IDENTIFIED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="s1"&gt;'your-strong-password'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;PRIVILEGES&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;laravel_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'laravel_user'&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="s1"&gt;'localhost'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
&lt;span class="n"&gt;FLUSH&lt;/span&gt; &lt;span class="k"&gt;PRIVILEGES&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
&lt;span class="n"&gt;EXIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once created, we should add the database access in the production &lt;code&gt;.env&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;⚠️ The &lt;code&gt;.env&lt;/code&gt; is never committed to Git and is created only once on the server. CI/CD pipelines should never modify it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 — Initializing the Application on the Server
&lt;/h2&gt;

&lt;p&gt;Before automation, we need to set up the folder structure. We will create a "&lt;strong&gt;shared&lt;/strong&gt;" structure so that logs and uploads persist between deployments.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Create the main directory  &lt;/span&gt;
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /var/www/laravel-app  
&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="nv"&gt;$USER&lt;/span&gt;:www-data /var/www/laravel-app  

&lt;span class="c"&gt;# 2. Setup the persistent folders  &lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; /var/www/laravel-app  
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; shared/storage/framework/&lt;span class="o"&gt;{&lt;/span&gt;cache/data,sessions,views&lt;span class="o"&gt;}&lt;/span&gt;  

&lt;span class="c"&gt;# 3. Create the .env file  &lt;/span&gt;
nano shared/.env  
&lt;span class="c"&gt;# (Paste your production env content here)  &lt;/span&gt;

&lt;span class="c"&gt;# 4. Set permissions ONE TIME  &lt;/span&gt;
&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="nv"&gt;$USER&lt;/span&gt;:www-data /var/www/laravel-app  
&lt;span class="nb"&gt;sudo chmod&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; 775 shared/storage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 5 — CI/CD Strategy with GitHub Actions
&lt;/h2&gt;

&lt;p&gt;Instead of running Composer and NPM commands directly on the server, the deployment workflow follows a &lt;strong&gt;build-then-deploy&lt;/strong&gt; strategy:&lt;/p&gt;

&lt;h3&gt;
  
  
  Why build in CI instead of the server?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Faster deployments&lt;/li&gt;
&lt;li&gt;More predictable results&lt;/li&gt;
&lt;li&gt;Fewer production dependencies&lt;/li&gt;
&lt;li&gt;Easier debugging&lt;/li&gt;
&lt;li&gt;Reduced server load&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;In this approach:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;GitHub Actions installs PHP and Node&lt;/li&gt;
&lt;li&gt;Composer dependencies are installed&lt;/li&gt;
&lt;li&gt;Frontend assets are built&lt;/li&gt;
&lt;li&gt;A clean release artifact is generated&lt;/li&gt;
&lt;li&gt;The artifact is deployed to the server via SSH&lt;/li&gt;
&lt;li&gt;A new release directory is created&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;current&lt;/code&gt; symlink is updated atomically&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The server becomes a &lt;strong&gt;stable runtime environment&lt;/strong&gt;, not a build machine.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;.github/workflows/deploy.yml&lt;/code&gt; in your Laravel project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy Laravel to VPS&lt;/span&gt;  

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;main&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;  

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;  

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
    &lt;span class="c1"&gt;# 1️⃣ Checkout Code  &lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout repository&lt;/span&gt;  
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;  

    &lt;span class="c1"&gt;# 2️⃣ Setup PHP  &lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup PHP&lt;/span&gt;  
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shivammathur/setup-php@v2&lt;/span&gt;  
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
        &lt;span class="na"&gt;php-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;8.3&lt;/span&gt;  
        &lt;span class="na"&gt;extensions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mbstring, xml, ctype, iconv, intl, pdo_mysql&lt;/span&gt;  

    &lt;span class="c1"&gt;# 3️⃣ Install Backend Dependencies  &lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Composer dependencies&lt;/span&gt;  
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;  
        &lt;span class="s"&gt;composer install --no-dev --prefer-dist --optimize-autoloader  &lt;/span&gt;

    &lt;span class="c1"&gt;# 4️⃣ Build Frontend Assets  &lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup Node &amp;amp; Build&lt;/span&gt;  
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;  
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
        &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;20&lt;/span&gt;  
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;  
        &lt;span class="s"&gt;npm ci  &lt;/span&gt;
        &lt;span class="s"&gt;npm run build  &lt;/span&gt;

    &lt;span class="c1"&gt;# 5️⃣ Prepare Files for Transfer  &lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Archive application&lt;/span&gt;  
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;  
        &lt;span class="s"&gt;tar --exclude='./storage' \  &lt;/span&gt;
            &lt;span class="s"&gt;--exclude='./.git' \  &lt;/span&gt;
            &lt;span class="s"&gt;--exclude='./node_modules' \  &lt;/span&gt;
            &lt;span class="s"&gt;--exclude='./tests' \  &lt;/span&gt;
            &lt;span class="s"&gt;-czf /tmp/release.tar.gz .  &lt;/span&gt;

        &lt;span class="s"&gt;# 2. Move it back to the workspace  &lt;/span&gt;
        &lt;span class="s"&gt;mv /tmp/release.tar.gz .  &lt;/span&gt;

    &lt;span class="c1"&gt;# 6️⃣ Upload to Server  &lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload artifact via SCP&lt;/span&gt;  
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appleboy/scp-action@master&lt;/span&gt;  
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
        &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.VPS_HOST }}&lt;/span&gt;  
        &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.VPS_USERNAME }}&lt;/span&gt;  
        &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.VPS_SSH_KEY }}&lt;/span&gt;  
        &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.VPS_PORT || 22 }}&lt;/span&gt;  
        &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;release.tar.gz"&lt;/span&gt;  
        &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/var/www/laravel-app"&lt;/span&gt;  

    &lt;span class="c1"&gt;# 7️⃣ Deploy on Server  &lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Execute Remote SSH Commands&lt;/span&gt;  
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appleboy/ssh-action@master&lt;/span&gt;  
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
        &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.VPS_HOST }}&lt;/span&gt;  
        &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.VPS_USERNAME }}&lt;/span&gt;  
        &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.VPS_SSH_KEY }}&lt;/span&gt;  
        &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.VPS_PORT || 22 }}&lt;/span&gt;  
        &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;  
          &lt;span class="s"&gt;set -e  &lt;/span&gt;

          &lt;span class="s"&gt;APP_DIR="/var/www/laravel-app"  &lt;/span&gt;
          &lt;span class="s"&gt;RELEASE_ID=$(date +%Y%m%d%H%M%S)  &lt;/span&gt;
          &lt;span class="s"&gt;RELEASE_PATH="$APP_DIR/releases/$RELEASE_ID"  &lt;/span&gt;

          &lt;span class="s"&gt;# 1. Create new release directory  &lt;/span&gt;
          &lt;span class="s"&gt;mkdir -p $RELEASE_PATH  &lt;/span&gt;

          &lt;span class="s"&gt;# 2. Extract files  &lt;/span&gt;
          &lt;span class="s"&gt;tar -xzf $APP_DIR/release.tar.gz -C $RELEASE_PATH  &lt;/span&gt;
          &lt;span class="s"&gt;rm $APP_DIR/release.tar.gz  &lt;/span&gt;

          &lt;span class="s"&gt;# 3. Link Shared Resources  &lt;/span&gt;
          &lt;span class="s"&gt;ln -sfn $APP_DIR/shared/.env $RELEASE_PATH/.env  &lt;/span&gt;
          &lt;span class="s"&gt;ln -sfn $APP_DIR/shared/storage $RELEASE_PATH/storage  &lt;/span&gt;

          &lt;span class="s"&gt;# 4. Set Permissions  &lt;/span&gt;
          &lt;span class="s"&gt;sudo chown -R $USER:www-data $RELEASE_PATH  &lt;/span&gt;

          &lt;span class="s"&gt;# Ensure group write access for cache (so webserver can write to it)  &lt;/span&gt;
          &lt;span class="s"&gt;sudo chmod -R 775 $RELEASE_PATH/bootstrap/cache  &lt;/span&gt;

          &lt;span class="s"&gt;# 5. Run Migrations &amp;amp; Optimize  &lt;/span&gt;
          &lt;span class="s"&gt;cd $RELEASE_PATH  &lt;/span&gt;
          &lt;span class="s"&gt;php artisan migrate --force  &lt;/span&gt;
          &lt;span class="s"&gt;php artisan optimize  &lt;/span&gt;

          &lt;span class="s"&gt;# 6. Atomic Switch (Zero Downtime)  &lt;/span&gt;
          &lt;span class="s"&gt;ln -sfn $RELEASE_PATH $APP_DIR/current  &lt;/span&gt;

          &lt;span class="s"&gt;# 7. Reload PHP-FPM (Ensure this matches your server version!)  &lt;/span&gt;
          &lt;span class="s"&gt;sudo systemctl reload php8.3-fpm  &lt;/span&gt;

          &lt;span class="s"&gt;# 8. Cleanup old releases (keep latest 5)   &lt;/span&gt;
          &lt;span class="s"&gt;cd $APP_DIR/releases  &lt;/span&gt;
          &lt;span class="s"&gt;ls -t | tail -n +6 | xargs -r rm -rf  &lt;/span&gt;

          &lt;span class="s"&gt;echo "🚀 Deployment $RELEASE_ID success!"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In your GitHub repository:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to Settings → Secrets and variables → Actions&lt;/li&gt;
&lt;li&gt;Add these secrets:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;VPS_HOST&lt;/code&gt;: Your VPS IP address&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;VPS_USERNAME&lt;/code&gt;: SSH username&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;VPS_SSH_KEY&lt;/code&gt;: Private SSH key&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;VPS_PORT&lt;/code&gt;: SSH port (default: 22)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 6 — Release-Based Deployment Flow
&lt;/h2&gt;

&lt;p&gt;Each deployment creates a new release directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/var/www/laravel-app/releases/20260210151258
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shared directories such as storage and bootstrap/cache are symlinked into each release.&lt;/p&gt;

&lt;p&gt;Once everything is ready:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Database migrations are executed with &lt;code&gt;--force&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Caches are optimized&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;current&lt;/code&gt; symlink is switched to the new release&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;⚡ Because the symlink update is atomic, &lt;strong&gt;users rarely notice downtime&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting Common Issues
&lt;/h2&gt;

&lt;h3&gt;
  
  
  500 Internal Server Error
&lt;/h3&gt;

&lt;p&gt;Most commonly caused by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Incorrect file permissions&lt;/li&gt;
&lt;li&gt;Missing PHP extensions&lt;/li&gt;
&lt;li&gt;Wrong PHP-FPM socket version&lt;/li&gt;
&lt;li&gt;Broken symlink paths&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check that &lt;code&gt;storage/&lt;/code&gt; and &lt;code&gt;bootstrap/cache/&lt;/code&gt; are writable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Blank Page After Deployment
&lt;/h3&gt;

&lt;p&gt;Often caused by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Incorrect Nginx root path&lt;/li&gt;
&lt;li&gt;Missing &lt;code&gt;.env&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Application key not generated&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Make sure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;APP_ENV=production  
APP_DEBUG=false
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  SSH Action Fails
&lt;/h3&gt;

&lt;p&gt;Usually due to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Incorrect private key format&lt;/li&gt;
&lt;li&gt;Wrong SSH username&lt;/li&gt;
&lt;li&gt;Server firewall rules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Test SSH access manually before debugging CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;This setup gives you a professional, automated pipeline. You push to &lt;code&gt;main&lt;/code&gt;, and GitHub Actions handles the rest — running tests, building assets, and switching the live site without dropping connections.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Happy Deploying! 🚀&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🔗 Stay Connected
&lt;/h2&gt;

&lt;p&gt;Follow me for more Laravel tutorials, dev tips, deployment workflows and solving real-world production headaches.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Follow me on &lt;a href="https://www.linkedin.com/in/houdaifaboucenna/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Follow me here on &lt;a href="https://medium.com/@houdaifaboucenna" rel="noopener noreferrer"&gt;Medium&lt;/a&gt; and &lt;a href="https://medium.com/@houdaifaboucenna/subscribe" rel="noopener noreferrer"&gt;join my mailing list&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Follow me here on &lt;a href="https://dev.to/houdaifa360"&gt;Dev.to&lt;/a&gt; for more in-depth content and tutorials!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Found this article useful?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
🙏 Show your support by clapping 👏, subscribing 🔔, sharing to social networks&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>vps</category>
      <category>githubactions</category>
      <category>cicd</category>
    </item>
    <item>
      <title>I wrote my first article here @dev.to, sharing my solution for deploying a Laravel app in a limited shared hosting environment without SSH access. I hope someone will find it useful, enjoy it, and leave a comment</title>
      <dc:creator>HoudaifaDevBS</dc:creator>
      <pubDate>Sat, 07 Feb 2026 12:08:45 +0000</pubDate>
      <link>https://forem.com/houdaifadev/i-wrote-my-first-article-here-devto-sharing-my-solution-for-deploying-a-laravel-app-in-a-limited-133a</link>
      <guid>https://forem.com/houdaifadev/i-wrote-my-first-article-here-devto-sharing-my-solution-for-deploying-a-laravel-app-in-a-limited-133a</guid>
      <description>&lt;p&gt;

&lt;/p&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://dev.to/houdaifadev/deploying-laravel-on-shared-hosting-no-ssh-required-1kbg" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fff9cx3ecn4ujqn69y4zw.webp" height="auto" class="m-0"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://dev.to/houdaifadev/deploying-laravel-on-shared-hosting-no-ssh-required-1kbg" rel="noopener noreferrer" class="c-link"&gt;
            Deploying Laravel on Shared Hosting (No SSH Required) - DEV Community
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Have you ever wondered if you can deploy a Laravel application on shared hosting without SSH?  The...
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8j7kvp660rqzt99zui8e.png"&gt;
          dev.to
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;




</description>
      <category>laravel</category>
      <category>php</category>
      <category>tutorial</category>
      <category>cpanel</category>
    </item>
    <item>
      <title>Deploying Laravel on Shared Hosting (No SSH Required)</title>
      <dc:creator>HoudaifaDevBS</dc:creator>
      <pubDate>Sat, 07 Feb 2026 12:04:29 +0000</pubDate>
      <link>https://forem.com/houdaifadev/deploying-laravel-on-shared-hosting-no-ssh-required-1kbg</link>
      <guid>https://forem.com/houdaifadev/deploying-laravel-on-shared-hosting-no-ssh-required-1kbg</guid>
      <description>&lt;p&gt;Have you ever wondered if you can deploy a Laravel application on shared hosting without SSH?&lt;/p&gt;

&lt;p&gt;The answer is YES. I faced this exact situation during my early freelance journey: a client with a tight budget needed to see the first version of his Laravel website online before moving forward. The hosting environment had &lt;strong&gt;no SSH, no Composer, no Artisan, no NPM&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So I had to figure out a safe and reliable alternative.&lt;/p&gt;

&lt;p&gt;In this article, I'll share the exact workflow I use to deploy Laravel applications on shared hosting &lt;strong&gt;without SSH access&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Laravel Deployment on Shared Hosting Is Different
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Project File Structure
&lt;/h3&gt;

&lt;p&gt;A typical Laravel project looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;project/  
 ├── app/  
 ├── bootstrap/  
 ├── config/  
 ├── public/  
 ├── storage/  
 ├── vendor/  
 └── artisan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, shared hosting usually exposes only one public directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;├── public_html/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. No SSH, No commands
&lt;/h3&gt;

&lt;p&gt;If there is SSH access, obviously, there will be:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;No composer dependencies can be installed&lt;/li&gt;
&lt;li&gt;No asset building since we no longer have "npm run"&lt;/li&gt;
&lt;li&gt;No migrations can be executed&lt;/li&gt;
&lt;li&gt;No cache clearing or optimizing&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How to solve the situation step-by-step
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Prepare Your Application Locally
&lt;/h3&gt;

&lt;p&gt;First, I clone my Laravel project into a production-ready copy so I don't affect my development setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;├── laravel_app/  
├── laravel_app_prod/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside &lt;code&gt;laravel_app_prod&lt;/code&gt;, I prepare the application for production.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;// clearing the cache  
php artisan config:clear  
php artisan cache:clear  
php artisan route:clear  
php artisan view:clear  

// installing production dependencies and optimize  
composer &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-dev&lt;/span&gt; &lt;span class="nt"&gt;--optimize-autoloader&lt;/span&gt;  

// building frontend assets  
npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2 — Re-organize Project File Structure
&lt;/h3&gt;

&lt;p&gt;To match shared hosting file structure, I move only &lt;code&gt;public/&lt;/code&gt; folder content inside the new &lt;code&gt;public_html/&lt;/code&gt; folder, like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public_html/  &amp;lt;-------------------|  
laravel_app_prod/                 |  
 ├── app/                         |  
 ├── bootstrap/                   |  
 ├── config/                      |  
 ├── public/ ---------------------|  
 ├── storage/             
 ├── vendor/              
 └── artisan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3 — Fix Laravel Paths in index.php
&lt;/h3&gt;

&lt;p&gt;Now, we should do one important step to make the app boot correctly, so we update the paths in &lt;code&gt;public/index.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;file_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;  
    &lt;span class="nv"&gt;$maintenance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;__DIR__&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/../laravel_app_prod/storage/framework/maintenance.php'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="k"&gt;require&lt;/span&gt; &lt;span class="nv"&gt;$maintenance&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
&lt;span class="p"&gt;}&lt;/span&gt;  

&lt;span class="k"&gt;require&lt;/span&gt; &lt;span class="k"&gt;__DIR__&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/../laravel_app_prod/vendor/autoload.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;require_once&lt;/span&gt; &lt;span class="k"&gt;__DIR__&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/../laravel_app_prod/bootstrap/app.php'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;handleRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;capture&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a critical step — incorrect paths will cause a 500 error.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4 — Set Up The Database Using cPanel
&lt;/h3&gt;

&lt;p&gt;From cPanel:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;strong&gt;MySQL Database Wizard&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Create a database&lt;/li&gt;
&lt;li&gt;Create a database user&lt;/li&gt;
&lt;li&gt;Assign the user &lt;strong&gt;ALL PRIVILEGES&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then update the &lt;code&gt;.env&lt;/code&gt; file inside &lt;code&gt;laravel_app_prod&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;APP_NAME=MyApp  
APP_ENV=production  
APP_DEBUG=false          // very important (security)  
APP_URL=https://your-app-domain  

DB_DATABASE=database_name  
DB_USERNAME=database_user  
DB_PASSWORD=database_password  // (you already saved it)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠️ &lt;strong&gt;Never enable&lt;/strong&gt; &lt;code&gt;APP_DEBUG=true&lt;/code&gt; &lt;strong&gt;in production.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5 — Temporary Deploy Route (Token Protected)
&lt;/h3&gt;

&lt;p&gt;Coming to the important part, no SSH, no problem, we can temporarily execute Artisan commands via a &lt;strong&gt;protected web route&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;⚠️ This route &lt;strong&gt;must be removed immediately after deployment&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Add a secret token to &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DEPLOY_TOKEN=verySecretRandomToken123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the route in &lt;code&gt;routes/web.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Support\Facades\Artisan&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Support\Facades\Route&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  

&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/deploy/{token}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
  &lt;span class="nf"&gt;abort_unless&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$token&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'DEPLOY_TOKEN'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  

  &lt;span class="c1"&gt;// 1. Run Migrations &amp;amp; Clear Cache  &lt;/span&gt;
  &lt;span class="nc"&gt;Artisan&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'migrate'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'--force'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;  
  &lt;span class="nc"&gt;Artisan&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'optimize:clear'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  

  &lt;span class="c1"&gt;// 2. Fix Storage Link (The Custom Fix)  &lt;/span&gt;
  &lt;span class="c1"&gt;// We point to the 'public_html' folder using $_SERVER['DOCUMENT_ROOT']  &lt;/span&gt;
  &lt;span class="nv"&gt;$targetFolder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;storage_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'app/public'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  
  &lt;span class="nv"&gt;$linkFolder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'DOCUMENT_ROOT'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/storage'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;file_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$linkFolder&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="nb"&gt;symlink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$targetFolder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$linkFolder&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  
    &lt;span class="nv"&gt;$storageStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Storage link created successfully.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="nv"&gt;$storageStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Storage link already exists.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
  &lt;span class="p"&gt;}&lt;/span&gt;  

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"Deployment completed.&amp;lt;br&amp;gt;"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;  
      &lt;span class="s2"&gt;"Migrations run.&amp;lt;br&amp;gt;"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;  
      &lt;span class="s2"&gt;"Cache cleared.&amp;lt;br&amp;gt;"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;  
      &lt;span class="nv"&gt;$storageStatus&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures only someone with the correct token can trigger deployment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6 — Upload Files to the Server
&lt;/h3&gt;

&lt;p&gt;Using &lt;strong&gt;cPanel File Manager&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Zip &lt;code&gt;laravel_app_prod/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Zip the contents of &lt;code&gt;public/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Upload:

&lt;ul&gt;
&lt;li&gt;Put &lt;code&gt;laravel_app_prod&lt;/code&gt; &lt;strong&gt;outside&lt;/strong&gt; &lt;code&gt;public_html&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Public files &lt;strong&gt;inside&lt;/strong&gt; &lt;code&gt;public_html&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then extract both archives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why did we put&lt;/strong&gt; &lt;code&gt;laravel_app_prod&lt;/code&gt; &lt;strong&gt;outside&lt;/strong&gt; &lt;code&gt;public_html&lt;/code&gt;&lt;strong&gt;?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
This is a crucial security measure. By placing your core application logic, and most importantly your &lt;code&gt;.env&lt;/code&gt; &lt;strong&gt;file&lt;/strong&gt;, one level &lt;em&gt;above&lt;/em&gt; the public directory, you ensure that they are &lt;strong&gt;physically impossible&lt;/strong&gt; to access via a web browser.&lt;/p&gt;

&lt;p&gt;Even if your server misconfigures and starts serving PHP files as text (which happens!), your credentials remain hidden because they live outside the public root.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 7 — Set Critical Permissions
&lt;/h3&gt;

&lt;p&gt;Laravel requires write access to specific folders. If these aren't set, your logs won't write and sessions won't save.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In cPanel File Manager, navigate to &lt;code&gt;laravel_app_prod/storage&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Right-click → &lt;strong&gt;Change Permissions&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Set to &lt;strong&gt;775&lt;/strong&gt; (User: Read/Write/Execute, Group: Read/Write/Execute, World: Read/Execute).&lt;/li&gt;
&lt;li&gt;Do the same for &lt;code&gt;laravel_app_prod/bootstrap/cache&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;🛑 &lt;strong&gt;&lt;em&gt;Note&lt;/em&gt;&lt;/strong&gt;: Never set folders to 777. It is a major security risk on shared hosting.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 8 — Run Migrations and Remove the Deploy Route
&lt;/h3&gt;

&lt;p&gt;Open in the browser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://your-domain.com/deploy/verySecretRandomToken123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If everything is correct, you'll see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Deployment completed successfully
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Immediately after that:&lt;/p&gt;

&lt;p&gt;❗ Delete the deploy route&lt;br&gt;&lt;br&gt;
❗ Remove &lt;code&gt;DEPLOY_TOKEN&lt;/code&gt; from &lt;code&gt;.env&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This is critical for security.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Issues Faced
&lt;/h2&gt;

&lt;h3&gt;
  
  
  500 Internal Server Error
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Incorrect file paths in &lt;code&gt;index.php&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Wrong PHP version (use cPanel &lt;strong&gt;MultiPHP Manager&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;Incorrect folder permissions (&lt;code&gt;storage 775&lt;/code&gt;, &lt;code&gt;bootstrap/cache 775&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  403 Unauthorized
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Unmatched route token with &lt;code&gt;DEPLOY_TOKEN&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Shared hosting is not ideal for Laravel, but it's still very common for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Client projects&lt;/li&gt;
&lt;li&gt;MVPs&lt;/li&gt;
&lt;li&gt;Budget-constrained deployments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With proper preparation and a secure workflow, Laravel applications can run reliably even &lt;strong&gt;without SSH access&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;💡 Pro Tip: The Safer Database Alternative&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
If you are uncomfortable running migrations via a web route (which carries security risks), you can use the &lt;strong&gt;Export/Import method&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run migrations on your &lt;strong&gt;local&lt;/strong&gt; machine.&lt;/li&gt;
&lt;li&gt;Export your local database as an &lt;code&gt;.sql&lt;/code&gt; file using your local tool.&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;cPanel &amp;gt; phpMyAdmin&lt;/strong&gt; on the server.&lt;/li&gt;
&lt;li&gt;Import the &lt;code&gt;.sql&lt;/code&gt; file directly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is safer because it doesn't require executable logic in your production routes.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔗 Stay Connected
&lt;/h2&gt;

&lt;p&gt;Follow me for more Laravel tutorials, dev tips, deployment workflows and solving real-world production headaches.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Follow me on &lt;a href="https://www.linkedin.com/in/houdaifaboucenna/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Follow me here on &lt;a href="https://medium.com/@houdaifaboucenna" rel="noopener noreferrer"&gt;Medium&lt;/a&gt; and &lt;a href="https://medium.com/@houdaifaboucenna/subscribe" rel="noopener noreferrer"&gt;join my mailing list&lt;/a&gt; for more in-depth content and tutorials!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Found this article useful?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
🙏 Show your support by clapping 👏, subscribing 🔔, sharing to social networks&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>tutorial</category>
      <category>cpanel</category>
    </item>
  </channel>
</rss>
