Laravel + Sail in Windows with Docker and PowerShell.

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!