Why I Decided to Change My Laradock Workflow
For a long time, my local Laradock setup was very simple to use in everyday development. I started the required services once and then worked with my projects without constantly thinking about Docker.
That changed when I needed several PHP versions at the same time. Some projects remained on PHP 8.3, others were already running PHP 8.4, while newer ones moved to PHP 8.5.
I had prepared this multi-PHP environment while upgrading my local Laravel setup and package to PHP 8.5. The next problem was making that setup convenient enough for everyday CLI work.
For HTTP, this is quite natural to solve: each Nginx virtual host can use its own PHP-FPM service. CLI usage was less convenient. For Artisan, Composer, or even a simple php -v, I had to remember which workspace belonged to which project and type long Docker Compose commands.
I wanted to bring back the simple workflow without giving up a proper multi-PHP environment.
docker compose run --rm --no-deps workspace-85 bash -lc \ 'cd /var/www/icanup && php artisan test'dartisan testMulti-PHP Architecture: Separate CLI and PHP-FPM for Each
The first important decision was not to mix the CLI runtime with PHP-FPM.
PHP-FPM handles HTTP requests through Nginx, while the workspace is used for CLI tools such as Artisan, Composer, PHPUnit, PHPStan, and other development commands.
My final Laradock setup looks like this:
PHP | CLI service | FPM service |
|---|---|---|
PHP 8.3 | workspace | php-fpm |
PHP 8.4 | workspace-84 | php-fpm-84 |
PHP 8.5 | workspace-85 | php-fpm-85 |
PHP 8.3 remains the default runtime, while 8.4 and 8.5 run as additional parallel environments.
If a site runs through php-fpm-85, that does not mean that php artisan or Composer also runs on PHP 8.5. CLI and FPM are independent runtimes. This is why my multi-PHP environment uses matching workspace, workspace-84, and workspace-85 services.
How I Configured Persistent Workspaces
I did not want to create a temporary container for every CLI command. All three workspaces therefore run as regular persistent Docker Compose services.
The main workspace keeps its host ports, while workspace-84 and workspace-85 do not publish them. This allows all three containers to run simultaneously without port conflicts.
The PHP-FPM services are also connected to their matching workspaces.
php-fpm-84: depends_on: !override - workspace-84 workspace-84: ports: !reset [] php-fpm-85: depends_on: !override - workspace-85 workspace-85: ports: !reset []nginx: depends_on: - php-fpm - php-fpm-84 - php-fpm-85Matching UID and GID Between the Host and Workspace
Another problem appeared after I started using the wrappers.
The source code is bind-mounted from the host machine and belongs to my local user with UID/GID 1000:1000. Initially, the laradock user inside the containers had a different UID, causing Git to treat the repository ownership as suspicious.
Instead of adding a global safe.directory=*, I aligned the Laradock user UID/GID with the host user.
WORKSPACE_PUID=1000WORKSPACE_PGID=1000When bind-mounted files belong to UID 1000, development commands inside the container should ideally run as a user with the same UID. This solves the Git warning and also prevents root:root files from appearing in application repositories.
Local Project-to-Workspace Mapping
I cannot add local Docker runtime files to every work repository, so the PHP version mapping lives entirely outside the application projects.
I keep it in:
~/.config/laradock/projects.conf
PHP 8.3 is the default, so projects running on 8.3 do not need to be listed at all. Only projects requiring an alternative workspace are added to the mapping.
default=workspace # PHP 8.4/home/alyona/Projects/some-project/project-core=workspace-84 # PHP 8.5/home/alyona/Projects/icanup=workspace-85Heading:
I did not want separate aliases such as php83, php84, php85, composer84, or composer85.
The project already has a local mapping, so the CLI can determine the correct workspace automatically.
I use one dispatcher called laradock-exec.
It:
detects the current project's Git root;
looks up the appropriate workspace in
projects.conf;falls back to
workspace;converts the host path
/home/alyona/Projects/...to/var/www/...;verifies that the required service exists and is running;
executes the command through
docker compose exec;runs it as the
laradockuser.
exec docker compose exec \ --user laradock \ -w "$container_dir" \ "$service" \ "$@"dphp, dartisan and dcomposer
On top of the dispatcher, I keep only three small wrapper commands.
dphp runs PHP in the correct workspace.
dartisan is a shortcut for php artisan.
dcomposer runs Composer on the same PHP version as the project's CLI runtime.
#!/usr/bin/env bashexec "${HOME}/.local/bin/laradock-exec" php "$@"#!/usr/bin/env bashexec "${HOME}/.local/bin/laradock-exec" php artisan "$@"#!/usr/bin/env bashexec "${HOME}/.local/bin/laradock-exec" composer "$@"What My Daily Workflow Looks Like Now
I can now work with a Docker-based PHP runtime almost as if PHP were installed directly for each project.
cd ~/Projects/icanup dphp -vdartisan testdartisan migratedcomposer installFor IcanUp, the dispatcher automatically selects workspace-85, so all these commands run on PHP 8.5.
In another Core project, the same commands automatically use workspace-84.
Any project missing from the mapping falls back to the default workspace running PHP 8.3.
This also removes one easy source of mistakes: accidentally running a project or its tests under the wrong PHP version. I had already seen how noticeable that can become when the same round() test behaved differently on PHP 8.3 and PHP 8.4.
I no longer need to remember container names every time I run Artisan or Composer.
cd ~/Projects/laradock docker compose exec \ --user laradock \ -w /var/www/icanup \ workspace-85 \ php artisan testcd ~/Projects/icanup dartisan testHow to Add Another PHP 8.4 or PHP 8.5 Project
A new project does not need any Docker-specific files inside its repository. First, I determine its Git root:
git rev-parse --show-toplevelThen I only need to add one line to ~/.config/laradock/projects.conf.
# PHP 8.4/home/alyona/Projects/project=workspace-84 # PHP 8.5/home/alyona/Projects/project=workspace-85The Commands Also Work from Nested Directories
The dispatcher uses git rev-parse --show-toplevel, so I do not need to be in the project root.
Even from:
~/Projects/icanup/app/Servicesdphp -v detects the root at ~/Projects/icanup, selects workspace-85, and runs from /var/www/icanup inside the container.
It is a small detail, but it makes the wrapper much more useful in real daily development.
What I Ended Up With
PHP 8.3, 8.4, and 8.5 can run simultaneously.
Each PHP version has its own CLI workspace and PHP-FPM service.
I do not need to enter Docker containers manually.
No temporary workspace containers are needed.
No Docker runtime files are added to work repositories.
Artisan and Composer always use the correct PHP version.
Commands run with the correct UID/GID.
PHP 8.3 remains the default for older projects.
PHP 8.4 and 8.5 projects require only one local mapping line.
Docker should simplify my development environment, not make me think about which container should run php artisan every time.
A multi-PHP setup should not make local development more complicated by itself.
For me, the most practical solution was to keep a clear separation between PHP-FPM and the CLI runtime, use persistent workspaces, and add a very thin local CLI layer on top of Docker Compose.
The infrastructure remains fully containerized, while my everyday workflow is reduced to the short dphp, dartisan, and dcomposer commands.



