Managing Laravel Queues Efficiently With FrankenPHP, Redis and Docker
Introduction
TL;DR: 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.
Continuing from the previous article on getting started with FrankenPHP and Docker 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.
To get up and running quickly, we can use the demo repository that contains a sample Laravel application configured with Redis and Docker with FrankenPHP.
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 official documentation for a comprehensive understanding.
Prerequisites
Before we begin, ensure you have the following installed on your machine:
Getting Started
You can either clone the demo repository or create a new Laravel application from scratch, based on previous article.
The rest of this article will assume you are using the docker-compose.yml
from the demo repository (or the one from the previous article) as a starting point, and that you have a working Laravel application.
Remember to generate the application key and set up your .env
file with the necessary configurations. You can do this by running (skip any steps that you already have done):
composer install
cp .env.example .env
docker compose build --no-cache
docker compose up -d
docker compose run --rm artisan key:generate
Configuration
To use Redis for queues in your Laravel application, you need to configure the .env
file. Open the .env
file and set the following values:
QUEUE_CONNECTION=redis
REDIS_HOST=redis
REDIS_PASSWORD=null
REDIS_PORT=6379
This is the basic setup for using Redis as the queue driver. The REDIS_HOST
is set to redis
, which is the name of the Redis service defined in the docker-compose.yml
file.
Security tip: For local development, the Redis container is fine as-is. But in production environments, it’s important to secure Redis properly:
- Use authentication
- Bind only to trusted networks
- Avoid exposing Redis to the public internet
You can find more details in the official Redis Docker Hub documentation.
Creating and dispatching a Job
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:
docker compose run --rm artisan make:job LogMessageJob
This will create a new job class in the app/Jobs
directory. Open the newly created LogMessageJob.php
file and modify the handle
method to log a message:
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Log;
class LogMessageJob implements ShouldQueue
{
use Queueable;
/**
* Create a new job instance.
*/
public function __construct()
{
//
}
/**
* Execute the job.
*/
public function handle(): void
{
Log::info('LogMessageJob executed successfully', [
'timestamp' => now(),
]);
}
}
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 routes/web.php
file:
<?php
use Illuminate\Support\Facades\Route;
Route::get('/log-message', function () {
// Dispatch the job to the queue
dispatch(new \App\Jobs\LogMessageJob());
return 'Job dispatched!';
});
Now, when you visit the /log-message
route, it will dispatch the LogMessageJob
to the Redis queue:
curl http://localhost/log-message
You should see the message “Job dispatched!” in your browser.
Testing the Queue Worker
At this point, let’s verify that the job has been properly queued. You can do this by running the following command:
$ docker compose run --rm artisan queue:monitor default
# Should ouput something like this
Queue name ............................................................................................ Size / Status
[redis] default .............................................................................................. [1] OK
This verifies that we have a job queued in the default
queue. For this article, we will use the default
queue.
Let’s verify that our job can be processed, before queuing more jobs. You can do this by running the following command:
$ docker compose run --rm artisan queue:work --once
# should output something like this
INFO Processing jobs from the [default] queue.
2025-05-20 10:54:19 App\Jobs\LogMessageJob .................................................................. RUNNING
2025-05-20 10:54:19 App\Jobs\LogMessageJob ............................................................ 226.54ms DONE
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.
$ cat storage/logs/laravel.log
[2025-05-20 10:55:27] local.INFO: LogMessageJob executed successfully {"timestamp":"2025-05-20 10:55:27"}
Hopefully, everything works as expected so far. If you see any errors, please check the logs for more information.
Running the Queue Worker in the background
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.
/usr/local/bin/php
).
For this guide, the purpose is to leverage FrankenPHP’s CLI capabilities to run the queue worker, which can be accessed with /usr/local/bin/frankenphp php-cli
Let’s set up a Service in our Docker Compose setup, that will run the worker as a separate container. Open the docker-compose.yml
file and add the following service:
# New Service
worker:
image: laravel-app
volumes:
- ".:/app"
working_dir: /app
command: "frankenphp php-cli artisan queue:work"
# Let's make sure that Redis and our Database is up and running as well.
depends_on:
- redis
- mysql
This will create a new service called worker
that will run the queue worker in the background. The depends_on
option ensures that the Redis and MySQL services are up and running before starting the worker.
You can start the worker by running the following command:
docker compose up -d worker
This will start the worker in the background. You can check the logs of the worker by running:
docker compose logs -f worker
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:
curl http://localhost/log-message
You should see the job being processed in the logs of the worker container:
$ docker compose logs -f worker
worker-1 | 2025-05-20 11:09:58 App\Jobs\LogMessageJob ......................... RUNNING
worker-1 | 2025-05-20 11:09:58 App\Jobs\LogMessageJob ................... 211.46ms DONE
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 docker compose up -d worker
command multiple times. Each worker will process jobs from the same queue, allowing you to scale your application easily.
docker-compose.yml
file that runs the schedule:work
command. The documentation states that schedule:work
can be used for local development, but doesn’t directly mention that it can be used in production.
However, it is a great way to run scheduled tasks in the background, without having to set up a cron job.Laravel Horizon … and FrankenPHP
Running multiple workers is great, but it can be hard to manage them. This is where Laravel Horizon comes in. Horizon provides a beautiful dashboard and code-driven configuration for your Redis queues.
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 php artisan horizon
command, referencing the local PHP binary instead.
This particular section will make it possible for you to run Horizon with FrankenPHP.
Installing Horizon
You can install Horizon by running the following command:
composer require laravel/horizon
docker compose run --rm artisan horizon:install
This will install Horizon and publish the configuration file to config/horizon.php
. You can customize the configuration file to suit your needs.
config/horizon.php
or via environment variables (e.g., HORIZON_PREFIX
, QUEUE_CONNECTION
).What’s the problem?
Horizon relies on the PHP_BINARY
constant to determine which PHP binary to use. Unfortunately, FrankenPHP doesn’t set this constant, leading to a Permission denied error when Horizon attempts to launch workers or supervisors.
$ docker compose run --rm artisan horizon
INFO Horizon started successfully.
sh: 1: exec: : Permission denied
sh: 1: exec: : Permission denied
sh: 1: exec: : Permission denied
...
This is because Horizon tries to run the php
command, but PHP_BINARY
is empty when using FrankenPHP, so it doesn’t know which binary to execute.
Fixing the problem
The quick fix is to not use FrankenPHP’s php-cli
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.
Here’s a workaround to make it work with FrankenPHP:
In your AppServiceProvider
, add the following to your boot
method:
use Laravel\Horizon\SupervisorCommandString;
use Laravel\Horizon\WorkerCommandString;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
SupervisorCommandString::$command = 'exec /usr/local/bin/frankenphp php-cli artisan horizon:supervisor';
WorkerCommandString::$command = 'exec /usr/local/bin/frankenphp php-cli artisan horizon:work';
}
}
This will override the default command used by Horizon to run the supervisor and worker commands. Now you can run Horizon with FrankenPHP:
$ docker compose run --rm artisan horizon
INFO Horizon started successfully.
Putting it into the Docker Compose setup
Now that we have Horizon working with FrankenPHP, we can add it to our Docker Compose setup. Open the docker-compose.yml
file and add the following service:
# New Service
horizon:
image: laravel-app
volumes:
- ".:/app"
working_dir: /app
command: "frankenphp php-cli artisan horizon"
# Horizon allows for a working healthcheck, so let's add one
healthcheck:
test: ["CMD", "frankenphp", "php-cli", "artisan", "horizon:status"]
timeout: 10s
retries: 3
depends_on:
- redis
- mysql
This will create a new service called horizon
that will run the Horizon command in the background. You can start Horizon by running the following command:
docker compose up -d horizon
Accessing the Horizon dashboard is as simple as visiting http://localhost/horizon
in your browser. You should see the Horizon dashboard, where you can monitor your queues and jobs.