Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Local Laravel Dev with Sail and Docker

By Kokil Thapa | Last reviewed: August 2026

Setting up local Laravel dev with Sail and Docker eliminates the most persistent source of bugs in PHP development: environment drift between your laptop and production server. Instead of installing PHP, MySQL, Redis, and Node directly on your host machine, Sail provides a lightweight Docker abstraction that runs everything in isolated containers while keeping your code editable locally. This approach gives you reproducible environments, instant service switching, and zero pollution of your host OS.

How do you set up local Laravel dev with Sail and Docker from scratch?

The initial setup for Laravel developers in Nepal and elsewhere takes under five minutes if you already have Docker Desktop or a compatible container runtime installed. Sail is not a separate virtualization layer; it is a convenience wrapper around standard Docker Compose that ships sensible defaults for Laravel applications.

Prerequisites and version requirements

Before starting, verify these minimum versions on your host machine:

  • Docker Engine: 24.x or higher (Docker Desktop 4.28+ on macOS/Windows, or native Docker CE on Linux)
  • PHP: 8.2 minimum on host only for the initial composer create-project; Sail itself runs PHP 8.4 inside the container regardless of your host version
  • Laravel: 11.x or 12.x (Sail has been included by default since Laravel 8, but 2026 projects should target current majors)
  • WSL2: Required on Windows 10/11 for acceptable filesystem performance; never use Sail with legacy Hyper-V backend

Step-by-step installation

  1. Create a new Laravel project using your host PHP:
    composer create-project laravel/laravel my-app
  2. Navigate into the project directory and install Sail as a dev dependency:
    cd my-app
    composer require laravel/sail --dev
  3. Publish the Sail configuration and choose your services interactively:
    php artisan sail:install
    Select mysql, redis, mailpit, and selenium unless you have specific needs for PostgreSQL, Meilisearch, or Memcached.
  4. Start all containers in detached mode:
    ./vendor/bin/sail up -d
    The first run pulls base images (~1.2 GB total) and builds the application container. Subsequent starts take seconds.
  5. Run migrations and seeders inside the container:
    ./vendor/bin/sail artisan migrate:fresh --seed
  6. Access your application at http://localhost (port 80) or http://localhost:8000 depending on your published configuration.
Host MachineVS Code / IDEProject FilesBrowser :8000./vendor/bin/sailVolume MountDocker Containerslaravel.testPHP 8.4 + NginxmysqlMySQL 8.4redisRedis 7.4mailpitSMTP TestingseleniumBrowser TestsInternal Docker NetworkContainers communicate by service name
Architecture diagram for local Laravel dev with Sail and Docker showing host-to-container communication flow

A critical detail many tutorials omit: the sail binary is just a Bash script that forwards commands to docker compose exec. You can always bypass it and run raw Docker Compose commands when debugging. On a real client project where Sail's alias conflicted with an existing shell function, I simply added alias l='vendor/bin/sail' to the team's .bashrc template and documented both invocation methods.

How does Sail compare to Homestead, Valet, and native installs?

Choosing the right local environment matters for long-term maintainability. Each option has trade-offs that affect onboarding time, parity with production, and resource consumption.

CriteriaLaravel SailLaravel HomesteadLaravel ValetNative PHP Install
Setup Time5 minutes30–60 minutes10 minutesVariable (OS-dependent)
Production ParityHigh (containerized)Medium (Vagrant VM)Low (macOS-only, no DB)Very Low
Cross-PlatformLinux, macOS, Windows (WSL2)Linux, macOS, WindowsmacOS only (Linux beta)All platforms
Resource UsageModerate (~1.5 GB RAM)Heavy (~3–4 GB RAM)Minimal (~100 MB)Minimal
Service IsolationFull per-projectShared across sitesNone (global PHP)None
Database ManagementBundled containersBundled in VMExternal requiredManual install
CI/CD ReusabilityDirect (same docker-compose)Not reusableNot reusableNot reusable
Best ForTeams, production-matchingLegacy multi-version PHPSolo macOS quick prototypingSimple scripts, learning

In practice, Sail wins for any project that will eventually deploy to a containerized or Linux production environment. Homestead still has niche value when you need PHP 7.4 alongside 8.4 for legacy maintenance, but new projects in 2026 should default to Sail. Valet remains excellent for rapid WordPress or static site work on macOS, but its lack of bundled databases makes it unsuitable for serious Laravel application development.

For teams hiring web developers in Nepal, Sail dramatically reduces onboarding friction. A new developer clones the repository, runs sail up, and has an identical environment within minutes — no wiki pages documenting Homebrew taps or PPA repositories that break six months later.

How do you customize Sail services and optimize performance?

The default docker-compose.yml generated by sail:install is a starting point, not a final configuration. Real projects require adjustments for database tuning, custom PHP extensions, Xdebug integration, and filesystem performance.

Adding custom PHP extensions

Sail's base image includes common extensions, but specialized packages like bcmath, gd, or intl may be missing. Create a custom Dockerfile at docker/8.4/Dockerfile:

FROM ubuntu:24.04

LABEL maintainer="Taylor Otwell"

ARG WWWGROUP
ARG NODE_VERSION=22
ARG POSTGRES_VERSION=17

WORKDIR /var/www/html

ENV DEBIAN_FRONTEND=noninteractive
ENV TZ=UTC
ENV SUPERVISOR_PHP_COMMAND="/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80"
ENV SUPERVISOR_PHP_USER="sail"

RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

RUN apt-get update \
    && mkdir -p /etc/apt/keyrings \
    && apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev dnsutils librsvg2-bin fswatch ffmpeg nano \
    && apt-get install -y php8.4-cli php8.4-dev php8.4-pgsql php8.4-sqlite3 php8.4-gd php8.4-curl php8.4-imap php8.4-mysql php8.4-mbstring php8.4-xml php8.4-zip php8.4-bcmath php8.4-soap php8.4-intl php8.4-readline php8.4-ldap php8.4-msgpack php8.4-igbinary php8.4-redis php8.4-swoole php8.4-memcached php8.4-pcov php8.4-xdebug \
    && pecl install ds \
    && echo "extension=ds.so" > /etc/php/8.4/cli/conf.d/20-ds.ini \
    && apt-get -y autoremove \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

Then update your docker-compose.yml to build from this custom file instead of pulling the upstream image:

services:
    laravel.test:
        build:
            context: ./docker/8.4
            dockerfile: Dockerfile
            args:
                WWWGROUP: '${WWWGROUP}'
        # ... rest of service config unchanged

Xdebug configuration for step debugging

Enable Xdebug by setting SAIL_XDEBUG_MODE=develop,debug in your .env file. For VS Code, add this launch configuration to .vscode/launch.json:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Listen for Xdebug",
            "type": "php",
            "request": "launch",
            "port": 9003,
            "pathMappings": {
                "/var/www/html": "${workspaceFolder}"
            }
        }
    ]
}

A common mistake on Linux hosts: forgetting to set SAIL_XDEBUG_CONFIG="client_host=host.docker.internal". Without this, Xdebug cannot connect back to your IDE because the container's localhost differs from the host's localhost. On WSL2, you may also need to add a firewall rule allowing port 9003.

Filesystem performance on macOS and Windows

Docker's bind mounts are notoriously slow on non-Linux hosts due to filesystem translation overhead. Mitigate this with VirtioFS (Docker Desktop 4.28+) or Mutagen for aggressive caching:

# In docker-compose.yml, replace standard volume mount:
volumes:
    - '.:/var/www/html'

# With VirtioFS-enabled mount (Docker Desktop settings must enable VirtioFS first):
volumes:
    - type: virtiofs
      source: .
      target: /var/www/html

In my experience working on production Laravel applications with large vendor directories and node_modules, VirtioFS reduced page load times from ~2.5 seconds to ~400ms on macOS. The improvement is less dramatic on Linux hosts where native bind mounts already perform well.

Volume Mount Performance ComparisonPage Load Time (Lower is Better)2500msLegacy Bind(macOS gRPC)1200msMutagen(Sync Cache)400msVirtioFS(macOS/Win)350msNative Bind(Linux Host)Time
Relative page load times across Docker volume mount strategies for local Laravel dev with Sail and Docker

What are the common troubleshooting issues with Sail and how do you fix them?

Even with mature tooling, Sail introduces failure modes that don't exist with native installs. These are the problems I encounter repeatedly when helping teams adopt containerized development.

Port conflicts with existing services

If port 80, 3306, or 6379 is already bound on your host, Sail fails silently or throws cryptic binding errors. Check active ports before starting:

# Find what's using port 80
sudo lsof -i :80

# Or change Sail's mapped ports in .env
APP_PORT=8000
FORWARD_DB_PORT=33060
FORWARD_REDIS_PORT=63790

Never modify ports directly in docker-compose.yml — they'll be overwritten next time you run sail:install. The .env variables are the supported override mechanism.

Permission errors on storage and bootstrap/cache

Container processes run as user sail (UID 1337 by default), which often mismatches your host UID. Fix ownership after cloning:

./vendor/bin/sail root chown -R sail:sail /var/www/html/storage /var/www/html/bootstrap/cache

For permanent resolution, set WWWGROUP and WWWUSER in your .env to match your host IDs. Retrieve them with id -u and id -g, then rebuild: ./vendor/bin/sail build --no-cache.

Database connection refused during migrations

The MySQL container takes 10–20 seconds to initialize on first boot. If sail artisan migrate fails immediately, wait and retry. For CI pipelines or scripted setups, add a health check wait loop:

#!/bin/bash
until ./vendor/bin/sail mysqladmin ping -h mysql --silent; do
    echo "Waiting for MySQL..."
    sleep 2
done
./vendor/bin/sail artisan migrate:fresh --seed

Stale containers after branch switching

When switching between branches with different service configurations (e.g., one branch uses PostgreSQL, another uses MySQL), orphaned containers persist. Always tear down completely:

./vendor/bin/sail down -v   # Removes volumes too!
./vendor/bin/sail up -d

The -v flag destroys database contents. Omit it if you want to preserve data, but understand that schema mismatches between branches will cause migration failures.

Sail Not Working?Can containers start?NoYesPort conflict error?App loads but errors?Change APP_PORTin .env fileDB connection refused?Wait 20s for MySQL initor add health checkPermission denied?chown sail:sail storage/
Troubleshooting decision tree for resolving common issues in local Laravel dev with Sail and Docker

How do you integrate Sail with GitLab CI and production deployments?

One of Sail's strongest advantages over Valet or native installs is that your local docker-compose.yml can serve as the foundation for CI pipelines. The same container definitions that power your development environment can run tests, build assets, and validate migrations before deployment.

For teams using Deployer 7 or similar tools (as I do for multiple sister sites sharing infrastructure), the pattern is straightforward:

# .gitlab-ci.yml excerpt
test:
  stage: test
  image: docker:24.0
  services:
    - docker:24.0-dind
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
  before_script:
    - docker compose -f docker-compose.yml up -d --wait
    - docker compose exec -T laravel.test composer install --prefer-dist --no-interaction
  script:
    - docker compose exec -T laravel.test php artisan test --parallel
    - docker compose exec -T laravel.test php artisan migrate:fresh --force
  after_script:
    - docker compose down -v

This approach catches environment-specific bugs that unit tests alone miss: missing PHP extensions, misconfigured queue drivers, or database driver incompatibilities. When building Laravel APIs that depend on precise Redis or PostgreSQL configurations, CI parity with local development prevents an entire category of "works on my machine" failures.

For asset compilation, commit built artifacts rather than running Node inside production containers. Your CI pipeline should execute sail npm run build, copy the output to a release artifact, and let Deployer handle distribution. Production servers shouldn't need Node.js installed — this reduces attack surface and simplifies server provisioning.

Getting started with local Laravel dev with Sail and Docker

Adopting local Laravel dev with Sail and Docker pays dividends immediately through faster onboarding, fewer environment-related bugs, and closer parity with production infrastructure. Start with the default configuration, measure your actual pain points (usually filesystem performance or Xdebug connectivity), then customize incrementally. Avoid over-engineering your Docker setup before you've shipped real features.

If you're evaluating whether to migrate an existing team to Sail, or need help optimizing a containerized Laravel workflow that isn't performing, reach out to discuss your specific setup. I regularly help teams transition from native installs or Homestead to Sail-based workflows that actually improve developer velocity rather than adding operational overhead.

Frequently Asked Questions

Laravel Sail is a lightweight Docker wrapper providing a consistent PHP 8.4, MySQL 8.4, and Redis 7.x environment without installing software locally. Unlike Valet or XAMPP, Sail guarantees your local stack matches production exactly, eliminating version mismatch bugs during deployment.

Run composer require laravel/sail --dev then php artisan sail:install. Select services like MySQL, Redis, or Mailpit when prompted. This publishes the docker-compose.yml file configured for your specific application requirements and PHP version.

No. Sail requires WSL2 on Windows because native Docker Desktop filesystem performance is too slow for PHP applications. Install WSL2 with Ubuntu 24.04 first, then run all Sail commands inside the Linux terminal for acceptable disk I/O speeds.

Slow performance usually stems from filesystem latency between host and container. On macOS, enable VirtioFS in Docker Desktop settings instead of gRPC FUSE. On Windows, ensure you are running commands inside WSL2, not PowerShell. Binding volumes to /var/www/html rather than nested directories also significantly improves read/write throughput for Artisan and Composer operations.

Use host 127.0.0.1 and port 3306 by default, unless you changed mappings in docker-compose.yml. The username is sail and password is password as defined in your .env file. Never use localhost as the hostname because some database clients interpret this as a Unix socket connection rather than TCP/IP, causing immediate connection refusals.

Yes, but you must customize ports in each project's docker-compose.yml file. Change the forwarded ports for MySQL, Redis, and the app service before running sail up. Alternatively, set APP_PORT=8001 in your .env file and update the compose configuration accordingly. I manage several legal-tech portals locally using distinct port mappings to avoid collisions between active development environments.

Modify the build.dockerfile or image tag in docker-compose.yml to target php:8.4-cli or node:22-alpine. Run sail down -v followed by sail build --no-cache to rebuild containers with updated base images. Always test thoroughly after upgrading, as extension compatibility and deprecated function warnings frequently surface during major version transitions in production-grade applications.

Permission issues occur when node_modules was created by root instead of the sail user. Delete the directory and run sail npm install again. If problems persist, add USER sail to your Dockerfile before dependency installation steps. In my experience building eCommerce platforms, ensuring correct ownership early prevents cryptic build failures during CI/CD pipeline execution later.

Enable Xdebug by setting SAIL_XDEBUG_MODE=debug and SAIL_XDEBUG_CONFIG=client_host=host.docker.internal in your .env file. Configure VS Code launch.json with pathMappings pointing /var/www/html to your local project root. Restart containers with sail down && sail up -d. Breakpoints now trigger correctly for both HTTP requests and Artisan commands executed via sail artisan.

No. Sail is strictly a local development tool optimized for convenience, not security or performance. Production environments require dedicated orchestration like Kubernetes, Docker Swarm, or traditional PHP-FPM with Nginx. I deploy client sites using Deployer 7 to Ubuntu servers with PHP-FPM 8.4, keeping Sail exclusively for local feature development and testing workflows.

A standard Sail stack with PHP 8.4, MySQL 8.4, and Redis 7.x typically uses 1.5GB to 2.5GB RAM at idle. Allocate at least 4GB to Docker Desktop to prevent swapping during Composer installs or database migrations. On resource-constrained machines, disable unused services in docker-compose.yml to reclaim memory for IDE and browser processes.

Sail automatically creates named Docker volumes for MySQL and Redis data defined in docker-compose.yml. Running sail down preserves these volumes; only sail down -v destroys them permanently. Always verify volume names before pruning. On real client projects, I have recovered hours of migration work simply because default volume persistence prevented accidental data loss during routine container rebuilds.

Yes, if you use alternative runtimes like OrbStack on macOS or Podman on Linux. OrbStack offers superior performance and lower memory overhead compared to Docker Desktop. Ensure the runtime exposes a compatible Docker socket at /var/run/docker.sock. Sail detects available runtimes automatically, making switching transparent for most development workflows without modifying configuration files.

Execute sail artisan schedule:work in a separate terminal to process cron-defined jobs continuously. For queue workers, run sail artisan queue:work --tries=3. Both commands respect your .env queue driver configuration. During development of booking systems, I keep these processes running alongside sail up to test email notifications and background processing without deploying to staging servers repeatedly.

Environment variables differ significantly; DB_HOST must be mysql not 127.0.0.1 inside containers. File permissions reset after rebuilds, requiring chown -R sail:sail /var/www/html occasionally. Mailtrap configuration changes to Mailpit with different ports. Existing .env files need updating before first sail up. Plan extra time for these adjustments when transitioning established projects to containerized local development environments.

Share this article

Quick Contact Options
Choose how you want to connect me: