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.

Addresses for a hypothetical local port mapping
CallerDestinationWhy
Laravel containermysql:3306The service name resolves inside the Compose network.
Host database client127.0.0.1:3307Example host mapping 3307 forwards to container port 3306.
Laravel containerredis:6379Use the configured Redis service and its internal port.
Browser on the hosthttp://localhost:8081Example 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

Common Sail failures and focused checks
SymptomLikely boundaryFirst check
vendor/bin/sail is missingDependencies or working directoryAre you at the application root, with development dependencies installed?
Port is already allocatedHost publicationFind the conflicting listener and review the mapped host port.
Database connection refusedService startup or addressContainer status, service hostname and internal port.
Database access deniedCredentials or retained dataLoaded application configuration and existing database account.
Page loads, assets failFrontend build or Vite dev connectionBuild output and the browser URL/port used for assets.
Job is queued but never processedWorker lifecycleQueue connection/name and a worker actually consuming it.
Permission denied writing filesMounted-file ownershipOwner 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.