Laravel Sail: a developer’s cheat sheet 🐳

Laravel ships with Sail — a thin command-line wrapper around docker compose that gives you the whole Laravel toolchain (PHP, MySQL, Redis, Mailpit, Node) in containers, without you needing to install any of them on your host. The only thing you need on the laptop is Docker. Everything else lives in containers and goes away when you delete the project.

This is the quick-reference I keep open in another tab while building Laravel apps on macOS. 🍎

What you actually need on the host

  • macOS (these notes target Apple Silicon and Intel Macs equally)
  • Docker Desktop — the only hard prerequisite. Sail uses it for everything else (PHP, Composer, Node, MySQL, Redis).
  • That’s it. You don’t need PHP installed locally. You don’t need Composer locally. You don’t need Node locally. You install them once via Sail’s bootstrap and from then on every command runs inside containers.

Spin up a fresh project (with MySQL and Redis)

The official one-liner uses Laravel’s builder image to scaffold a new app and pre-select the services you want. Tell it mysql and redis in the with query parameter:

1
2
3
curl -s "https://laravel.build/example-app?with=mysql,redis" | bash
cd example-app
./vendor/bin/sail up -d

That brings up four containers — your app, MySQL, Redis, and Mailpit (the dev mail-catcher) — and exposes the app on http://localhost. The first run pulls images and takes a couple of minutes; subsequent sail up calls are fast.

Tip: alias sail so you don’t have to type the long path every time.

1
alias sail='[ -f sail ] && sh sail || sh vendor/bin/sail'

Drop that into your ~/.zshrc and you can just type sail up -d, sail artisan …, etc., from anywhere inside a Sail project.

The Artisan commands you’ll reach for daily

Anything you’d run as php artisan … on a non-Sail setup, you run as sail artisan …. Sail just shells into the app container and forwards the command. The most common ones:

1
2
3
4
5
6
sail artisan tinker                      # interactive REPL with your app booted
sail artisan route:list                  # show every registered route
sail artisan migrate                     # run pending migrations
sail artisan make:controller UserController
sail artisan make:model Department -m    # model + migration in one shot
sail artisan queue:work                  # start a worker against the default queue

tinker is the standout feature you’ll likely use most — it’s a Laravel-aware PHP REPL with every facade, every model, and your full config() ready to go. Need to check what User::find(1)->roles returns? sail artisan tinker, type the expression, get an answer. Beats writing a controller-and-route just to peek at data.

Mailpit — see every email your app sends

Sail bundles Mailpit, a friendly local SMTP server with a web UI. Any mail your app tries to send (password resets, notifications, queued emails) gets caught and shown at:

1
http://localhost:8025

No SMTP credentials, no real provider, no actual emails leaving your machine. Just open the inbox and see what your app sent. The .env Sail generates already wires MAIL_MAILER=smtp, MAIL_HOST=mailpit, MAIL_PORT=1025, so it works on first run.

Database workflow: migrate, seed, refresh

The mental model: migrations describe schema changes, seeders insert sample data, and there’s a small family of commands for moving between states while you’re iterating on a feature.

1
2
3
4
5
6
7
8
9
# Wipe the database, re-run every migration from scratch, then run seeders
sail artisan migrate:refresh --seed

# Create a new migration file in database/migrations/
sail artisan make:migration create_departments_table

# Roll back the last batch (or the last N batches) and re-apply forward —
# the fastest way to iterate on a brand-new migration you're still tweaking
sail artisan migrate:rollback --step=1 && sail artisan migrate

The third one is the workhorse for daily development: edit the migration, roll it back one step, run forward, repeat. migrate:refresh –seed is heavier — it nukes everything and re-applies, so save it for when you’ve made many changes and want a clean slate.

Installing dependencies

Composer (PHP) and npm (frontend) both run inside the Sail container. The full “I just pulled a fresh branch” sequence:

1
sail composer install && sail npm install && sail npm run dev

sail npm run dev starts Vite in dev mode for hot reloading. For a production-style build, use sail npm run build and serve the compiled assets.

Routes and pages

The flow for a new page is short. Define a route, point it at a controller method, render a Blade view.

1
2
3
4
5
// routes/web.php
use App\Http\Controllers\DashboardController;

Route::get('/dashboard', [DashboardController::class, 'index'])
    ->name('dashboard');
1
sail artisan make:controller DashboardController
1
2
3
4
5
// app/Http/Controllers/DashboardController.php
public function index()
{
    return view('dashboard', ['user' => auth()->user()]);
}

Then check what’s wired by listing every registered route:

1
sail artisan route:list

Add –except-vendor to hide the Laravel default routes and see only yours; –name=dashboard filters to a single route by name.

Getting a shell inside a container

Sometimes you need to poke around inside a container — inspect a config file, run a one-off mysql command, check redis state. Sail has shortcuts:

1
2
3
sail shell        # bash inside the app container (root — be careful)
sail mysql        # mysql client connected to the dev database
sail redis        # redis-cli connected to the local redis

Under the hood these are just docker exec calls. The equivalents:

1
2
3
docker exec -it example-app-laravel.test-1 bash    # what 'sail shell' does
docker exec -it example-app-mysql-1 bash           # what 'sail mysql shell' does
docker exec -it example-app-redis-1 sh             # what 'sail redis shell' does

The container names are <project-name>-<service-name>-1, so substitute your project’s directory name for example-app. sail shell drops you in as root in the app container — that’s deliberate (Sail’s container is a development sandbox), but it does mean you can break things by being careless. Treat it like an SSH session into a dev box.

Tests

Laravel uses PHPUnit under the hood (with Pest as a popular alternative). Sail makes the runner one command:

1
2
3
4
5
6
7
8
# Generate a unit test stub
sail artisan make:test UserTest --unit

# Run the whole suite
sail artisan test

# Run with HTML coverage (output goes to ./coverage)
sail artisan test --coverage-html coverage

–unit creates the test under tests/Unit/ (no Laravel app boot, fastest to run). Without it, you get a feature test under tests/Feature/ which boots the application and gives you the full HTTP-style helpers ($this->get(‘/dashboard’)->assertOk()). Use Unit for pure logic, Feature for anything touching routes, models, or services.

The –coverage-html flag requires Xdebug or PCOV in the container. Sail’s image ships PCOV, so this works out of the box on a default Sail setup.

When things misbehave: the cleanup checklist

Laravel caches a lot — config, routes, views, compiled service container. After bigger changes (especially editing config/*.php or env vars), the caches can lie to you. The reset:

1
2
3
4
sail artisan cache:clear
sail artisan config:clear
sail artisan route:clear
sail artisan view:clear

And of course, the first place to look when something is broken is the application log. Tail it in a separate terminal while you reproduce the bug:

1
tail -f storage/logs/laravel.log

Stack traces, query logs, anything you’ve Log::info()‘d — it all ends up here. If your app is logging to a different channel (configured in config/logging.php), check there instead.

The day-to-day shape

Once you’ve used Sail for a project or two, the daily loop becomes muscle memory: sail up -d in the morning, sail artisan commands as you build, sail artisan test before pushing, sail down when you switch projects. Nothing leaks onto the host, every project’s PHP/MySQL/Redis versions stay independent, and onboarding a new teammate is “install Docker, clone the repo, ./vendor/bin/sail up“.

For most Laravel work I do these days, I never type php directly anymore. ⛵

This entry was posted in Web Development and tagged , , , . Bookmark the permalink.

Comments are closed.