Since October 2025, and Laravel v11.3.0, Laravel has shipped with a composer script that allows you to run the command
composer run dev, to immediately start serving requests for your Laravel application locally. It was a useful
addition, but it uses the artisan serve command, amongst other things, which is great, unless you're using Laravel
Sail. Whenever I load up a project that happens to use Sail, I end up having to open multiple terminal tabs to run the
various commands.
"Why not just replace the commands with sail commands?"
You can replace the various parts with Sail-specific commands, and it'll run, but the issue comes when you end the process because it'll exit before Sail stops all the containers. There is a solution for this, but before we get to that, let's look at what's actually happening, and why.
Just here for the solution? This article is a detailed guide/walkthrough of both the problem and the solution. If you're not interested in all that, or you're coming back after already reading it, you can skip to the solution.
Jump Aheadinterested in all the details, you can jump ahead to the solution. {% /skip %}
The Command
The dev script/command actually consists two entries, with the second consisting of multiple commands. The first
entry is a call to Composer
(code here), which removes the command
timeout that would typically cause the process to exit early. The second entry is a command that is a little more
complex.
npx concurrently -k -c "#93c5fd,#c4b5fd,#d4d4d8,#fdba74" "php artisan serve" "php artisan queue:listen --tries=1" "php artisan pail" "npm run dev" --names=server,queue,logs,vite
Unescaped Commands
This and all other commands in this article are unescaped, appearing as they would in a shell. When adding them to a
script/command within the composer.json file, you'll need to escape the double quotes (").
Let’s look at what this command actually is, which will be useful because I know that the command itself is longer than the HTML element will display.
First up we have npx, which is a command that has come pre-bundled with npm since 5.2.0, and lets you execute a
package (it stands for node package exectue). One of the biggest benefits, depending on whom you ask, is that the
package that you're executing doesn't have to be installed locally.
The package that we're running here is called concurrently, and it's an npm package that, as the name and context suggest, lets you run multiple commands concurrently.
It's being used with three arguments, two at the start and one at the end.
-kTells it to kill all other processes once the first one exits.-c "#93c5fd,#c4b5fd,#d4d4d8,#fdba74"Tells it the colours to use for the processes.--names=server,queue,logs,viteGives each process a name.
Finally, we have the four commands that are actually being run.
Server
The command named server and coloured #93c5fd
is the artisan serve command, which
uses PHP built-in development server to serve the application.
php artisan serve
This is the first of the commands, which means that all other processes will be killed as soon as this one has exited.
Queue
The command named queue and coloured #c4b5fd
runs Laravels queue listener. It uses
the --tries argument, which tells it to only attempt a job once before treating it as a failure.
php artisan queue:listen --tries=1
Logs
Next up is the logs command with the colour #d4d4d8
which runs Laravel Pail, a
simple development package that tails the Laravel logs for you.
php artisan pail
Vite
Last but not least, we have the npm script called dev which runs Vite to process all the frontend assets.
npm run dev
Using Sail
If you're using Laravel Sail, your first thought may be to replace the relevant commands with sail ones, making the four commands the following:
./vendor/bin/sail up
./vendor/bin/sail artisan queue:listen --tries=1
./vendor/bin/sail artisan pail
npm run dev
Which will work, at least, initially. The problem here is the way that concurrently exits. It'll kill the processes
before docker is able to perform its teardown operations. If you run sail up on its own, and hit
ctrl+c
, you'll notice that it won't exit straight away, but will instead exit each
container one by one. When run in this command using concurrently, you won't see any output, but if you check with
docker, most, if not all of that projects containers will still be running.
So, to use this command effectively, we need a way to detect that the process is exiting, and perform the necessary
cleanup, which would be sail stop (or sail down if you like it being destructive).
Trapping the Signal
Luckily, if you're using a POSIX-compliant shell, which to be honest is pretty much all of them as long it's not
Windows, there's a builtin shell command called trap
(man pages here) which lets you run something for a signal.
Signals?
If you're not familiar with signals, they're essentially a way to notify a process that something happened. They're used to tell a process they've been interrupted, terminated, killed, etc.
This means that the sail up command needs to be two commands in one, with the first using trap.
trap "./vendor/bin/sail stop" EXIT SIGTERM SIGINT SIGHUP; sail up
The only downside is that it will be caught multiple times because the teardown and cleanup will cause multiple signals. If you can live with the docker containers being stopped something like four times, then you can use the below command.
npx concurrently -k -c "#93c5fd,#c4b5fd,#d4d4d8,#fdba74" "bash -c 'cleanup() { ./vendor/bin/sail stop; }; trap cleanup EXIT SIGTERM SIGINT SIGHUP; ./vendor/bin/sail up'" "./vendor/bin/sail artisan queue:listen --tries=1" "./vendor/bin/sail artisan pail" "npm run dev" --names=server,queue,logs,vite
As you can probably appreciate, this looks a bit messy, and it's not ideal that it's stopping containers multiple times. But fear not, there is a better solution.
The Solution
The ideal way to handle this is to create yourself a dev.sh script in the root of your project, and change the command
to the following:
npx concurrently -k -c "#93c5fd,#c4b5fd,#d4d4d8,#fdba74" "bash dev.sh" "./vendor/bin/sail artisan queue:listen --tries=1" "./vendor/bin/sail artisan pail" "npm run dev" --names=server,queue,logs,vite
Then put this in dev.sh.
#!/bin/bash
DESTROY=${DESTROY:-false}
cleanup() {
trap '' EXIT SIGTERM SIGINT SIGHUP
echo ""
if [ "$DESTROY" = "true" ]; then
echo "Stopping and removing containers..."
./vendor/bin/sail down
else
echo "Stopping containers..."
./vendor/bin/sail stop
fi
}
trap cleanup EXIT SIGTERM SIGINT SIGHUP
echo "Starting Sail..."
./vendor/bin/sail up
You can probably tell that I've added a bit of polish to this, with messages explaining what's happening and the likes, even going as far as allowing you to choose between stopping the containers or destroying them. However, there are two very important bits.
The first is this.
trap cleanup EXIT SIGTERM SIGINT SIGHUP
This tells the shell to run the cleanup function when it receives the following signals.
EXITThis is a sort of pseudo signal that detects that a process is about to exit, rather than it is exiting.SIGTERMA "please shut down" signal, the kind sent by thekillcommand.SIGINTThis is an interupt signal, typically sent by pressing ctrl+c .SIGHUPThis is a hangup signal, typically sent by the shell when it is closed, like if you close your terminal.
The second important bit is inside the cleanup function.
trap '' EXIT SIGTERM SIGINT SIGHUP
This essentially resets the other trap, so when running cleanup the signals aren't caught multiple times.
Using the Script
Update your composer.json file so the dev script is defined as follows:
{
"dev": [
"Composer\\Config::disableProcessTimeout",
"npx concurrently -k -c \"#93c5fd,#c4b5fd,#d4d4d8,#fdba74\" \"bash dev.sh\" \"./vendor/bin/sail artisan queue:listen --tries=1\" \"./vendor/bin/sail artisan pail\" \"npm run dev\" --names=server,queue,logs,vite"
]
}
Once you've got it all set, you can run the command like normal. If you want to destroy the containers instead, you set
the DESTROY environment variable when running the command.
DESTROY=true composer run dev
And there you have it! You can now run composer run dev and it'll work with sail.