Skip to content
ExponaBLOG
PHP & LARAVEL DEVELOPMENT · DAY 097 / 100

Laravel Sail with Docker: a hands-on guide for engineers

A Docker dev environment in one command.

Expona Admin6 min read

Laravel Sail with Docker: a hands-on guide for engineers

We've all been there: a new developer joins the team, spends two days installing PHP, MySQL, Redis, and three dependency managers, then discovers their version doesn't match production. Laravel Sail with Docker: a hands-on guide for engineers solves this problem in one command. Instead of fighting with system-level package managers and version conflicts, we use containers to ship the entire development stack—and it actually works the same way on every machine.

What Sail actually does for you

Laravel Sail is a lightweight Docker Compose wrapper that comes pre-configured in new Laravel projects. When you run sail up, you're not just spinning up a single container. You're orchestrating multiple services—PHP-FPM, Nginx, MySQL, Redis—that all talk to each other through a bridge network, all with sensible defaults already in place.

The beauty here is that Sail abstracts away the Docker boilerplate. You don't need to write a 60-line docker-compose.yml file from scratch or memorize volume mount syntax. It's already there, tested by thousands of teams, and ready to go. When you run sail up, you're leveraging a battle-tested configuration that the Laravel ecosystem has refined over years.

Think of Sail as the bridge between developers who love Docker but don't want to become DevOps engineers, and teams that need consistency across their entire development pipeline.

Sail up starts PHP, MySQL and Redis in one command

Let's walk through what happens when a fresh developer clones your repository and runs a single command:

php
sail up

Behind the scenes, Docker Compose reads the docker-compose.yml file that Sail maintains. This file defines three core services by default:

  • Laravel application container: Runs PHP-FPM and your application code
  • MySQL database: Pre-configured with the database name, user, and password from your .env file
  • Redis cache: Ready to handle sessions, queues, and caching without extra setup

All three services start in parallel, each in its own isolated environment. They're networked together automatically, so your Laravel app can reach MySQL on mysql:3306 and Redis on redis:6379 without any additional configuration.

Here's what your development team actually does:

  1. Clone the repository
  2. Run sail up
  3. Run sail artisan migrate in a new terminal tab
  4. Visit localhost and the app is live

That's it. No downloading MySQL Community Edition, no configuring port conflicts, no "works on my machine" arguments. The entire stack is containerized, versioned in your repository, and reproducible on every developer's machine.

When you need to see what's running, use:

php
sail ps

This shows you the status of every service, their exposed ports, and container IDs. It's your quick health check.

Same environment for every developer

Before containerization, this was a nightmare. Developer A had MySQL 5.7, developer B had 8.0, and production ran 8.0.23. Three months later, a subtle query incompatibility surfaced in staging. With Sail, everyone—including CI/CD pipelines—runs the exact same versions.

The docker-compose.yml file is committed to your repository, alongside your code. When you update a service version, it gets committed, reviewed in a pull request, and deployed consistently. No environment variable surprises. No "I'll update my local MySQL tomorrow."

Consider a real scenario: your team discovers that MySQL 5.7 doesn't handle JSON comparison the way 8.0 does. You make one change in docker-compose.yml:

php
services:
  mysql:
    image: 'mysql:8.0'
    # ... rest of config

You commit, push, and every developer who pulls the branch gets the same MySQL version automatically. No Slack messages. No onboarding friction. The environment versioning is baked into version control.

This extends to PHP extensions, too. If you need the pcntl extension for background workers, you build a custom Dockerfile that layers it on top of the standard PHP image. Everyone who uses that Dockerfile gets it. Testing becomes predictable because the test environment matches local development, which matches staging and production.

Add services like Meilisearch

Real applications need more than MySQL and Redis. You might need Elasticsearch for full-text search, Meilisearch for a better search experience, or Mailhog for email testing. Sail makes adding these services straightforward.

Let's say your team wants to integrate Meilisearch (an open-source search engine that's faster to set up than Elasticsearch and great for smaller teams). You modify docker-compose.yml:

php
services:
  meilisearch:
    image: 'getmeili/meilisearch:latest'
    environment:
      MEILI_MASTER_KEY: 'your-secret-key'
    ports:
      - '7700:7700'
    volumes:
      - 'sail-meilisearch:/meili_data'

volumes:
  sail-meilisearch:

Then update your .env file:

php
MEILISEARCH_HOST=meilisearch
MEILISEARCH_PORT=7700
MEILISEARCH_KEY=your-secret-key

Run sail up again, and Meilisearch is available at meilisearch:7700 from your Laravel application. Your team can now develop search features locally, test indexing logic, and debug without any external services or SaaS dashboards.

Other common additions include:

  • Mailhog: Captures outgoing emails in a web UI instead of sending them
  • MinIO: Local S3-compatible object storage for file upload testing
  • Postgres: If your team uses PostgreSQL instead of MySQL
  • Chrome/Selenium: For browser automation and headless testing

Each service gets its own container, its own volume for persistence, and its own environment variables. They're all networked together, so your Laravel app can talk to any of them as if they were running on localhost.

Use alias sail for convenience

Typing ./vendor/bin/sail gets old fast. Sail itself provides an alias setup command that makes sail a shell function available from anywhere in your project directory:

php
alias sail='sh $([ -f sail ] && echo sail || echo vendor/bin/sail)'

Add this to your .bashrc, .zshrc, or equivalent shell configuration file. Now sail up works from anywhere in your project. Same for sail artisan, sail composer require, and sail npm install.

This is more than convenience—it makes Sail feel native. When a new developer asks "how do I run tests," the answer is sail artisan test, not "well, you need to understand how Sail works first, then..." The command structure mirrors standard Laravel commands. The mental overhead drops significantly.

You can also set up a per-project alias if you work on multiple Sail projects. Just add it to a script in your repository root that developers source when they set up their environment.

Common mistakes

Forgetting to rebuild after changing docker-compose.yml: If you add a new service or update an image tag, run sail build before sail up. Otherwise, you'll be running the old configuration.

Not committing docker-compose.yml: Some teams put it in .gitignore because it "has secrets." Don't. Use environment variables for secrets (via .env, which should be in .gitignore). The compose file is configuration, not secrets, and it belongs in version control.

Running commands without sail: If you install dependencies with composer install directly on your host machine, you might get different versions or extensions than what's in the container. Use sail composer, sail npm, and sail artisan instead. Your host machine's PHP version shouldn't matter.

Ignoring volume persistence: By default, database data is stored in a named volume. When you run sail down, the volume persists. But if you run sail down -v, you delete the volume and lose your data. Know the difference, and document it for your team.

Key takeaways

  • Sail is a Docker Compose wrapper that eliminates environment setup friction and enforces consistency across your team
  • One command, sail up, starts a complete development stack: PHP, MySQL, Redis, and any additional services you define
  • Services are versioned in docker-compose.yml and committed to your repository, making environment parity automatic
  • Adding services like Meilisearch, Mailhog, or MinIO is a matter of adding them to the compose file and configuring environment variables
  • Using the sail alias makes commands feel native and reduces the mental overhead of containerized development
  • Always run dependency managers and Artisan through sail to ensure consistency inside the container
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles