A Complete Guide to Laravel Sail
Tags: laravel, docker, laravelsail, php
Published: 2022-03-09 09:43:02

> _Truman continues to steer his wrecked sailboat towards the infinitely receding horizon. All is calm until we see the bow of the boat suddenly strike a huge, blue wall, knocking Truman off his feet. Truman recovers and clambers across the deck to the bow of the boat. Looming above him out of the sea is a cyclorama of colossal dimensions. The sky he has been sailing towards is nothing but a painted backdrop._

– Andrew M. Niccol, [The Truman Show](http://www.dailyscript.com/scripts/the-truman-show_shooting.html)

***

On December 8 2020, Taylor Otwell announced the launch of [Laravel Sail](https://laravel.com/docs/sail), a development environment based on Docker, along with a large overhaul of Laravel's documentation:

{% embed https://twitter.com/taylorotwell/status/1336357588791947264 %}

The announcement caused a wave of excitement across the community, as a lot of people identified the new environment as an opportunity to finally get into Docker. But it also left some confusion in its wake, as Sail isn't exactly a guide to becoming a Docker expert, and it introduces an approach to development that is quite different from its predecessors.

This post is about what to expect from Laravel Sail, how it works and how to make the most of it. It is also a plea to developers to break away from it, in favour of their own, tailored solution.

But before we get there, we need to take a look under the deck, starting with a high-level explanation of what Sail is.

## In this post

* What is Laravel Sail?
* How does it compare to its predecessors?
* How does it work?
* Extending Laravel Sail
* What's wrong with Laravel Sail anyway?
* The whale in the cabin
* You don't need Laravel Sail
* Conclusion
* Resources

## What is Laravel Sail?

Sail is Laravel's latest development environment. It is the most recent addition to an already long list featuring official solutions like [Homestead](https://laravel.com/docs/homestead) and [Valet](https://laravel.com/docs/valet) on the one hand, and community efforts like [Laragon](https://laragon.org), [Laradock](http://laradock.io), [Takeout](https://github.com/tighten/takeout) and [Vessel](https://vessel.shippingdocker.com/) on the other (according to the [GitHub repository](https://github.com/laravel/sail#inspiration), Sail is largely inspired by the latter).

Laravel Sail is based on [Docker](https://www.docker.com), a technology leveraging [containers](https://www.docker.com/resources/what-container) to essentially package up applications so they can run quickly and easily on any operating system.

The future of Sail appears to be bright, as the Laravel documentation immediately featured it as the [preferred way](https://laravel.com/docs/installation#your-first-laravel-project) to instal and run Laravel projects locally, a spot that Homestead and Valet had occupied for a long time.

## How does it compare to its predecessors?

As a refresher, [Homestead](https://laravel.com/docs/homestead) is a [Vagrant](https://www.vagrantup.com) box (a virtual machine) featuring everything most Laravel applications need. That includes essential components like PHP, MySQL and a web server (Nginx), but also other technologies like PostgreSQL, Redis or Memcached.

[Valet](https://laravel.com/docs/valet), on the other hand, is a lightweight environment for macOS focussed on performance. It relies on a local installation of PHP instead of a virtual machine, and is intended to be used alongside other services like [DBngin](https://dbngin.com) or [Takeout](https://github.com/tighten/takeout) to manage other dependencies like databases.

While Homestead and Valet look quite different on paper, they promote the same general approach to local development, which is also shared by most of the aforementioned solutions: they try to be one-size-fits-all environments for Laravel projects and aim at managing them all under one roof.

Sail's approach is different, in that the instructions to build the development environment come with the project's source code. Instead of relying on the presence of a third-party solution like Homestead on the developer's machine, Docker will read the set of instructions and build the corresponding environment from scratch.

In other words, the application comes with batteries included and a single command will spin up and configure its dependencies. It will work regardless of the developer's operating system, so long as Docker is installed on it.

Laravel Sail introduces the notion of a bespoke development environment for the application, which, in my opinion, is the technology's real kicker.

While this approach is a major departure from traditional solutions, Sail still bears some resemblance to them around the tools it comes with, some of which are essential, others not.

Let's review the most important ones and how they are implemented.

## How does it work?

From here on, it will be easier to follow along with a fresh installation of Laravel, although the files I refer to always point to the [official GitHub repository](https://github.com/laravel/sail). If you've got a little bit of time, go follow the [instructions for your operating system](https://laravel.com/docs/installation#your-first-laravel-project) now and come back once you've successfully run `./vendor/bin/sail up` – I'll take it from there.

While Sail allows us to [pick the services](https://laravel.com/docs/#choosing-your-sail-services) we're interested in when creating a new Laravel application, by default it is composed of three main components – PHP, MySQL and Redis. As per the [documentation](https://laravel.com/docs/sail#introduction), the whole setup gravitates around two files – [docker-compose.yml](https://github.com/laravel/sail/blob/1.x/stubs/docker-compose.stub) (located at the root of the project after a fresh installation) and the [sail script](https://github.com/laravel/sail/blob/1.x/bin/sail) (under `vendor/bin`).

### The `docker-compose.yml` file

As mentioned already, Laravel Sail is based on Docker, which is a technology leveraging containers. The rule of thumb is that each container should only run one process, which roughly translates to each container running a single piece of software. Applying this rule to the above setup means we'll need a container for PHP, one for MySQL, and a third one for Redis.

These containers make up your application, and for it to function properly, they need to be _orchestrated_. There are several ways to do this, but Laravel Sail relies on [Docker Compose](https://docs.docker.com/compose/) for the job, which is the preferred solution for local setups.

Docker Compose expects us to describe the various components of our application in a `docker-compose.yml` file, in YAML format. If you open the one at the root of the project, you will see a `version` parameter at the top, followed by a `services` section containing a list of components comprising the ones I've just mentioned – `laravel.test`, `mysql` and `redis` (and some other ones I'll briefly cover later).

I'll describe the `mysql`, `redis` and `laravel.test` services in that order, as the two first ones are simpler than the last.

#### The `mysql` service

As the name suggests, the `mysql` service handles the MySQL database:

```
mysql:
    image: 'mysql/mysql-server:8.0'
    ports:
        - '${FORWARD_DB_PORT:-3306}:3306'
    environment:
        MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'
        MYSQL_ROOT_HOST: "%"
        MYSQL_DATABASE: '${DB_DATABASE}'
        MYSQL_USER: '${DB_USERNAME}'
        MYSQL_PASSWORD: '${DB_PASSWORD}'
        MYSQL_ALLOW_EMPTY_PASSWORD: 1
    volumes:
        - 'sailmysql:/var/lib/mysql'
    networks:
        - sail
    healthcheck:
        test: ["CMD", "mysqladmin", "ping", "-p${DB_PASSWORD}"]
        retries: 3
        timeout: 5s
```

The `image` parameter indicates which _image_ should be used for this container. An easy way to understand the difference between images and containers is to borrow from Object-Oriented Programming concepts – an image is akin to a class and a container to an instance of that class.

Here, we specify that we want to use the tag `8.0` of the `mysql/mysql-server` image, corresponding to MySQL Server version 8.0. By default, images are downloaded from [Docker Hub](https://hub.docker.com), which is the largest Docker image registry. Have a look at [the page for MySQL Server](https://hub.docker.com/r/mysql/mysql-server) – most images come with simple documentation explaining how to use them.

The `ports` section allows us to map local ports to container ports, following the `local:container` notation. In the code snippet above, the value of the `FORWARD_DB_PORT` environment variable (or `3306` if that value is empty) is mapped to the container's `3306` port. This is mostly useful to connect third-party software to the database, like [MySQL Workbench](https://www.mysql.com/products/workbench/) or [Sequel Ace](https://sequel-ace.com/) (the setup would also work without it).

`environments` is where we define environment variables for the container. Here, most of them receive the value of existing environment variables, which are loaded from the `.env` file at the root of the project – `docker-compose.yml` automatically detects and imports the content of this file. For instance, in the `MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'` line, the container's `MYSQL_ROOT_PASSWORD` environment variable will receive the value of `DB_PASSWORD` from the `.env` file.

`volumes` is to declare some of the container's files or folders as _volumes_, either by mapping specific local files or folders to them or by letting Docker deal with it.

Here, a single Docker-managed volume is defined: `sailmysql`. This type of volume must be declared in a separate `volumes` section, at the same level as `services`. We can find it at the bottom of the `docker-compose.yml` file:

```
volumes:
    sailmysql:
        driver: local
    sailredis:
        driver: local
    sailmeilisearch:
        driver: local
```

The `sailmysql` volume is mapped to the container's `/var/lib/mysql` folder, which is where the MySQL data is stored. This volume ensures that the data is persisted even when the container is destroyed, which is the case when we run the `./vendor/bin/sail down` command.

The `networks` section allows us to specify which internal networks the container should be available on. Here, all services are connected to the same `sail` network, which is also defined at the bottom of `docker-compose.yml`, in the `networks` section above the `volumes` one:

```
networks:
    sail:
        driver: bridge
```

Finally, `healthcheck` is a way to indicate which conditions need to be true for the service to be _ready_, as opposed to just be _started_. I'll get back to this soon.

#### The `redis` service

The `redis` service is very similar to the `mysql` one:

```
redis:
    image: 'redis:alpine'
    ports:
        - '${FORWARD_REDIS_PORT:-6379}:6379'
    volumes:
        - 'sailredis:/data'
    networks:
        - sail
    healthcheck:
        test: ["CMD", "redis-cli", "ping"]
        retries: 3
        timeout: 5s
```

We pull the `alpine` tag of the [official image](https://hub.docker.com/_/redis) for Redis ([Alpine](https://alpinelinux.org/) is a lightweight Linux distribution) and we define which port to forward. We then declare a volume to persist the data, connect the container to the `sail` network, and define the health check to perform to ensure the service is ready.

#### The `laravel.test` service

The `laravel.test` service is slightly more complex:

```
laravel.test:
    build:
        context: ./vendor/laravel/sail/runtimes/8.0
        dockerfile: Dockerfile
        args:
            WWWGROUP: '${WWWGROUP}'
    image: sail-8.0/app
    extra_hosts:
        - 'host.docker.internal:host-gateway'
    ports:
        - '${APP_PORT:-80}:80'
    environment:
        WWWUSER: '${WWWUSER}'
        LARAVEL_SAIL: 1
        XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}'
        XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}'
    volumes:
        - '.:/var/www/html'
    networks:
        - sail
    depends_on:
        - mysql
        - redis
        - meilisearch
        - selenium
```

For starters, the name is a bit confusing, but this service is the one handling PHP (i.e. the one serving the Laravel application).

Next, it has a `build` section that we haven't seen before, which points to the [Dockerfile](https://github.com/laravel/sail/blob/1.x/runtimes/8.0/Dockerfile) that is present under the `vendor/laravel/sail/runtimes/8.0` folder.

[Dockerfiles](https://docs.docker.com/engine/reference/builder/) are text documents containing instructions to build images. Instead of using an existing image _as-is_ from Docker Hub, the Laravel team described their own in a Dockerfile. The first time we run the `./vendor/bin/sail up` command, we build that image and create a container based on it.

Open the Dockerfile and take a look at the first line:

```
FROM ubuntu:21.04
```

This means that the tag `21.04` of [Ubuntu's image](https://hub.docker.com/_/ubuntu) is used as a starting point for the custom image; the rest of the file is essentially a list of instructions to build upon it, installing everything a standard Laravel application needs. That includes PHP, various extensions, and other packages like Git or Supervisor, as well as Composer.

The end of the file also deserves a quick explanation:

```
COPY start-container /usr/local/bin/start-container
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY php.ini /etc/php/8.0/cli/conf.d/99-sail.ini
RUN chmod +x /usr/local/bin/start-container

EXPOSE 8000

ENTRYPOINT ["start-container"]
```

We can see that a bunch of local files are copied over to the container:

* the [php.ini file](https://github.com/laravel/sail/blob/1.x/runtimes/8.0/php.ini) is some custom configuration for PHP;
* the [supervisord.conf file](https://github.com/laravel/sail/blob/1.x/runtimes/8.0/supervisord.conf) is a configuration file for [Supervisor](http://supervisord.org/), a process manager here responsible for starting the PHP process;
* the [start-container file](https://github.com/laravel/sail/blob/1.x/runtimes/8.0/start-container) is a Bash script that will do a few things every time the container starts, because it is defined as the container's [ENTRYPOINT](https://docs.docker.com/engine/reference/builder/#entrypoint). We can see that it's made executable by the `RUN chmod +x` instruction;
* finally, `EXPOSE 8000` doesn't do anything, apart from informing the reader that this container listens on the specified port at runtime (which actually seems wrong here, since the application is served on port 80, not 8000).

Other things are happening in this Dockerfile, but the above is the gist of it. Note that this one pertains to PHP 8.0, but Laravel Sail also supports [other versions](https://github.com/laravel/sail/blob/1.x/runtimes) whose Dockerfiles you can point to from the `laravel.test` service in `docker-compose.yml`, instead of that of PHP 8.0.

Bringing our attention back to `docker-compose.yml`, we can see that there is an `extra_hosts` section we haven't encountered before. I won't get into too much detail, but it's here to ensure [Xdebug](https://xdebug.org/) runs smoothly [across operating systems](https://github.com/laravel/sail/pull/222).

The service also has a `depends_on` section containing the list of services that should be available before the Laravel application starts. Here, we need MySQL, Redis, Meilisearch and Selenium to be up and running first to avoid connection errors.

This is where the health checks described earlier are useful. By default, `depends_on` will wait for the specified services to be _started_, which doesn't necessarily mean they are _ready_. By specifying on which conditions these services are deemed ready, we ensure they are in the required state prior to starting the Laravel application.

The rest of the settings should be familiar by now, so I'll skip them.

#### The `meilisearch`, `mailhog` and `selenium` services

These are the smaller services I referred to earlier; they are already documented [here](https://laravel.com/docs/sail#meilisearch), [here](https://laravel.com/docs/sail#previewing-emails) and [here](https://laravel.com/docs/sail#laravel-dusk). The point is they work the same way as the other ones: they pull existing images from Docker Hub and use them as-is, with minimal configuration.

### The `sail` script

If you followed [Laravel's installation instructions](https://laravel.com/docs/installation#your-first-laravel-project) earlier, you must have run the following command at some point:

```
$ ./vendor/bin/sail up
```

The [sail file](https://github.com/laravel/sail/blob/1.x/bin/sail) that we call here is a Bash script whose function is to present a user-friendly interface to interact with Sail, saving us the pain to run and remember sometimes long-winded Docker commands.

Let's open it now for closer inspection (don't worry if you're not familiar with Bash – it's pretty straightforward).

We can ignore the whole first part of the file and focus on the big `if` statement which starts like this:

```
if [ $# -gt 0 ]; then
    # Proxy PHP commands to the "php" binary on the application container...
    if [ "$1" == "php" ]; then
        # ...
```

In plain English, the `$# -gt 0` bit translates to "if the number of arguments is greater than 0", meaning whenever we call the `sail` script with arguments, the execution will enter that `if` statement.

In other words, when we run the `./vendor/bin/sail up` command, we call the `sail` script with the `up` argument, and the execution gets inside the big `if` statement where it looks for a condition matching the `up` argument. Since there is none, the script goes all the way down to the end of the big `if`, in the sort of catch-all `else` we can find there:

```
# Pass unknown commands to the "docker-compose" binary...
else
    docker compose "$@"
fi
```

The comment already tells us what's going on – the script passes the `up` argument on to the `docker-compose` binary. In other words, when we run `./vendor/bin/sail up` we actually run `docker-compose up`, which is the [standard Docker Compose command](https://docs.docker.com/compose/reference/up/) to start the containers for the services listed in `docker-compose.yml`.

This command downloads the corresponding images once if necessary, and builds the Laravel image based on the Dockerfile we talked about earlier.

Give it a try! Run `./vendor/bin/sail up` first, then `docker compose up` – they do the same thing.

Let's now look at a more complicated example, one involving Composer, which is among the packages installed by the application's Dockerfile. But before we do that, let's start Sail in [detached mode](https://laravel.com/docs/sail#starting-and-stopping-sail) to run the containers in the background:

```
$ ./vendor/bin/sail up -d
```

The `sail` script allows us to run Composer commands, e.g.:

```
$ ./vendor/bin/sail composer --version
```

The above calls the `sail` script with `composer` and `--version` as arguments, meaning the execution will enter that big `if` statement again.

Let's look for the condition dealing with Composer:

```
# ...
# Proxy Composer commands to the "composer" binary on the application container...
elif [ "$1" == "composer" ]; then
    shift 1

    if [ "$EXEC" == "yes" ]; then
        docker-compose exec \
            -u sail \
            "$APP_SERVICE" \
            composer "$@"
    else
        sail_is_not_running
    fi
    # ...
```

The first line of the condition starts with `shift`, which is a Bash _built-in_ that skips as many arguments as the number it is followed by. In this case, `shift 1` skips the `composer` argument, making `--version` the new first argument. The program then makes sure that Sail is running, before executing a weird command split over four lines, which I break down below:

```
docker-compose exec \
    -u sail \
    "$APP_SERVICE" \
    composer "$@"
```

`exec` is the way Docker Compose allows us to execute commands on a container which is already running. `-u` is an option indicating which user we want to execute the command as, and `$APP_SERVICE` is the container on which we want to run it all. Here, its value is `laravel.test`, which is the service's name in `docker-compose.yml` as explained in a previous section. It is followed by the command we want to run once we're in the container, namely `composer` followed by all the script's arguments. These now only comprise `--version`, since we've skipped the first argument.

In other words, when we run:

```
$ ./vendor/bin/sail composer --version
```

The command that is executed behind the scenes is the following:

```
$ docker compose exec -u sail "laravel.test" composer "--version"
```

It would be quite cumbersome to type this kind of command every single time; that's why the `sail` script provides shortcuts for them, making the user experience much smoother.

Have a look at the rest of the smaller `if` statements inside the big one to see what else is covered – you'll see that the same principle is always more or less applied.

***

There are a few other features available out of the box (like [making local containers public](https://laravel.com/docs/sail#sharing-your-site)), but we've now covered the substance of what Laravel Sail currently offers. While this is a pretty good start already, it is somewhat limited, even for a basic application.

The good news is that the Laravel team is aware of this, and built the environment with extension in mind:

> _Since Sail is just Docker, you are free to customize nearly everything about it._

– [The Laravel documentation](https://laravel.com/docs/sail#sail-customization)

Let's see what that means in practice.

## Extending Laravel Sail

The code covered in this section is also available as a [GitHub repository](https://github.com/osteel/laravel-sail-extended) you can refer to at any moment.

We're going to explore three ways to extend Laravel Sail, using [MongoDB](https://www.mongodb.com) as a test subject. But before we proceed, let's make sure we get our hands on as many configuration files as possible.

The only thing we've got access to initially is the [docker-compose.yml file](https://github.com/laravel/sail/blob/1.x/stubs/docker-compose.stub), but we can publish more assets with the following command, which will create a new `docker` folder at the root of the project:

```
$ ./vendor/bin/sail artisan sail:publish
```

We'll get back to those in a minute; for the time being, let's try and instal the [Laravel MongoDB](https://github.com/jenssegers/laravel-mongodb) package, which will make it easy to use MongoDB with our favourite framework:

```
$ ./vendor/bin/sail composer require jenssegers/mongodb
```

Unfortunately, Composer is complaining about some missing extension:

```
...
- mongodb/mongodb[dev-master, 1.10.0, ..., v1.10.x-dev] require ext-mongodb ^1.11.0 -> it is missing from your system. Install or enable PHP's mongodb extension.
...
```

Let's fix this!

### Installing extra extensions

Earlier in this post, we talked about the way Sail uses Dockerfiles to build images matching Laravel's requirements for various PHP versions. These files were published with the command we ran at the beginning of this section – all we need to do is edit them to add the extensions we need, and to rebuild the corresponding images.

Many extensions are available out of the box and we can list them with the following command:

```
$ ./vendor/bin/sail php -m
```

MongoDB is not part of them; to add it, open the `docker/8.0/Dockerfile` file and spot the `RUN` instruction installing the various packages:

```
RUN apt-get update \
    && apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python2 \
    && mkdir -p ~/.gnupg \
    && chmod 600 ~/.gnupg \
    && echo "disable-ipv6" >> ~/.gnupg/dirmngr.conf \
    && apt-key adv --homedir ~/.gnupg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys E5267A6C \
    && apt-key adv --homedir ~/.gnupg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C300EE8C \
    && echo "deb http://ppa.launchpad.net/ondrej/php/ubuntu hirsute main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \
    && apt-get update \
    && apt-get install -y php8.0-cli php8.0-dev \
        php8.0-pgsql php8.0-sqlite3 php8.0-gd \
        php8.0-curl php8.0-memcached \
        php8.0-imap php8.0-mysql php8.0-mbstring \
        php8.0-xml php8.0-zip php8.0-bcmath php8.0-soap \
        php8.0-intl php8.0-readline php8.0-pcov \
        php8.0-msgpack php8.0-igbinary php8.0-ldap \
        php8.0-redis php8.0-swoole php8.0-xdebug \
    && php -r "readfile('http://getcomposer.org/installer');" | php -- --install-dir=/usr/bin/ --filename=composer \
    && curl -sL https://deb.nodesource.com/setup_$NODE_VERSION.x | bash - \
    && apt-get install -y nodejs \
    && npm install -g npm \
    && curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
    && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
    && apt-get update \
    && apt-get install -y yarn \
    && apt-get install -y mysql-client \
    && apt-get install -y postgresql-client \
    && apt-get -y autoremove \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
```

It's easy to identify the block related to PHP extensions since they all start with `php8.0`. Add the MongoDB extension at the end of the list:

```
php8.0-redis php8.0-swoole php8.0-xdebug php8.0-mongodb \
```

You can see the detail of available PHP extensions for Ubuntu 21.04 [here](https://packages.ubuntu.com/hirsute/php/).

Save the file and run the following command:

```
$ ./vendor/bin/sail build
```

This will go through all the services listed in `docker-compose.yml` and build the corresponding images if they have changed, including the `laravel.test` service's, whose Dockerfile we've just updated.

Once it's done, start the containers again:

```
$ ./vendor/bin/sail up -d
```

The command will detect that the image corresponding to the `laravel.test` service has changed, and recreate the container:

```
...
laravel-sail-extended_mailhog_1 is up-to-date
laravel-sail-extended_meilisearch_1 is up-to-date
Recreating laravel-sail-extended_laravel.test_1 ... done
```

That's it! The MongoDB extension for PHP is now installed and enabled. We've only done it for the PHP 8.0 image, but you can apply the same process to [other PHP versions](https://github.com/laravel/sail/tree/1.x/runtimes) by updating their own Dockerfile, with the right extension name (e.g. `php7.4-mongodb`).

We can now safely import the Laravel MongoDB package:

```
$ ./vendor/bin/sail composer require jenssegers/mongodb
```

Next up: adding a Docker service for MongoDB.

### Adding new services

MongoDB is essentially another database, meaning the corresponding service will be very similar to that of MySQL and Redis. A quick search on [Docker Hub](https://hub.docker.com) reveals that there is [an official image](https://hub.docker.com/_/mongo) for it, which we are going to use.

Its documentation contains an example configuration for Docker Compose, which we can copy and adjust to our needs. Open `docker-compose.yml` and add the following service at the bottom, after the other ones:

```
mongo:
    image: 'mongo:4.4'
    restart: always
    environment:
        MONGO_INITDB_ROOT_USERNAME: '${DB_USERNAME}'
        MONGO_INITDB_ROOT_PASSWORD: '${DB_PASSWORD}'
        MONGO_INITDB_DATABASE: '${DB_DATABASE}'
    volumes:
        - 'sailmongo:/data/db'
    networks:
        - sail
```

The changes I've made are the following: first, I specified the tag `4.4` of the `mongo` image. If you don't specify one, Docker Compose will pull the `latest` tag by default, which is not a good practice since it will refer to different versions of MongoDB over time, as new releases are available. The introduction of breaking changes could create instability in your Docker setup, so it's better to target a specific version, ideally matching the production one.

Then, I declared a `MONGO_INITDB_DATABASE` environment variable for the container to create a database with the corresponding name at start-up, and I matched the value of each environment variable to one coming from the `.env` file (we'll come back to those in a minute).

I also added a `volumes` section, mounting a Docker-managed volume in the container's `/data/db` folder. The same principle as MySQL and Redis here applies – if you don't persist the data on your local machine, it will be lost every time the MongoDB container is destroyed. In other words, as the MongoDB data is stored in the container's `/data/db` folder, we persist that folder locally using a volume.

As this volume doesn't exist yet, we need to declare it at the bottom of `docker-compose.yml`, after the other ones:

```
volumes:
    sailmysql:
        driver: local
    sailredis:
        driver: local
    sailmeilisearch:
        driver: local
    sailmongo:
        driver: local
```

Finally, I added the `networks` section to ensure the service is on the same network as the others.

We can now configure Laravel MongoDB as per the package's [instructions](https://github.com/jenssegers/laravel-mongodb#configuration). Open `config/database.php` and add the following database connection:

```
'mongodb' => [
    'driver' => 'mongodb',
    'host' => env('DB_HOST'),
    'port' => env('DB_PORT'),
    'database' => env('DB_DATABASE'),
    'username' => env('DB_USERNAME'),
    'password' => env('DB_PASSWORD'),
    'options' => [
        'database' => env('DB_AUTHENTICATION_DATABASE', 'admin'),
    ],
],
```

Open the `.env` file at the root of the project and change the database values as follows:

```
DB_CONNECTION=mongodb
DB_HOST=mongo
DB_PORT=27017
DB_DATABASE=laravel_sail
DB_USERNAME=root
DB_PASSWORD=root
```

The above makes MongoDB the main database connection. In a real case scenario, you might want to make it a secondary database like Redis, but for demonstration purposes, this will do.

`DB_HOST` is the name of the MongoDB service from `docker-compose.yml`. Behind the scenes, Docker Compose resolves the service's name to the container's IP on the networks it manages (in our case, that's the single `sail` network defined at the end of `docker-compose.yml`).

`DB_PORT` is the port MongoDB is available on, which is `27017` by default, as per the [image's description](https://hub.docker.com/_/mongo).

We're ready for a test! Run the following command again:

```
$ ./vendor/bin/sail up -d
```

It will download MongoDB's image, create the new volume and start the new container, which will also create the `laravel_sail` database:

```
➜  ./vendor/bin/sail up -d
Pulling mongo (mongo:4.4)...
4.4: Pulling from library/mongo
7b1a6ab2e44d: Pull complete
90eb44ebc60b: Pull complete
...
```

Let's make sure of that by running Laravel's default migrations:

```
$ ./vendor/bin/sail artisan migrate
```

We can push the test further by updating the `User` model so it [extends](https://github.com/jenssegers/laravel-mongodb#extending-the-authenticable-base-model) Laravel MongoDB's `Authenticable` model:

```
<?php

namespace App\Models;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Notifications\Notifiable;
use Jenssegers\Mongodb\Auth\User as Authenticatable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    // ...
```

Use Tinker to try and create a model:

```
$ ./vendor/bin/sail tinker

Psy Shell v0.10.5 (PHP 8.0.0 — cli) by Justin Hileman
>>> \App\Models\User::factory()->create();
```

Great! Our MongoDB integration is functional.

We can keep interacting with it using Tinker and Eloquent, but oftentimes it is useful to have direct access to the database, through third-party software or via a command-line interface such as the [Mongo shell](https://docs.mongodb.com/manual/mongo/).

Let's add the latter to our setup.

### Custom `sail` commands

The good news is the Mongo shell is already available, as long as we know the right formula to summon it. Here it is, along with some extra commands to log into the database and list the users (run the first command from the project's root):

```
$ docker-compose exec mongo mongo

MongoDB shell version v4.4.10
connecting to: mongodb://127.0.0.1:27017/?compressors=disabled&gssapiServiceName=mongodb
Implicit session: session { "id" : UUID("be711959-a359-4d1f-8fd4-9af7bad42c23") }
MongoDB server version: 4.4.10
Welcome to the MongoDB shell.
For interactive help, type "help".
For more comprehensive documentation, see
    https://docs.mongodb.com/
Questions? Try the MongoDB Developer Community Forums
    https://community.mongodb.com
> use admin
switched to db admin
> db.auth("root", "root")
1
> use laravel_sail
switched to db laravel_sail
> db.users.find()
```

The `docker-compose exec mongo mongo` command should look familiar – earlier in the article, we looked at what the `sail` script does behind the scenes, which mostly consists of translating simple `sail` commands into more complex `docker-compose` ones. Here, we're telling the `docker-compose` binary to execute the `mongo` command on the `mongo` container.

To be fair, this command isn't too bad and we could easily remember it; but for consistency, it would be nice to have a simpler `sail` equivalent, like the following:

```
$ ./vendor/bin/sail mongo
```

To achieve this we'd need to complete the `sail` script somehow, but as it is located inside the `vendor` folder – which is created by Composer – we cannot update it directly. We need a way to build upon it without modifying it, which I've summarised below:

1. make a copy of the `sail` script at the root of the project;
2. replace the content of its big `if` statement with custom conditions;
3. if none of the custom conditions matches the current arguments, pass them on to the original `sail` script.

If we take a closer look at the `sail` file with `ls -al`, we can see that it's a symbolic link to the `vendor/laravel/sail/bin/sail` file:

```
➜  ls -al vendor/bin
total 0
drwxr-xr-x   8 yannick  staff   256 26 Nov 10:37 .
drwxr-xr-x  48 yannick  staff  1536 26 Nov 12:24 ..
...
lrwxr-xr-x   1 yannick  staff    24 26 Nov 10:36 sail -> ../laravel/sail/bin/sail
...
```

Copy that file and paste it at the root of our project:

```
$ cp vendor/laravel/sail/bin/sail .
```

Open the new copy and replace the content of its big `if` with the following, leaving the rest as-is:

```
if [ $# -gt 0 ]; then
    # Initiate a Mongo shell terminal session within the "mongo" container...
    if [ "$1" == "mongo" ]; then

        if [ "$EXEC" == "yes" ]; then
            docker-compose exec mongo mongo
        else
            sail_is_not_running
        fi

    # Pass unknown commands to the original "sail" script...
    else
        ./vendor/bin/sail "$@"
    fi
fi
```

In the above code, we removed all the `if...else` conditions inside the big `if` and added one of our own, which runs the command we used earlier to access the Mongo shell if the value of the script's first argument is `mongo`. If it's not, the execution will hit the last `else` statement and call the original `sail` script with all the arguments.

You can try this out now – save the file and run the following command:

```
$ ./sail mongo
```

It should open a Mongo shell session in your terminal.

Try another command, to make sure the original `sail` script is taking over when it's supposed to:

```
$ ./sail artisan
```

The Artisan menu should display.

That's it! If you need more commands, you can add them as new `if...else` conditions inside the big `if` of the copy of the `sail` script at the root of the project.

It works exactly the same way, except that you now need to run `./sail` instead of `./vendor/bin/sail` (or update your Bash alias if you created one as suggested in the [documentation](https://laravel.com/docs/sail#configuring-a-bash-alias)).

***

We are now running a fully functional instance of MongoDB as part of our Docker setup, nicely integrated with Laravel Sail. But MongoDB is a mere example here – you can do the same with pretty much any technology you'd like to use.

Go [take a look](https://hub.docker.com) now! Most major actors have Docker images – official or maintained by the community – with easy-to-follow instructions. In most cases, you'll have a local instance of the software running in minutes.

There are probably many more things we could do to customise Laravel Sail, but the three methods described above should get you a long way already.

At this stage, you may be thinking that Laravel's new environment has a lot going for it, maybe even more so than you initially thought. Yet, the point of this article is to move away from it...

So where am I going with this?

## What's wrong with Laravel Sail anyway?

If you made it this far, you're probably wondering what's wrong with Laravel Sail, now that it's clear how far we can push it.

As it stands, it's a pretty decent solution. In an earlier version of this article, I was pointing to a whole list of issues which have now largely been solved. So why not use it?

Let me break it to you right now: once you know and understand everything I've explained in the previous sections, you don't need Laravel Sail anymore.

That's right – you can take that knowledge and walk away.

But before I elaborate on this, let's review some remaining pain points of Sail, even though I expect the Laravel team to address most of them sooner rather than later.

The first one concerns the custom `sail` commands: while it's possible to extend the `sail` script as demonstrated above, the process is a bit ugly and somewhat hacky. Sail's maintainers could fix this with an explicit Bash extension point allowing users to add their own shortcuts, or by publishing the `sail` script along with the other files.

Second, the Laravel application is served by PHP's development server. I won't go into too much detail here, but as mentioned earlier, [Supervisor](http://supervisord.org/) manages the PHP process in the `laravel.test` container. [This line](https://github.com/laravel/sail/blob/1.x/runtimes/8.0/supervisord.conf#L8) is where Supervisor runs the `php artisan serve` command, which starts PHP's development server under the hood.

The point here is that the environment doesn't use a proper web server (e.g. Nginx), which means we can't easily have local domain names, nor bring HTTPS to the setup. This may be fine for quick prototyping, but more elaborate development will most likely need those.

The last issue belongs to a separate category, as it relates to Docker overall and not Laravel Sail specifically. It should be carefully considered before going down the Docker path and deserves a section of its own.

## The whale in the cabin

The one major caveat that appears to be absent from the conversation so far relates to performance. While this shouldn't affect Linux users, if you run Docker Desktop on your system you will most likely experience long execution times, especially on macOS (it seems that using [WSL 2](https://docs.docker.com/docker-for-windows/wsl/) on Windows can mitigate the slowness).

While HTTP response times have improved a great deal, running local commands like Yarn or Composer is still very slow. You've probably experienced it yourself while going through this tutorial, when we installed the Laravel MongoDB package with Composer, for instance.

I won't go into too much detail here, but the reason essentially comes from the host's underlying filesystem, which does not perform well around mounted local directories. As we've seen, this is how Laravel Sail gets the application's source code in the Laravel application's container.

This is where an approach like [Takeout](https://tech.osteel.me/posts/you-dont-need-laravel-sail)'s makes sense, as instead of running PHP from a Docker container, they expect developers to run it on their local machine (e.g. via [Valet](https://laravel.com/docs/valet)), all the while providing instances of services like MySQL or MongoDB, thus offering convenience without sacrificing performance. But from the moment you choose to run PHP via a Docker container (like Sail does), the added value of Takeout decreases, in my opinion.

There are strategies to mitigate these performance issues, but the Laravel documentation doesn't mention them, let alone that performance might be degraded at all, which I find surprising.

That being said, you might be comfortable enough with performance as it is; I, for one, have been OK with it for years, even though I use Docker Desktop on macOS. The bottom line is that this aspect should be carefully considered before moving your whole setup to a solution running PHP in a container, be it Laravel Sail or something else.

But once you've made that decision, and whether or not the other issues are eventually addressed, the main idea of this article remains the same.

## You don't need Laravel Sail

If you're considering building anything substantial using Laravel Sail as your development environment, sooner or later you will have to extend it. You'll find yourself fumbling around the Dockerfiles and eventually writing your own; you'll have to add some services to `docker-compose.yml`, and maybe throw in a few custom Bash commands.

Once you get there, there's one question you should ask yourself:

> _What's stopping me from building my own setup?_

The answer is _nothing_. Once you feel comfortable extending Laravel Sail, you already have the knowledge required to build your own environment.

Think about it: the `docker-compose.yml` file is not specific to Laravel Sail, that's just how Docker Compose works. The same goes for Dockerfiles – they are standard Docker stuff. The Bash layer? That's all there is to it – some Bash code, and as you can see, it's not that complicated.

So why artificially restrain yourself within the constraints of Sail?

And more importantly: why limit yourself to using Docker in the context of Laravel?

Your application may start as a monolith, but it might not always be. Perhaps you've got a separate frontend, and you use Laravel as the API layer. In that case, you might want your development environment to manage them both; to run them simultaneously so they interact with each other like they do on a staging environment or in production.

If your whole application is a [monorepo](https://en.wikipedia.org/wiki/Monorepo), your Docker configuration and Bash script could be at the root of the project, and you could have your frontend and backend applications in separate subfolders, e.g. `src`.

The corresponding tree view would look something like this:

```
my-app/
├── bash-script
├── docker-compose.yml
└── src/
    ├── backend/
    │   └── Dockerfile
    └── frontend/
        └── Dockerfile
```

The `docker-compose.yml` file would declare two services – one for the backend and one for the frontend – both pointing to their respective Dockerfile.

If the backend and the frontend live in different repositories, you could create a third one, containing your Docker development environment exclusively. Just _git-ignore_ the `src` folder and complete your Bash script so that it pulls both application repositories into it, using the same commands you would normally run by hand.

Even if your project is a Laravel monolith, this kind of structure is already cleaner than mixing up development-related files with the rest of the source code. Moreover, if your application grows bigger and needs other components besides Laravel, you're already in a good position to support them.

Once you've made the effort to understand Laravel Sail to extend it, nothing's stopping you from building your own development environments, _whether or not Laravel is part of the equation_. That's right, you can build bespoke Docker-based environments for anything.

And if Laravel is part of the stack, nothing prevents you from reusing Sail's Dockerfiles if you're not comfortable writing your own yet; after all, they are already optimised for Laravel. Likewise, you can draw inspiration from Sail's `docker-compose.yml` file if that helps.

## Conclusion

Don't get me wrong – Sail has a lot going for it, and I am glad to see an established actor such as Laravel push forward the adoption of Docker for local development.

We love our frameworks because they offer guidelines to achieve desired results in a way we know to be efficient and battle-tested, and it's only natural that they also seek to provide the environment that will allow their users to build upon them. But one thing that Sail incidentally shows us is that this doesn't have to be part of the framework's mandate anymore.

Much like Truman's sailboat helps him overcome his fear of the sea and takes him to the edges of the artificial world he lives in, Sail reveals both the confines of Laravel and a way to escape from them.

You may feel that Sail is more than enough for your needs today, or that you're not yet ready to go on your own. That's fine. But Laravel will always be limited by its monolithic nature, and as you grow as a developer, the day will come where your Laravel application will be but a single component of a larger system, for which Sail won't be enough anymore. Eventually, your small sailboat will bump into a painted backdrop.

If you'd like to explore this further but feel like you need more guidance, I've published [a series on the subject](/posts/docker-for-local-web-development-introduction-why-should-you-care "Docker for local web development, introduction: why should you care?") that should get you going. It requires no prior knowledge of Docker and covers web servers, HTTPS, domain names and many other things. It doesn't have all the answers but will get you to a place where you can find your own.

What you do next is entirely up to you; just know that there's a whole world out there, waiting for you.

> _Truman hesitates. Perhaps he cannot go through with it after all. The camera slowly zooms into Truman's face._
>
> _**TRUMAN:** "In case I don't see you – good afternoon, good evening and good night."_
>
> _He steps through the door and is gone._

## Resources

* [Laravel Sail documentation](https://laravel.com/docs/sail)
* [Laravel Sail repository](https://github.com/laravel/sail)
* [This article's repository](https://github.com/osteel/laravel-sail-extended)
* [Docker Hub](https://hub.docker.com)
* [Docker documentation](https://docs.docker.com)
* [Overview of Docker Compose](https://docs.docker.com/compose/)
* [Dockerfile reference](https://docs.docker.com/engine/reference/builder/)
* [What is a Container?](https://www.docker.com/resources/what-container)
* [Docker for local web development series](/posts/docker-for-local-web-development-introduction-why-should-you-care "Docker for local web development, introduction: why should you care?")