<?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>DEV Community: Dan Storm</title>
    <description>The latest articles on DEV Community by Dan Storm (@repox).</description>
    <link>https://dev.to/repox</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%2F3079114%2F3247c718-9812-49b2-acd6-1f9636bb6fc7.png</url>
      <title>DEV Community: Dan Storm</title>
      <link>https://dev.to/repox</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/repox"/>
    <language>en</language>
    <item>
      <title>Managing Laravel Queues Efficiently With FrankenPHP, Redis and Docker</title>
      <dc:creator>Dan Storm</dc:creator>
      <pubDate>Tue, 20 May 2025 12:27:17 +0000</pubDate>
      <link>https://dev.to/repox/managing-laravel-queues-efficiently-with-frankenphp-redis-and-docker-36mh</link>
      <guid>https://dev.to/repox/managing-laravel-queues-efficiently-with-frankenphp-redis-and-docker-36mh</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Learn how to manage Laravel queues using Redis, Docker, and FrankenPHP. This guide walks through setting up a queue worker, dispatching jobs, and integrating Laravel Horizon — all within a Dockerized environment.&lt;/p&gt;

&lt;p&gt;Continuing from the previous article on &lt;a href="https://dev.to/blog/getting-started-with-frankenphp-and-laravel/"&gt;getting started with FrankenPHP and Docker&lt;/a&gt; we will now focus on managing Laravel queues using Redis and Docker. This setup is ideal for handling background tasks in a Laravel application, ensuring that your web server remains responsive while processing jobs asynchronously.&lt;/p&gt;

&lt;p&gt;To get up and running quickly, we can use the &lt;a href="https://github.com/Repox/laravel-frankenphp-docker" rel="noopener noreferrer"&gt;demo repository&lt;/a&gt; that contains a sample Laravel application configured with Redis and Docker with FrankenPHP. &lt;/p&gt;

&lt;p&gt;This article assumes that you know what Laravel queues are and how they work. If you are new to Laravel queues, I recommend checking out the &lt;a href="https://laravel.com/docs/12.x/queues" rel="noopener noreferrer"&gt;official documentation&lt;/a&gt; for a comprehensive understanding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before we begin, ensure you have the following installed on your machine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.docker.com/" rel="noopener noreferrer"&gt;Docker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/compose/" rel="noopener noreferrer"&gt;Docker Compose&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://git-scm.com/" rel="noopener noreferrer"&gt;Git&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://getcomposer.org/" rel="noopener noreferrer"&gt;Composer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.php.net/" rel="noopener noreferrer"&gt;PHP&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Getting Started
&lt;/h3&gt;

&lt;p&gt;You can either clone the demo repository or create a new Laravel application from scratch, based on &lt;a href="https://dev.to/blog/getting-started-with-frankenphp-and-laravel/"&gt;previous article&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;The rest of this article will assume you are using the &lt;code&gt;docker-compose.yml&lt;/code&gt; from the demo repository (or the one from the previous article) as a starting point, and that you have a working Laravel application.&lt;/p&gt;

&lt;p&gt;Remember to generate the application key and set up your &lt;code&gt;.env&lt;/code&gt; file with the necessary configurations. You can do this by running (skip any steps that you already have done):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer &lt;span class="nb"&gt;install
cp&lt;/span&gt; .env.example .env
docker compose build &lt;span class="nt"&gt;--no-cache&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
docker compose run &lt;span class="nt"&gt;--rm&lt;/span&gt; artisan key:generate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;To use Redis for queues in your Laravel application, you need to configure the &lt;code&gt;.env&lt;/code&gt; file. Open the &lt;code&gt;.env&lt;/code&gt; file and set the following values:&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
REDIS_HOST=redis
REDIS_PASSWORD=null
REDIS_PORT=6379
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the basic setup for using Redis as the queue driver. The &lt;code&gt;REDIS_HOST&lt;/code&gt; is set to &lt;code&gt;redis&lt;/code&gt;, which is the name of the Redis service defined in the &lt;code&gt;docker-compose.yml&lt;/code&gt; file. &lt;/p&gt;

&lt;p&gt;⚠️ Security tip: For local development, the Redis container is fine as-is. But in production environments, it's important to secure Redis properly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use authentication&lt;/li&gt;
&lt;li&gt;Bind only to trusted networks&lt;/li&gt;
&lt;li&gt;Avoid exposing Redis to the public internet&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can find more details in the &lt;a href="https://hub.docker.com/_/redis" rel="noopener noreferrer"&gt;official Redis Docker Hub documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating and dispatching a Job
&lt;/h3&gt;

&lt;p&gt;We need to have something to queue. We'll create a simple job that outputs a message to our log file. You can create a job using the Artisan command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose run &lt;span class="nt"&gt;--rm&lt;/span&gt; artisan make:job LogMessageJob
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will create a new job class in the &lt;code&gt;app/Jobs&lt;/code&gt; directory. Open the newly created &lt;code&gt;LogMessageJob.php&lt;/code&gt; file and modify the &lt;code&gt;handle&lt;/code&gt; method to log a message:&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="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Jobs&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\Contracts\Queue\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;Illuminate\Foundation\Queue\Queueable&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\Log&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LogMessageJob&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;Queueable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="cd"&gt;/**
     * Create a new job instance.
     */&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="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;//&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="cd"&gt;/**
     * Execute the job.
     */&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;Log&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'LogMessageJob executed successfully'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'timestamp'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;now&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;For simplicity, we are not passing any data to the job. You can modify this to suit your needs. Let's queue the job in the &lt;code&gt;routes/web.php&lt;/code&gt; file:&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="cp"&gt;&amp;lt;?php&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;'/log-message'&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="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Dispatch the job to the queue&lt;/span&gt;
    &lt;span class="nf"&gt;dispatch&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;\App\Jobs\LogMessageJob&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;'Job dispatched!'&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;Now, when you visit the &lt;code&gt;/log-message&lt;/code&gt; route, it will dispatch the &lt;code&gt;LogMessageJob&lt;/code&gt; to the Redis queue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost/log-message
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see the message "Job dispatched!" in your browser.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing the Queue Worker
&lt;/h3&gt;

&lt;p&gt;At this point, let’s verify that the job has been properly queued. You can do this by running the following command:&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="nv"&gt;$ &lt;/span&gt;docker compose run &lt;span class="nt"&gt;--rm&lt;/span&gt; artisan queue:monitor default
  &lt;span class="c"&gt;# Should ouput something like this&lt;/span&gt;
  Queue name ............................................................................................ Size / Status
  &lt;span class="o"&gt;[&lt;/span&gt;redis] default .............................................................................................. &lt;span class="o"&gt;[&lt;/span&gt;1] OK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This verifies that we have a job queued in the &lt;code&gt;default&lt;/code&gt; queue. For this article, we will use the &lt;code&gt;default&lt;/code&gt; queue.&lt;/p&gt;

&lt;p&gt;Let's verify that our job can be processed, before queuing more jobs. You can do this by running the following command:&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="nv"&gt;$ &lt;/span&gt;docker compose run &lt;span class="nt"&gt;--rm&lt;/span&gt; artisan queue:work &lt;span class="nt"&gt;--once&lt;/span&gt;
  &lt;span class="c"&gt;# should output something like this&lt;/span&gt;
   INFO  Processing &lt;span class="nb"&gt;jobs &lt;/span&gt;from the &lt;span class="o"&gt;[&lt;/span&gt;default] queue.

  2025-05-20 10:54:19 App&lt;span class="se"&gt;\J&lt;/span&gt;obs&lt;span class="se"&gt;\L&lt;/span&gt;ogMessageJob .................................................................. RUNNING
  2025-05-20 10:54:19 App&lt;span class="se"&gt;\J&lt;/span&gt;obs&lt;span class="se"&gt;\L&lt;/span&gt;ogMessageJob ............................................................ 226.54ms DONE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will process the first job in the queue and then exit. You should see a message in your log file indicating that the job was executed successfully.&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="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;storage/logs/laravel.log
  &lt;span class="o"&gt;[&lt;/span&gt;2025-05-20 10:55:27] local.INFO: LogMessageJob executed successfully &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"timestamp"&lt;/span&gt;:&lt;span class="s2"&gt;"2025-05-20 10:55:27"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hopefully, everything works as expected so far. If you see any errors, please check the logs for more information.&lt;/p&gt;

&lt;h3&gt;
  
  
  Running the Queue Worker in the background
&lt;/h3&gt;

&lt;p&gt;Now that we know that our job can be processed, we want to run the queue worker in the background. Docker is great for this, as we can run the queue worker in a separate container.&lt;/p&gt;

&lt;p&gt;ℹ️ we can run Artisan commands with the PHP binary inside the container (located at &lt;code&gt;/usr/local/bin/php&lt;/code&gt;).&lt;br&gt;&lt;br&gt;
For this guide, the purpose is to leverage FrankenPHP's CLI capabilities to run the queue worker, which can be accessed with &lt;code&gt;/usr/local/bin/frankenphp php-cli&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Let's set up a Service in our Docker Compose setup, that will run the worker as a separate container. Open the &lt;code&gt;docker-compose.yml&lt;/code&gt; file and add the following service:&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="c1"&gt;# New Service&lt;/span&gt;
  &lt;span class="na"&gt;worker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;laravel-app&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.:/app"&lt;/span&gt;
    &lt;span class="na"&gt;working_dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/app&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;frankenphp&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;php-cli&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;artisan&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;queue:work"&lt;/span&gt;
    &lt;span class="c1"&gt;# Let's make sure that Redis and our Database is up and running as well.&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; 
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;redis&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mysql&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will create a new service called &lt;code&gt;worker&lt;/code&gt; that will run the queue worker in the background. The &lt;code&gt;depends_on&lt;/code&gt; option ensures that the Redis and MySQL services are up and running before starting the worker.&lt;br&gt;
You can start the worker by running the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; worker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will start the worker in the background. You can check the logs of the worker by running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; worker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Having an extra terminal open, you can now follow the logs while dispatching jobs. You can do this by running the following command, to dispatch a new job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost/log-message
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see the job being processed in the logs of the worker container:&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="nv"&gt;$ &lt;/span&gt;docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; worker
  worker-1  |   2025-05-20 11:09:58 App&lt;span class="se"&gt;\J&lt;/span&gt;obs&lt;span class="se"&gt;\L&lt;/span&gt;ogMessageJob ......................... RUNNING
  worker-1  |   2025-05-20 11:09:58 App&lt;span class="se"&gt;\J&lt;/span&gt;obs&lt;span class="se"&gt;\L&lt;/span&gt;ogMessageJob ................... 211.46ms DONE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can now dispatch as many jobs as you want, and the worker will process them in the background. You can also run multiple workers by running the &lt;code&gt;docker compose up -d worker&lt;/code&gt; command multiple times. Each worker will process jobs from the same queue, allowing you to scale your application easily.&lt;/p&gt;

&lt;p&gt;ℹ️ As a bonus info, you can use the same approach for scheduling tasks in your Laravel application. You can create a new service in your &lt;code&gt;docker-compose.yml&lt;/code&gt; file that runs the &lt;code&gt;schedule:work&lt;/code&gt; command. The documentation states that &lt;code&gt;schedule:work&lt;/code&gt; can be used for &lt;a href="https://laravel.com/docs/12.x/scheduling#running-the-scheduler-locally" rel="noopener noreferrer"&gt;local development&lt;/a&gt;, but doesn't directly mention that it can be used in production.&lt;br&gt;&lt;br&gt;
However, it is a great way to run scheduled tasks in the background, without having to set up a cron job.&lt;/p&gt;
&lt;h3&gt;
  
  
  Laravel Horizon ... and FrankenPHP
&lt;/h3&gt;

&lt;p&gt;Running multiple workers is great, but it can be hard to manage them. This is where &lt;a href="https://laravel.com/docs/12.x/horizon" rel="noopener noreferrer"&gt;Laravel Horizon&lt;/a&gt; comes in. Horizon provides a beautiful dashboard and code-driven configuration for your Redis queues. &lt;/p&gt;

&lt;p&gt;Managing this in a containerized environment is a bit tricky, but it is possible. There are some caveats with FrankenPHP, so if you want to use Horizon without FrankenPHP it is doable by using the &lt;code&gt;php artisan horizon&lt;/code&gt; command, referencing the local PHP binary instead. &lt;/p&gt;

&lt;p&gt;This particular section will make it possible for you to run Horizon with FrankenPHP.&lt;/p&gt;
&lt;h4&gt;
  
  
  Installing Horizon
&lt;/h4&gt;

&lt;p&gt;You can install Horizon by running the following command:&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 laravel/horizon
docker compose run &lt;span class="nt"&gt;--rm&lt;/span&gt; artisan horizon:install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will install Horizon and publish the configuration file to &lt;code&gt;config/horizon.php&lt;/code&gt;. You can customize the configuration file to suit your needs.&lt;/p&gt;

&lt;p&gt;ℹ️ You can customize Horizon's behavior in &lt;code&gt;config/horizon.php&lt;/code&gt; or via environment variables (e.g., &lt;code&gt;HORIZON_PREFIX&lt;/code&gt;, &lt;code&gt;QUEUE_CONNECTION&lt;/code&gt;).&lt;/p&gt;

&lt;h4&gt;
  
  
  What's the problem?
&lt;/h4&gt;

&lt;p&gt;Horizon relies on the &lt;a href="https://www.php.net/manual/en/reserved.constants.php#constant.php-binary" rel="noopener noreferrer"&gt;&lt;code&gt;PHP_BINARY&lt;/code&gt;&lt;/a&gt; constant to determine which PHP binary to use. Unfortunately, &lt;a href="https://github.com/dunglas/frankenphp/discussions/1477" rel="noopener noreferrer"&gt;FrankenPHP doesn’t set this constant&lt;/a&gt;, leading to a Permission denied error when Horizon attempts to launch workers or supervisors.&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="nv"&gt;$ &lt;/span&gt;docker compose run &lt;span class="nt"&gt;--rm&lt;/span&gt; artisan horizon

     INFO  Horizon started successfully.

   sh: 1: &lt;span class="nb"&gt;exec&lt;/span&gt;: : Permission denied
   sh: 1: &lt;span class="nb"&gt;exec&lt;/span&gt;: : Permission denied
   sh: 1: &lt;span class="nb"&gt;exec&lt;/span&gt;: : Permission denied
   ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is because Horizon tries to run the &lt;code&gt;php&lt;/code&gt; command, but &lt;code&gt;PHP_BINARY&lt;/code&gt; is empty when using FrankenPHP, so it doesn't know which binary to execute.&lt;/p&gt;

&lt;h4&gt;
  
  
  Fixing the problem
&lt;/h4&gt;

&lt;p&gt;The quick fix is to &lt;em&gt;not&lt;/em&gt; use FrankenPHP's &lt;code&gt;php-cli&lt;/code&gt; command, but instead use the local PHP binary. But as mentioned, the purpose of this article is to use FrankenPHP for everything, so we need to fix this.&lt;/p&gt;

&lt;p&gt;Here's a workaround to make it work with FrankenPHP:&lt;/p&gt;

&lt;p&gt;In your &lt;code&gt;AppServiceProvider&lt;/code&gt;, add the following to your &lt;code&gt;boot&lt;/code&gt; method:&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;Laravel\Horizon\SupervisorCommandString&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;Laravel\Horizon\WorkerCommandString&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AppServiceProvider&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;ServiceProvider&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;boot&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;SupervisorCommandString&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nv"&gt;$command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'exec /usr/local/bin/frankenphp php-cli artisan horizon:supervisor'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nc"&gt;WorkerCommandString&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nv"&gt;$command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'exec /usr/local/bin/frankenphp php-cli artisan horizon:work'&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;This will override the default command used by Horizon to run the supervisor and worker commands. Now you can run Horizon with FrankenPHP:&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="nv"&gt;$ &lt;/span&gt;docker compose run &lt;span class="nt"&gt;--rm&lt;/span&gt; artisan horizon

     INFO  Horizon started successfully.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Putting it into the Docker Compose setup
&lt;/h4&gt;

&lt;p&gt;Now that we have Horizon working with FrankenPHP, we can add it to our Docker Compose setup. Open the &lt;code&gt;docker-compose.yml&lt;/code&gt; file and add the following service:&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="c1"&gt;# New Service&lt;/span&gt;
  &lt;span class="na"&gt;horizon&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;laravel-app&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.:/app"&lt;/span&gt;
    &lt;span class="na"&gt;working_dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/app&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;frankenphp&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;php-cli&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;artisan&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;horizon"&lt;/span&gt;
    &lt;span class="c1"&gt;# Horizon allows for a working healthcheck, so let's add one&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;frankenphp"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;php-cli"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;artisan"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;horizon:status"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3&lt;/span&gt;    
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; 
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;redis&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mysql&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will create a new service called &lt;code&gt;horizon&lt;/code&gt; that will run the Horizon command in the background. You can start Horizon by running the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; horizon
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Accessing the Horizon dashboard is as simple as visiting &lt;code&gt;http://localhost/horizon&lt;/code&gt; in your browser. You should see the Horizon dashboard, where you can monitor your queues and jobs.&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%2Fblog.danstorm.dev%2Fimages%2Flaravel_horizon_dashboard.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%2Fblog.danstorm.dev%2Fimages%2Flaravel_horizon_dashboard.png" title="Horizon Dashboard" alt="Laravel Horizon dashboard showing active workers and job metrics" width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>docker</category>
      <category>frankenphp</category>
    </item>
    <item>
      <title>Getting started with FrankenPHP, Laravel and Docker</title>
      <dc:creator>Dan Storm</dc:creator>
      <pubDate>Wed, 23 Apr 2025 11:12:02 +0000</pubDate>
      <link>https://dev.to/repox/getting-started-with-frankenphp-laravel-and-docker-11p1</link>
      <guid>https://dev.to/repox/getting-started-with-frankenphp-laravel-and-docker-11p1</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Learn how to run a Laravel app with MySQL using FrankenPHP and Docker—from local dev to production-ready builds. &lt;a href="https://github.com/Repox/laravel-frankenphp-docker" rel="noopener noreferrer"&gt; GitHub Repo&lt;/a&gt; for demo.&lt;/p&gt;

&lt;p&gt;This blog post assumes that you either already know what &lt;a href="https://frankenphp.dev/" rel="noopener noreferrer"&gt;FrankenPHP&lt;/a&gt; is and want to get started - or that you might not worry so much about the inner workings, but just want to try it out.&lt;/p&gt;

&lt;p&gt;This is an opinionated way of working with the tech stack, and you should be able to adapt it to your needs.&lt;/p&gt;

&lt;p&gt;What I wanted to achieve was running a Laravel application with a MySQL database - and it should all run in a Docker Compose environment.&lt;/p&gt;

&lt;p&gt;I've published a demo repository for this blog post, which you can find &lt;a href="https://github.com/Repox/laravel-frankenphp-docker" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Requirements
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.docker.com/" rel="noopener noreferrer"&gt;Docker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/compose/" rel="noopener noreferrer"&gt;Docker Compose&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://git-scm.com/" rel="noopener noreferrer"&gt;Git&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://getcomposer.org/" rel="noopener noreferrer"&gt;Composer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.php.net/" rel="noopener noreferrer"&gt;PHP&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Installing Laravel
&lt;/h3&gt;

&lt;p&gt;While Laravel has different &lt;a href="https://laravel.com/docs/12.x/installation" rel="noopener noreferrer"&gt;installation options&lt;/a&gt;, my preferred way is just using Composer's &lt;code&gt;create-project&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;composer create-project laravel/laravel my-project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will create a Laravel project in the &lt;code&gt;./my-project&lt;/code&gt; folder.&lt;/p&gt;

&lt;h3&gt;
  
  
  Docker Compose
&lt;/h3&gt;

&lt;p&gt;Obviously, you could use &lt;a href="https://laravel.com/docs/12.x/sail" rel="noopener noreferrer"&gt;Laravel Sail&lt;/a&gt; and you'd be up and running in no time with a local docker development environment.&lt;/p&gt;

&lt;p&gt;I actually did this for a long time, I was quite happy with it. But I still needed to do something different for production deployments, as Laravel Sail's containers aren't meant for production environments.&lt;/p&gt;

&lt;p&gt;Laravel can actually run with the bare minimum that FrankenPHP provides - but since I need a MySQL database, I need to install some extensions for FrankenPHP to allow Laravel to communicate with the database. So let's create a Dockerfile in our root project for that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; dunglas/frankenphp&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;install-php-extensions &lt;span class="se"&gt;\
&lt;/span&gt;    pdo_mysql

&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; SERVER_NAME=:80&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, let's use this for our Docker Compose file:&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;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;laravel-app&lt;/span&gt;
        &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
            &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Dockerfile&lt;/span&gt;
        &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;80:80'&lt;/span&gt;
        &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.:/app'&lt;/span&gt;
        &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mysql&lt;/span&gt;
    &lt;span class="na"&gt;mysql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;mysql/mysql-server:8.0'&lt;/span&gt;
        &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3306:3306'&lt;/span&gt;
        &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;MYSQL_ROOT_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${DB_PASSWORD}'&lt;/span&gt;
            &lt;span class="na"&gt;MYSQL_ROOT_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%'&lt;/span&gt;
            &lt;span class="na"&gt;MYSQL_DATABASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${DB_DATABASE}'&lt;/span&gt;
            &lt;span class="na"&gt;MYSQL_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${DB_USERNAME}'&lt;/span&gt;
            &lt;span class="na"&gt;MYSQL_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${DB_PASSWORD}'&lt;/span&gt;
            &lt;span class="na"&gt;MYSQL_ALLOW_EMPTY_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
        &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;mysql:/var/lib/mysql'&lt;/span&gt;
        &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CMD'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;mysqladmin'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ping'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-p${DB_PASSWORD}'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
            &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
            &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mysql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For MySQL credentials, I'm referencing the environment variables defined in the &lt;code&gt;.env&lt;/code&gt; file. What's relevant here is the following lines in your &lt;code&gt;.env&lt;/code&gt; file:&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="nv"&gt;DB_CONNECTION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mysql
&lt;span class="nv"&gt;DB_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mysql
&lt;span class="nv"&gt;DB_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3306
&lt;span class="nv"&gt;DB_DATABASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;laravel
&lt;span class="nv"&gt;DB_USERNAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;laravel
&lt;span class="nv"&gt;DB_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;password
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm also exposing port 3306 for allowing to connect to the database with your preferred database tool.&lt;/p&gt;

&lt;p&gt;And that's it! You should be able to run &lt;code&gt;docker compose up&lt;/code&gt; and then open your browser to &lt;code&gt;http://localhost&lt;/code&gt; and you should see the Laravel application running on &lt;a href="http://localhost" rel="noopener noreferrer"&gt;http://localhost&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Running your database migrations should be done within your container network. You can achieve this by running the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose run &lt;span class="nt"&gt;--rm&lt;/span&gt; app php artisan migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Laravel Octane
&lt;/h3&gt;

&lt;p&gt;One of the main benefits &lt;a href="https://laravel.com/docs/12.x/octane" rel="noopener noreferrer"&gt;Laravel Octane&lt;/a&gt; is that it allows you to run your application in a single thread, and this should allow for faster performance. While, Laravel Octane supports FrankenPHP, it is worth considering whether you actually need it.&lt;/p&gt;

&lt;p&gt;A &lt;a href="https://github.com/dunglas/frankenphp/pull/933" rel="noopener noreferrer"&gt;pull request&lt;/a&gt; improved performance for CGI mode for FrankenPHP which indicates a performance up to par with worker mode. You can obviously test it out and see if it's necessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  What next?
&lt;/h2&gt;

&lt;p&gt;As earlier mentioned, one of my issues with Laravel Sail is that I still had to do something else with my production container. So let's mature our development setup for something a bit more useful.&lt;/p&gt;

&lt;h3&gt;
  
  
  Building a production-ready container
&lt;/h3&gt;

&lt;p&gt;We can use multiple targets in our Dockerfile to build a production-ready container, and a development container. &lt;/p&gt;

&lt;p&gt;In my case, &lt;a href="https://github.com/mlocati/docker-php-extension-installer?tab=readme-ov-file#supported-php-extensions" rel="noopener noreferrer"&gt;I've added some extensions&lt;/a&gt; that I've deemed useful for a number of different things that I've been developing, but you should probably check if you need them all or other extensions.&lt;/p&gt;

&lt;p&gt;Additionally, as I'm serving my container behind a reverse proxy, I'll still be serving my application on port 80.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;dunglas/frankenphp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;install-php-extensions &lt;span class="se"&gt;\
&lt;/span&gt;    pdo_mysql &lt;span class="se"&gt;\
&lt;/span&gt;    redis &lt;span class="se"&gt;\
&lt;/span&gt;    zip &lt;span class="se"&gt;\
&lt;/span&gt;    opcache &lt;span class="se"&gt;\
&lt;/span&gt;    intl &lt;span class="se"&gt;\
&lt;/span&gt;    pcntl

&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; SERVER_NAME=:80&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;production&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;mv&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PHP_INI_DIR&lt;/span&gt;&lt;span class="s2"&gt;/php.ini-production"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PHP_INI_DIR&lt;/span&gt;&lt;span class="s2"&gt;/php.ini"&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . /app&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My &lt;code&gt;base&lt;/code&gt; target is used for development use, but my &lt;code&gt;production&lt;/code&gt; target is used for production use. I'm using a &lt;a href="https://github.com/features/actions" rel="noopener noreferrer"&gt;GitHub Actions&lt;/a&gt; workflow to checkout my code, installing dependencies without development dependencies, and building my application. When that's done, I build the Docker image and send it to my container registry.&lt;/p&gt;

&lt;h3&gt;
  
  
  Docker Compose with multiple services
&lt;/h3&gt;

&lt;p&gt;I'm extending my &lt;code&gt;docker-compose.yml&lt;/code&gt; file with some additional services. You can of course pick and choose as you see fit, but I'm including the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MySQL

&lt;ul&gt;
&lt;li&gt;Redis&lt;/li&gt;
&lt;li&gt;Artisan (just a helper for running artisan commands)
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;laravel-app&lt;/span&gt;
        &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
            &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Dockerfile&lt;/span&gt;
            &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;base&lt;/span&gt;
        &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;80:80'&lt;/span&gt;
        &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.:/app'&lt;/span&gt;
        &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mysql&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;redis&lt;/span&gt;
    &lt;span class="na"&gt;mysql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;mysql/mysql-server:8.0'&lt;/span&gt;
        &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3306:3306'&lt;/span&gt;
        &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;MYSQL_ROOT_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${DB_PASSWORD}'&lt;/span&gt;
            &lt;span class="na"&gt;MYSQL_ROOT_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%'&lt;/span&gt;
            &lt;span class="na"&gt;MYSQL_DATABASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${DB_DATABASE}'&lt;/span&gt;
            &lt;span class="na"&gt;MYSQL_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${DB_USERNAME}'&lt;/span&gt;
            &lt;span class="na"&gt;MYSQL_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${DB_PASSWORD}'&lt;/span&gt;
            &lt;span class="na"&gt;MYSQL_ALLOW_EMPTY_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
        &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;mysql:/var/lib/mysql'&lt;/span&gt;
        &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CMD'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;mysqladmin'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ping'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-p${DB_PASSWORD}'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
            &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
            &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
    &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;redis:alpine'&lt;/span&gt;
        &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;6379:6379'&lt;/span&gt;
        &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;redis:/data'&lt;/span&gt;
        &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CMD'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;redis-cli'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ping'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
            &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
            &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
    &lt;span class="na"&gt;artisan&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.:/app"&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;laravel-app&lt;/span&gt;
        &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mysql&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;redis&lt;/span&gt;
        &lt;span class="na"&gt;entrypoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;frankenphp&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;php-cli&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;artisan'&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;mysql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we constructed a Docker Compose file that's ready to use, and we can run &lt;code&gt;docker compose up&lt;/code&gt; to start our application.&lt;/p&gt;

&lt;p&gt;To run database migrations, we can use the dedicated service for that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose run &lt;span class="nt"&gt;--rm&lt;/span&gt; artisan migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will use the container's internal network to connect to the database, enabling you to run migrations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;As prefaced, this is an opinionated approach. If you need to adapt it to your needs, that's fine. But I think you'll find this to be a useful starting point. &lt;/p&gt;

&lt;p&gt;You only need an &lt;code&gt;.env.production&lt;/code&gt; file to have a production-ready container, suited to your production environment.&lt;/p&gt;

&lt;p&gt;Remember to update &lt;code&gt;APP_ENV&lt;/code&gt; and &lt;code&gt;APP_KEY&lt;/code&gt; in your &lt;code&gt;.env&lt;/code&gt; file. Generating a new &lt;code&gt;APP_KEY&lt;/code&gt; is easily done by running &lt;code&gt;php artisan key:generate&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;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;--prefer-dist&lt;/span&gt; &lt;span class="nt"&gt;--optimize-autoloader&lt;/span&gt;
php artisan optimize
docker build &lt;span class="nt"&gt;-t&lt;/span&gt; laravel-app &lt;span class="nt"&gt;--target&lt;/span&gt; production &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can now use your tagged Docker image to run your application in production.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cleaning up
&lt;/h3&gt;

&lt;p&gt;When you're done with the application, you can run &lt;code&gt;docker compose down -v&lt;/code&gt; to clean up the containers and volumes.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>docker</category>
      <category>frankenphp</category>
    </item>
  </channel>
</rss>
