Laravel Sail gives a team a Docker-based local environment and a consistent way to run application commands inside it. The practical benefit is a shared starting point for PHP, database and supporting services. It does not automatically make a laptop identical to production or make a project reproducible without agreed inputs.
This guide follows an existing Laravel 12 application with MySQL and Redis. It is a local-development runbook checked against the versioned documentation, not a claim that these containers were launched for this article. Keep production credentials and data outside the exercise. Docker was unavailable in our publication environment, so the runtime acceptance steps below remain checks for the target machine.
Agree what the local environment must reproduce
Start with a workflow: log in using a seeded test account, request a fictional report, process its background job, and read the result. List the services that workflow actually needs. A frontend-only change may not require a search engine or WebSocket server, while a queued import cannot be assessed solely by loading the homepage.
Record the application revision, PHP version, enabled extensions, database engine, Node version and relevant dependency lockfiles. Use fictional seed data and a mail catcher instead of live customers and outbound production email. Decide whether external integrations use a sandbox, a fixture or are deliberately unavailable. “Works locally” is meaningful only when another developer can reproduce the same starting conditions.
Install Sail into a copy of the existing project
The Laravel 12 Sail guide covers macOS, Linux and Windows through WSL2. Confirm Docker Engine/Desktop and Compose are available in the shell that holds the project. Follow your team's supported Docker setup rather than assuming a Windows terminal and a WSL shell share every path and environment variable.
With compatible host PHP and Composer available, the documented existing-project route starts with:
composer require laravel/sail --dev
php artisan sail:install
Select MySQL and Redis for this example. Inspect the generated Compose file and environment changes before starting it; existing Compose customizations need merging. Newer generated projects use compose.yaml, while older repositories may retain docker-compose.yml. Work from the file your project actually commits. If host PHP is unsuitable, use the documentation's supported container bootstrap path instead of ignoring dependency platform requirements.
Once dependencies exist and configuration is ready, start and inspect the services:
./vendor/bin/sail up -d
./vendor/bin/sail ps
./vendor/bin/sail php --version
./vendor/bin/sail artisan about
Each command has a separate purpose: start the configured services, inspect their status, identify container PHP and inspect Laravel's loaded environment. A healthy database container does not prove that the application has the right credentials or schema. Verify the first request and one database operation too.
Keep host ports separate from service ports
Suppose another project already uses host ports 80 and 3306. You can choose another application port and forwarded database port in the variables referenced by your generated Compose file. That changes how tools on the host reach the containers; it does not change the service-to-service address inside the Compose network.
| Caller | Destination | Why |
|---|---|---|
| Laravel container | mysql:3306 | The service name resolves inside the Compose network. |
| Host database client | 127.0.0.1:3307 | Example host mapping 3307 forwards to container port 3306. |
| Laravel container | redis:6379 | Use the configured Redis service and its internal port. |
| Browser on the host | http://localhost:8081 | Example application port chosen to avoid a host conflict. |
The table assumes services named mysql and redis; inspect the actual Compose file. The Docker Compose networking guide explains service-name resolution and published ports. Inside a container, localhost points back to that container, not automatically to the database service or host machine.
For that configuration, application variables would use DB_HOST=mysql, DB_PORT=3306 and REDIS_HOST=redis. A host database client uses the forwarded port instead. Bind published development databases to loopback when host access is needed, or remove the publication when only containers need access. Review resolved Compose settings locally; their output can include secrets and should not be pasted into public support threads.
Run commands in the environment you are checking
When the application runs inside Sail, use Sail's PHP, Composer and Node wrappers for its normal work. A successful command under host PHP can hide a missing container extension; a host Node installation can also differ from the build environment.
./vendor/bin/sail composer install
./vendor/bin/sail npm ci
./vendor/bin/sail npm run build
./vendor/bin/sail artisan migrate:status
./vendor/bin/sail test
npm ci assumes a committed compatible lockfile. migrate:status inspects migrations; applying them is a separate step after confirming the database is disposable and correct. Seed only the intended local fixture. Generate an application key only for a newly created local environment that needs one; casually replacing an existing key can make encrypted data unreadable.
Passing tests with SQLite and synchronous queues proves different behavior from MySQL and a running Redis worker. Keep the fast tests, then execute the service-dependent path explicitly. Our Laravel CI/CD guide explains the CI boundary; our Horizon guide covers asynchronous queue processing.
Keep local data when stopping the environment
For an ordinary pause, use ./vendor/bin/sail stop. Containers and persistent data have different lifecycles. The Docker Compose down reference documents the --volumes option: adding it removes declared named volumes and attached anonymous volumes. It is not a routine fix for a port or dependency problem.
Changing an initialization password in the environment may not change the account already stored in an existing database volume. Diagnose the actual credentials and state. If a reset is needed, first decide whether to export the local data or intentionally replace it with the seed fixture. Write the reset instruction as a destructive operation with a clear expected loss.
Before an image rebuild, record the image/runtime change and keep the data decision separate. A new image and an old volume can produce a different result from a clean installation. Both may be valid tests, but they answer different upgrade questions.
Troubleshoot the failed boundary
| Symptom | Likely boundary | First check |
|---|---|---|
| vendor/bin/sail is missing | Dependencies or working directory | Are you at the application root, with development dependencies installed? |
| Port is already allocated | Host publication | Find the conflicting listener and review the mapped host port. |
| Database connection refused | Service startup or address | Container status, service hostname and internal port. |
| Database access denied | Credentials or retained data | Loaded application configuration and existing database account. |
| Page loads, assets fail | Frontend build or Vite dev connection | Build output and the browser URL/port used for assets. |
| Job is queued but never processed | Worker lifecycle | Queue connection/name and a worker actually consuming it. |
| Permission denied writing files | Mounted-file ownership | Owner and expected runtime user; avoid blanket world-writable permissions. |
Capture the error, service status and relevant log excerpt before changing several variables. If configuration is cached, inspect that separately from container environment. Restarting a service cannot repair every stale application setting, and clearing application configuration cannot change an image's installed extensions.
Use a clean-clone acceptance record
Ask a second developer to start from the committed instructions and fictional seed data. They should identify the exact runtime, load the app, submit one report, observe the worker result and run the required tests. Stop and start the environment, then verify whether the expected local data remains. Record the commands and outcomes rather than only “setup complete”.
The Laravel delivery workbook includes the service/port inventory, data-reset decision and evidence fields. When adding an admin panel, use the Filament workflow guide; for socket updates, use the Reverb guide and document which names are reachable by the browser, application and socket server.
Production still needs its own reviewed image/process management, secret handling, backups, TLS and release procedure. A reproducible local workflow is useful evidence for that work, not a replacement for it. Our Laravel development service can start from the failing setup step and the smallest workflow that demonstrates the problem.
