Laravel Sail is a light-weight command-line interface for interacting with Laravel’s default Docker development environment.
Laravel Sail – Laravel 10.x – The PHP Framework For Web Artisans
For me, it was the answer to the issues I faced on my Mac laptop with Brew deciding it had to update my PHP and no easy way to stop it. However, I also have a Windows PC on which I sometimes work and was left in shock when I realized that it wasn’t that easy on Windows.
The actual issue, in Linux, Windows or Mac, is that although Sail commands are executed inside the Docker image, you need PHP in your machine to do the initial composer install to get Laravel/Sail. To avoid headaches, the PHP version must match the version expected by Laravel and the packages you are using. When managing multiple Laravel applications, handling the PHP versions can be complex and time-consuming. Enter docker.
The benefit of docker, in this scenario, is that you can have a separate container for each Laravel application, each with its matching PHP version. The issue is that you have a chicken-and-egg problem; you need sail to create and start the container, but you need the container to run composer and install sail. The first part of this tutorial solves the chicken-egg problem.
The second issue (more like an annoyance) is that since Sail is a bash tool, you will need to install a Linux Subsystem in your Windows machine and use it to run your Sail commands. The main issue with this approach is that IDEs cannot easily use the WSL terminal to run commands, resulting in having to switch between applications while working with Laravel apps. The second part of this tutorial introduces a native PowerShell version of Sail that can be used directly in a PowerShell terminal and/or from within your IDE.
Bootstrap the (new) Laravel application with Docker Compose
To solve the chicken-and-egg problem, we can use a bootstrap container with PHP, from which we can execute Composer, the PHP dependency management tool. This container can be used to create a new Laravel project or to work on an existing one (e.g. in my case I pulled the code from a repository). You will use this container to run composer and populate the application’s vendor folder. Once the vendor folder is populated, you can use Laravel’s Sail.
If you have an existing application, clone it (or get a copy) to your Windows machine:
PS C:\git> git clone git@laravel-app.git
Cloning into 'laravel-app'...
PS C:\git> cd laravel-app
PS C:\git\laravel-app>
If you are starting a new Larvel application, create a new folder for it:
PS C:\git> mkdir laravel-app
Directory: C:\git
Mode LastWriteTime Length Name
---- ------------- ------ ----
d---- 9/26/2023 9:09 AM laravel-app
PS C:\git> cd .\laravel-app\
PS C:\git\laravel-app>
To avoid mixing the bootstrap files with your Laravel application files, create a folder called dev:
PS C:\git\laravel-app> mkdir dev
Directory: C:\git\laravel-app
Mode LastWriteTime Length Name
---- ------------- ------ ----
d---- 9/26/2023 10:06 AM dev
PS C:\git\laravel-app> cd dev
PS C:\git\laravel-app\dev>
Next, you’ll create the docker-compose.yml file that will define the containerized environment with PHP. In this file, you’ll set up a service named bootstrap, which will be based on a custom Docker image built with a Dockerfile you’ll set up later on.
Create a new docker-compose.yml file using your text editor of choice. Here, notepad++ is used (I highly recommend NOT to use Windows’ Notepad because it does not like files without extension and will automatically add the txt extension if you don’t use one).
PS C:\git\laravel-app\dev> Start notepad++ docker-compose.yml
PS C:\git\laravel-app\dev>
Copy the following content to this file, and don’t forget to replace the WWWGROUP value with the result from the previous command:
version: "3.7"
services:
#Bootstrap Container
bootstrap:
build:
dockerfile: Dockerfile
extra_hosts:
- 'host.docker.internal:host-gateway'
ports:
- '${APP_PORT:-80}:80'
volumes:
- '..:/var/www/html'
networks:
- sail
networks:
sail:
driver: bridge
Save and close the file when you are done. If you are using notepad, you can do that by pressing CTRL+S, and then closing the app.
This Dockerfile extends from the default php:fpm Docker image. It installs a few PHP dependencies that are required to install Laravel, and the Composer executable. To avoid version issues, you need to modify the first line to match the PHP version required by the Laravel version you are using. As opposed to Unix systems, docker in Windows will run your containers as root, not as your user in the host machine. So there is no need to create groups or users.
FROM php:8.1-fpm
# Install system dependencies
RUN apt-get update && apt-get install -y \
git \
curl \
libpng-dev \
libonig-dev \
libxml2-dev \
zip \
unzip
# Clear cache
RUN apt-get clean && rm -rf /var/lib/apt/lists/*
# Install PHP extensions
RUN docker-php-ext-install mbstring exif pcntl bcmath gd
# Use the default production configuration
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
# Get latest Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Set working directory
WORKDIR /var/www/html
Save and close the file when you’re done. Windows does not like files without extensions, so it probably added “.txt” to your file when saving it. You can check and remove the extension if needed:
PS C:\git\laravel-app\dev> dir
Directory: C:\git\laravel-app\dev
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 9/27/2023 8:54 AM 366 docker-compose.yml
-a--- 9/27/2023 8:54 AM 720 Dockerfile.txt
PS C:\git\laravel-app\dev> mv .\Dockerfile.txt Dockerfile
PS C:\git\laravel-app\dev> dir
Directory: C:\git\laravel-app\dev
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 9/27/2023 8:54 AM 366 docker-compose.yml
-a--- 9/27/2023 8:54 AM 720 Dockerfile
Next, you can bring your environment up with:
PS C:\git\laravel-app\dev> docker-compose up -d
This command will execute Docker Compose in detached mode, which means it will run in the background. The first time you bring an environment up with a custom image, Docker Compose will automatically build the image for you before creating the required containers. This might take a few moments to finish. You’ll see output similar to this:
[+] Building 61.2s (16/16) FINISHED docker:default
=> [bootstrap internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 795B 0.0s
=> [bootstrap internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [bootstrap internal] load metadata for docker.io/library/php:8.1-fpm 0.5s
=> [bootstrap internal] load metadata for docker.io/library/composer:latest 0.5s
=> CACHED [bootstrap stage-0 1/10] FROM docker.io/library/php:8.1-fpm@sha256:d94c26a8632c0c87557dbb1839a395fa5b 0.0s
=> => resolve docker.io/library/php:8.1-fpm@sha256:d94c26a8632c0c87557dbb1839a395fa5b69b5f8fbd944b025c5a5783a0dc 0.0s
=> CACHED [bootstrap] FROM docker.io/library/composer:latest@sha256:1ac7a547cb88acb0de62663b70f2b3d80ad273552882 0.0s
=> [bootstrap stage-0 3/10] RUN apt-get update && apt-get install -y git curl libpng-dev libon 17.3s
=> [bootstrap stage-0 4/10] RUN apt-get clean && rm -rf /var/lib/apt/lists/* 0.5s
=> [bootstrap stage-0 5/10] RUN docker-php-ext-install mbstring exif pcntl bcmath gd 40.1s
=> [bootstrap stage-0 6/10] RUN mv "/usr/local/etc/php/php.ini-production" "/usr/local/etc/php/php.ini" 0.4s
=> [bootstrap stage-0 7/10] COPY --from=composer:latest /usr/bin/composer /usr/bin/composer 0.1s
=> [bootstrap stage-0 8/10] RUN groupadd --force -g 1001 sail 0.5s
=> [bootstrap stage-0 9/10] RUN useradd -ms /bin/bash --no-user-group -g 1001 -u 1337 sail 0.8s
=> [bootstrap stage-0 10/10] WORKDIR /var/www/html 0.0s
=> [bootstrap] exporting to image 0.5s
=> => exporting layers 0.5s
=> => writing image sha256:21d0a638d751287c2dc7361b8c04f3563d3c96338cef44c4c98cffde5f91f108 0.0s
=> => naming to docker.io/library/dev-bootstrap 0.0s
time="2023-09-27T09:22:39-06:00" level=warning msg="Found orphan containers ([dev-app-1]) for this project. If you removed or renamed this service in your compose file, you can run this command with the --remove-orphans flag to clean it up."
[+] Running 1/1
✔ Container dev-bootstrap-1 Started
You can verify that your environment is up and running with:
PS C:\git\laravel-app\dev> docker-compose ps
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
dev-bootstrap-1 dev-bootstrap "docker-php-entrypoint php-fpm" bootstrap 2 minutes ago Up 2 minutes 0.0.0.0:80->80/tcp, 9000/tcp
PS C:\git\laravel-app\dev>
Once the bootstrap service is up, you can run Composer, to bootstrap an existing or new Laravel application. In order to do that, you’ll use docker compose exec to run commands on the bootstrap service, where PHP is installed.
New Laravel Application
For a new Laravel application, the following command will use Docker Compose to execute composer create-project, which will bootstrap a fresh installation of Laravel based on the laravel/laravel package:
PS C:\git\laravel-app\dev>docker-compose exec bootstrap composer create-project laravel/laravel --prefer-dist temp
Creating a "laravel/laravel" project at "./temp"
Info from https://repo.packagist.org: #StandWithUkraine
Installing laravel/laravel (v10.2.6)
- Installing laravel/laravel (v10.2.6): Extracting archive
Created project in /var/www/html/temp
> @php -r "file_exists('.env') || copy('.env.example', '.env');"
Loading composer repositories with package information
Updating dependencies
Lock file operations: 110 installs, 0 updates, 0 removals
...
82 packages you are using are looking for funding.
Use the `composer fund` command to find out more!
> @php artisan vendor:publish --tag=laravel-assets --ansi --force
INFO No publishable resources for tag [laravel-assets].
No security vulnerability advisories found.
> @php artisan key:generate --ansi
INFO Application key set successfully.
The Laravel application will be created in the temp folder, so next, you will have to move the files to the root of your laravel-app folder:
PS C:\git\laravel-app\dev> cd ..
PS C:\git\laravel-app> Copy-Item -Path "temp\*" -Destination ".\" -Recurse
PS C:\git\laravel-app> Remove-Item -Recurse -Force temp
PS C:\git\laravel-app> dir
Directory: C:\git\laravel-app
Mode LastWriteTime Length Name
---- ------------- ------ ----
d---- 9/27/2023 1:36 PM app
d---- 9/27/2023 1:36 PM bootstrap
d---- 9/27/2023 1:36 PM config
d---- 9/27/2023 1:36 PM database
d---- 9/27/2023 9:21 AM dev
d---- 9/27/2023 1:36 PM public
d---- 9/27/2023 1:36 PM resources
d---- 9/27/2023 1:36 PM routes
d---- 9/27/2023 1:36 PM storage
d---- 9/27/2023 1:36 PM tests
d---- 9/27/2023 1:36 PM vendor
-a--- 8/10/2023 1:19 AM 258 .editorconfig
-a--- 9/27/2023 1:36 PM 1148 .env
-a--- 8/10/2023 1:19 AM 1097 .env.example
-a--- 8/10/2023 1:19 AM 186 .gitattributes
-a--- 8/10/2023 1:19 AM 243 .gitignore
-a--- 8/10/2023 1:19 AM 1686 artisan
-a--- 8/10/2023 1:19 AM 1882 composer.json
-a--- 9/27/2023 1:31 PM 296013 composer.lock
-a--- 8/10/2023 1:19 AM 248 package.json
-a--- 8/10/2023 1:19 AM 1084 phpunit.xml
-a--- 8/10/2023 1:19 AM 4158 README.md
-a--- 8/10/2023 1:19 AM 263 vite.config.js
The next step is for you to install Sail (remember that the docker-compose commands must be executed from the dev folder):
PS C:\git\laravel-app> cd dev
PS C:\git\laravel-app\dev> docker-compose exec bootstrap composer require laravel/sail --dev
Info from https://repo.packagist.org: #StandWithUkraine
./composer.json has been updated
Running composer update laravel/sail
...
Using version ^1.25 for laravel/sail
After Sail has been installed, you may run the sail:install Artisan command. This command will publish Sail’s docker-compose.yml file to the root of your application. You need to select the services you want to install:
PS C:\git\laravel-app\dev> docker-compose exec bootstrap php artisan sail:install
┌ Which services would you like to install? ───────────────────┐
│ mariadb │
│ redis │
│ meilisearch │
│ mailpit │
└──────────────────────────────────────────────────────────────┘
Sail scaffolding installed successfully.
PS C:\git\laravel-app\dev>
Existing Laravel Application
For an existing Laravel application, the following command will use Docker Compose to execute composer install, which will install Laravel with all the dependencies defined in the composer.json file.:
PS C:\git\laravel-app\dev> docker-compose exec bootstrap composer install
Installing dependencies from lock file (including require-dev)
Verifying lock file contents can be installed on current platform.
Package operations: 191 installs, 0 updates, 0 removals
...
Generating optimized autoload files
> Illuminate\Foundation\ComposerScripts::postAutoloadDump
> @php artisan package:discover --ansi
...
126 packages you are using are looking for funding.
Use the `composer fund` command to find out more!
Depending on the packages you are using on the Laravel project, it is possible that the composer command will fail due to missing PHP extensions. For example, this project uses the *intl* package which is missing.
PS C:\git\laravel-app\dev>docker-compose exec bootstrap composer install
Installing dependencies from lock file (including require-dev)
Verifying lock file contents can be installed on current platform.
Your lock file does not contain a compatible set of packages. Please run composer update.
Problem 1
- Root composer.json requires PHP extension ext-intl * but it is missing from your system. Install or enable PHP's intl extension.
...
As pointed out by the error, the reason for failure can be either because there is a missing PHP extension (not installed) or it has not been enabled in the PHP configuration.
Installing additional PHP packages
In order to install a missing PHP package, you need to modify the docker file provided previously. For this, you must modify the RUN docker-php-ext-install command and add missing extensions. In this case, you will need to add the intl extension. Open the Docker file and modify line 19 (just after the # Install PHP extensions comment):
RUN docker-php-ext-install mbstring exif pcntl bcmath gd intl
Since you need to modify the container, you need Docker to create it again. So first, you need to delete it. Open your Docker desktop app, select the Containers view in the left pane. Find the dev-bootstrap container and delete it.
Next, select the Images view in the left pane, and delete the dev/bootstrap image:
Finally, you need to create the container again and run the composer install command:
PS C:\git\laravel-app\dev> docker-compose up -d
PS C:\git\eventastic\web\dev> docker-compose up -d
[+] Building 110.7s (15/15) FINISHED docker:default
...
=> [bootstrap stage-0 4/9] RUN docker-php-ext-install mbstring exif pcntl bcmath gd intl 106.9s
...
PS C:\git\laravel-app\dev>docker-compose exec bootstrap composer install
Installing dependencies from lock file (including require-dev)
Verifying lock file contents can be installed on current platform.
Your lock file does not contain a compatible set of packages. Please run composer update.
Problem 1
- openspout/openspout is locked to version v4.15.0 and an update of this package was not requested.
- openspout/openspout v4.15.0 requires ext-zip * -> it is missing from your system. Install or enable PHP's zip extension.
So, the intl package error is gone, but the command still fails due to the zip package. You need to repeat the previous steps till no more errors due to missing packages are found.
NOTE
Additional PHP packages can require additional system libraries. When adding a PHP package, make sure to search the forums to determine if additional system libraries are needed. In this case, the RUN apt-get update && apt-get install -y \ also needs to be modified in order to install the additional system libraries.
The second type of issue can be related to the required extension not enabled in the PHP configuration. In this case, we need to modify the php.ini file. Although it is technically possible to directly edit the php.ini file in the Docker container, it can be easier to modify the file in the Windows host. For this, we need to copy the php.ini file from the container to the host machine. The easiest way to do this is to use the docker cp command. You will first need to get the name of your bootstrap container. For this, you can use the docker ps command.
PS C:\git\laravel-app\dev> docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7598a58575ab dev-bootstrap "docker-php-entrypoi…" 12 seconds ago Up 10 seconds 0.0.0.0:80->80/tcp, 9000/tcp dev-bootstrap-1
In this case, the container name is dev-bootstrap-1. The other piece of information you need, is the destination path in the Windows host. You can use the pwd command to do this.
PS C:\git\laravel-app\dev> $pwd.path
C:\git\laravel-app\dev
Next, you will use the cp command to copy the php.ini file to the Windows host. Three pieces of information are used in the command. First, the name of the docker container must be replaced with your specific value. Second, the location of the php.ini file is specific to the fpm Docker images. Third, the file destination depends on where your project is located in the Windows host.
PS C:\git\laravel-app\dev> docker cp dev-bootstrap-1:/usr/local/etc/php/php.ini C:\git\laravel-app\dev
Successfully copied 75.3kB to C:\git\laravel-app\dev
Once you know what to change and made the changes, you can copy the php.ini file back to the container. For that, you need to invert the order of the cp arguments:
PS C:\git\laravel-app\dev> docker cp C:\git\laravel-app\dev\php.ini dev-bootstrap-1:/usr/local/etc/php
Successfully copied 75.3kB to dev-bootstrap-1:/usr/local/etc/php
For extension added via the Dockerfile using the docker-php-ext-install there is no need to edit the php.ini file, as the extensions are enabled using the PHP config extension mechanism.
Switching to Development
One you have bootstrapped your laravel application you can turn down your bootstrap container.
PS C:\git\laravel-app\dev> docker-compose down
[+] Running 2/2
Container dev-bootstrap-1 Removed 1.4s
Network dev_sail Removed
You no longer need the image, so you can go to your Docker panel an delete it. You can also delete the dev folder.
Native Sail via PowerShell
Laravel Sail is available in Windows by using the WSL2. However, that means that you will need some extra work to configure your IDE to integrate with WSL. To avoid the hassle, and for cases where your IDE does not play nice with WSL2, I have written a PowerShell version of Sail called Jib. To start using it, all you need to do is to install the script:
PS> Install-Script -Name Jib
If it is the first time you are installing scripts from the PowerShell Gallery, it is possible that you get a warning about configuring your PATH so installed scripts are available globally.
If you want to use Jib, you would need to run the installed script every time you open a PowerShell terminal. In order for Jib to be available in all your sessions, you add the script to your PowerShell profile. Windows creates the $PROFILE environment variable to point to the current user profile. You can open it with Notepad++ to edit it (if it does not exist, Notepad++ will ask you if you want to create the file):
PS C:\git\laravel-app\dev>Start notepad++ $profile
Edit the profile PS file to load the Jib script. You can add this line to the end of the profile:
...
$Path = Split-Path $Profile
. $Path\Scripts\Jib.ps1
After saving the changes, go back to the PowerShell terminal and reload your profile:
PS C:\git\laravel-app\dev>. $profile
It is possible that you get an error message while reloading the profile: Management_Install.ps1
cannot be loaded because the execution of scripts is disabled on this system. This stackoverflow post provides a great summary of why and how to fix it. After your profile has been reloaded, you should be able to check if Jib is correctly loaded:
PS C:\git\laravel-app\dev> Get-Help Invoke-Jib
NAME
Invoke-Jib
SYNOPSIS
Jib is PowerShell replacement for the Laravel Sail bash/shell command.
...
No you can use any of the supported Sail commands, for example, you can start your application:
PS C:\git\laravel-app\dev> Invoke-Jib up
Setting the ENVIRONMENT from the '.env' file
Deciding what docker compose to use.
Ensure that Docker is running...
Determine if Sail is currently up...
Pass thru to docker-compose up
Executing command in container: docker compose up
[+] Building 0.0s (0/0)
[+] Running 4/0
✔ Container laravel-app-mailhog-1 Created 0.0s
✔ Container laravel-app-mariadb-1 Created 0.0s
✔ Container laravel-app-redis-1 Created 0.0s
✔ Container laravel-app-laravel.test-1 Created 0.0s
...
laravel-app-laravel.test-1 | INFO Server running on [http://0.0.0.0:80].
laravel-app-laravel.test-1 |
laravel-app-laravel.test-1 | Press Ctrl+C to stop the server
laravel-app-laravel.test-1 |
laravel-app-mailhog-1 | [APIv1] KEEPALIVE /api/v1/events
Conclusion
You learned how to bootstrap a Laravel application using Docker, so we develop against a particular version of PHP in a Windows machine without requiring a local PHP installation (e.g. via XAMPP). You also installed the Jib PowerShell script, a Laravel Sail drop-in replacement, so we can interact with the Laravel container from the PowerShell terminal.
I hope you find this information useful!