Docker Compose for Local Laravel Dev Environment

Khimananda Oli 9 min read CI/CD and Automation
Docker Compose for Local Laravel Dev Environment

By Khimananda Oli | Last reviewed: August 2026

Setting up a reliable Docker Compose for local Laravel dev environment eliminates the "works on my machine" problem that still plagues PHP teams in 2026. Instead of wrestling with conflicting system PHP versions or Homebrew updates, you define your exact stack—PHP 8.4, Nginx, MariaDB, and Redis—in code that travels with your repository. This guide walks through a production-mirroring configuration that uses bind mounts for instant file reflection and includes practical optimizations for macOS and Linux performance.

How do you structure Docker Compose for local Laravel dev environment services?

The most common mistake I see when teams adopt containers for PHP is treating the application as a single monolithic container. For a functional Docker Compose for local Laravel dev environment, you must separate concerns just as you would in production. This separation allows you to restart individual components without losing state and mirrors the architecture you will eventually deploy to Kubernetes or EC2. If you are new to containerization fundamentals, review Docker for beginners: containerize a Laravel app from scratch before proceeding.

Local Laravel Container TopologyNginx (Web)Port 8080 → 80Static Assets + ProxyPHP-FPM (App)PHP 8.4 + ExtensionsBind Mount: ./srcMariaDB (DB)Port 3306Named VolumeRedisCache + QueuePort 6379Custom Bridge Network: laravel-local-netDNS Resolution by Service Name • Isolated from HostHost Bind Mount./src:/var/www/html (Instant Reflection)Named Volumesdb_data • redis_data (Persist State)
Service topology for Docker Compose for local Laravel dev environment showing network isolation and volume strategy

Defining the service boundaries

Your docker-compose.yml should contain exactly four core services for local development. The web service runs Nginx solely as a reverse proxy and static file server; it never executes PHP. The app service runs PHP-FPM with your application code mounted at /var/www/html. The database service runs MariaDB (or PostgreSQL if you prefer PostgreSQL administration essentials) with a named volume for data persistence. The cache service runs Redis for both application caching and queue drivers.

Network and volume configuration

Always define a custom bridge network rather than relying on the default bridge. Custom networks provide automatic DNS resolution between containers, meaning your PHP application can connect to database instead of managing IP addresses. For volumes, distinguish between bind mounts and named volumes explicitly:

  • Bind mounts (./src:/var/www/html) for application code — changes reflect instantly without rebuilds
  • Named volumes (db_data:/var/lib/mysql) for database files — survives container recreation
  • Anonymous volumes — avoid these entirely in development; they create orphaned storage

How do you configure PHP 8.4 and Nginx for Laravel containers?

The official PHP images are a starting point, not a destination. For a performant Docker Compose for local Laravel dev environment, you need a custom Dockerfile that installs required extensions, configures OPcache correctly for development, and sets appropriate user permissions. In 2026, PHP 8.4 is the stable target for new Laravel projects, bringing property hooks and improved type safety that your container should support natively.

Building the PHP-FPM image

Create a docker/php/Dockerfile that extends php:8.4-fpm-bookworm. Install system dependencies first (libpng, libjpeg, libonig, libzip), then install PHP extensions via docker-php-ext-install. Always include bcmath, gd, mbstring, pdo_mysql, zip, and opcache. For local development, add Xdebug conditionally using a build argument so CI builds can skip it:

FROM php:8.4-fpm-bookworm

ARG INSTALL_XDEBUG=true

RUN apt-get update && apt-get install -y \
    libpng-dev libjpeg62-turbo-dev libfreetype6-dev \
    libonig-dev libzip-dev unzip git curl \
    && docker-php-ext-configure gd --with-freetype --with-jpeg \
    && docker-php-ext-install -j$(nproc) \
        bcmath gd mbstring pdo_mysql zip opcache \
    && if [ "$INSTALL_XDEBUG" = "true" ]; then \
        pecl install xdebug && docker-php-ext-enable xdebug; \
    fi \
    && apt-get clean && rm -rf /var/lib/apt/lists/*

COPY ./docker/php/custom.ini /usr/local/etc/php/conf.d/custom.ini
WORKDIR /var/www/html
USER www-data

Nginx virtual host configuration

Your Nginx config must pass PHP requests to the FPM socket or TCP port while serving static assets directly. A common failure mode is misconfigured fastcgi_param SCRIPT_FILENAME; always use $realpath_root$fastcgi_script_name to resolve symlinks correctly during local development. Set client_max_body_size to match your upload limits and disable access logs for static assets to reduce I/O noise.

OPcache settings for development

Production OPcache settings destroy developer productivity. In your custom.ini, set opcache.validate_timestamps=1 and opcache.revalidate_freq=0 so PHP checks for file changes on every request. This adds ~5ms per request but eliminates the "why isn't my change showing?" debugging sessions. Keep opcache.enable_cli=0 unless you run long-lived workers.

How do you handle database persistence and seeding in containers?

Data loss is the primary complaint from developers adopting containerized databases. Your Docker Compose for local Laravel dev environment must guarantee that running docker compose down does not wipe your test data, while still allowing a clean reset when needed. This requires understanding the difference between stopping, removing, and pruning containers versus volumes.

Database Volume Lifecycledocker compose upCreates db_data if missingContainer RunningWrites to /var/lib/mysqldocker compose stopVolume PERSISTSdocker compose upReuses existing datadocker compose down -vDELETES db_dataFresh StartRun migrations + seedersRule: Never use -v flag unless intentionally resetting. Use docker compose exec app php artisan migrate:fresh --seed for DB resets.
Understanding volume persistence prevents accidental data loss in Docker Compose for local Laravel dev environment

Automating initialization

MariaDB and PostgreSQL images execute SQL files placed in /docker-entrypoint-initdb.d/ only on first volume creation. Place a create-database.sql script there to set up your database name, user, and grants automatically. For Laravel-specific setup, create an entrypoint script in your PHP container that waits for the database, runs migrations, and seeds data—but only if a FRESH_INSTALL environment variable is set. This prevents re-running destructive commands on every container start.

Handling credentials securely

Never commit production credentials to your repository. Create a .env.docker file specifically for container defaults and add it to .gitignore. Reference these values in your docker-compose.yml using the env_file directive. For teams working across Nepal and global offices, this also prevents timezone and locale mismatches; set TZ=Asia/Kathmandu in your compose file to ensure timestamps align with your business context.

What are the performance trade-offs between bind mounts and named volumes?

Performance is where many Docker Compose for local Laravel dev environment setups fail, particularly on macOS and Windows. Understanding the I/O characteristics of different mount types determines whether your containerized workflow feels native or painfully slow.

Mount TypeRead SpeedWrite SpeedPersistenceBest For
Bind Mount (native Linux)NativeNativeHost filesystemLinux hosts, CI runners
Bind Mount (macOS VirtioFS)~90% native~85% nativeHost filesystemmacOS development (2026+)
Bind Mount (macOS gRPC FUSE)~60% native~40% nativeHost filesystemLegacy macOS Docker Desktop
Named VolumeNativeNativeDocker managedDatabases, caches, build artifacts
tmpfsRAM speedRAM speedNone (ephemeral)Test suites, temp compilation

Optimizing macOS file sharing

If you develop on macOS, enable VirtioFS in Docker Desktop settings immediately. The older gRPC FUSE layer causes 3–5x slowdowns on Laravel's typical read-heavy workload (autoloading thousands of classes per request). With VirtioFS enabled in 2026, the gap between native and containerized development has narrowed to negligible levels for most workflows. For teams still experiencing slowness, consider moving vendor/ and node_modules/ to named volumes while keeping application code on bind mounts—this hybrid approach trades some convenience for significant I/O gains.

When to use tmpfs for testing

Laravel test suites generate massive temporary file churn. Mount /tmp and your framework's cache directory as tmpfs in your test runner service definition. This keeps ephemeral writes out of the bind mount entirely and can cut test execution time by 30–40% on large projects. Remember that tmpfs contents vanish when the container stops, which is exactly the behavior you want for test artifacts.

How do you integrate Xdebug and Artisan commands efficiently?

A containerized workflow should not make debugging or CLI tasks harder. Your Docker Compose for local Laravel dev environment must support step-through debugging and convenient Artisan access without requiring developers to memorize lengthy docker compose exec invocations.

Xdebug 3 Connection SequenceIDE (VS Code)PHP-FPM ContainerBrowser / HTTPHTTP RequestXdebug connects to host.docker.internal:9003Breakpoint hit responseStep / Continue commandHTTP Response returnedxdebug.client_host=host.docker.internal • xdebug.mode=debug • xdebug.start_with_request=trigger
Xdebug 3 connection sequence for Docker Compose for local Laravel dev environment using trigger-based activation

Configuring Xdebug 3 correctly

Xdebug 3 changed the connection model fundamentally. Set xdebug.client_host=host.docker.internal (works on Docker Desktop and Colima) rather than hardcoded IPs. Use xdebug.start_with_request=trigger instead of yes to avoid the massive performance penalty of attempting debug connections on every request. Enable debugging only when needed via browser extension or XDEBUG_TRIGGER query parameter. Map your local source path to /var/www/html in your IDE's launch configuration—this path mapping is where most developers get stuck.

Creating Artisan shell aliases

Typing docker compose exec app php artisan dozens of times daily destroys flow. Add a function to your team's shared .bashrc or project-level Makefile:

# Add to Makefile
artisan:
	docker compose exec app php artisan $(ARGS)

migrate:
	docker compose exec app php artisan migrate:fresh --seed

test:
	docker compose exec app php artisan test --parallel

This reduces cognitive load and makes the containerized workflow feel identical to native development. For teams transitioning from Valet or native PHP, this familiarity accelerates adoption significantly. See local Laravel dev with Sail and Docker if you prefer Laravel's official abstraction layer, though direct Compose configuration offers more control for complex stacks.

Queue worker handling

Don't forget background jobs. Add a dedicated worker service in your compose file that runs php artisan queue:work --tries=3 --timeout=60. Without this, queued emails, notifications, and jobs silently accumulate during local development, causing confusion when features appear broken. Set restart: unless-stopped so the worker auto-recovers after crashes, mirroring supervisor behavior in production.

Streamline Your Local Laravel Workflow

A well-configured Docker Compose for local Laravel dev environment pays dividends across every sprint: consistent PHP versions, isolated dependencies, and production-parity architecture that catches deployment issues before they reach staging. Start with the four-service pattern outlined here, optimize your bind mounts for your host OS, and integrate Xdebug and Artisan aliases from day one. If your team needs help designing a containerized development workflow that scales from solo developers to distributed teams across Nepal and beyond, reach out to discuss your infrastructure requirements.

Frequently Asked Questions

Use the official php:8.4-fpm-alpine image for 2026. It includes necessary extensions like pdo_mysql and bcmath while keeping container size under 150MB, ensuring fast rebuilds during local development cycles without unnecessary production dependencies or bloated layers slowing down composer install commands.

Define a named volume in your docker-compose.yml services section mapping to /var/lib/mysql. This ensures database records survive container destruction and recreation, preventing data loss during routine debugging sessions or when updating PHP versions in your local Laravel development environment configuration files.

Sail abstracts configuration, limiting customization for complex microservices. Native Docker Compose offers full control over networking, multi-stage builds, and non-standard services like Redis Cluster or Meilisearch, making it superior for teams needing precise environment parity with staging infrastructure beyond standard LAMP stack requirements.

Run chown -R www-data:www-data storage bootstrap/cache inside the app container via docker compose exec. Alternatively, set the WWWUSER and WWWGROUP build arguments in your Dockerfile to match your host UID/GID, preventing root-owned files from blocking local file writes during development.

Yes, filesystem I/O overhead adds latency. Mitigate this by enabling VirtioFS on macOS or WSL2 on Windows. For 2026 setups, bind mounting only necessary directories and using tmpfs for cache paths reduces command execution time significantly compared to default osxfs legacy volume performance.

Install the xdebug extension and set XDEBUG_MODE=debug plus XDEBUG_CLIENT_HOST=host.docker.internal in your docker-compose.yml environment variables. Map port 9003 to your IDE listener. Ensure pathMappings in VS Code or PhpStorm match container paths to enable breakpoint hitting during local request debugging.

Yes, assign unique project names using the COMPOSE_PROJECT_NAME variable or -p flag. Configure distinct external ports for Nginx and database services to avoid binding conflicts. Isolated networks prevent cross-project interference while sharing system resources efficiently on your local development machine.

Create a shared .env file referenced by env_file in each service definition. Never commit secrets; use Docker secrets or pass specific variables via environment keys. This centralizes configuration management for app, queue workers, and schedulers while maintaining security boundaries in your local Laravel setup.

Missing PHP extensions or lack of caching slows dependency resolution. Enable the Composer cache directory as a named volume and ensure unzip and git are installed in the image. Using parallel install plugins like hirak/prestissimo further accelerates package fetching within the containerized build context.

Configure an Nginx location block proxying /vite requests to http://node:5173. Set HMR_HOST to localhost and HMR_PORT to 5173 in vite.config.js. This allows hot module replacement to function correctly through the reverse proxy without exposing the Node container port directly to the host.

No, companies over 250 employees or $10M revenue require paid subscriptions as of 2026. Alternatives include OrbStack for macOS or Rancher Desktop for Linux/Windows, offering compatible Docker Compose functionality without licensing fees for enterprise teams building Laravel applications locally.

Add an entrypoint script executing php artisan migrate:fresh --seed before starting PHP-FPM. Guard this with an environment flag like RUN_MIGRATIONS=true to prevent accidental data wipes. This automates database provisioning for new developers cloning the repository and spinning up fresh local environments.

Health checks often fail due to missing curl packages or incorrect endpoints. Install curl in the Dockerfile and verify the check targets /up or a dedicated health route returning 200. Adjust interval and timeout values in docker-compose.yml to accommodate cold start delays during initial container boot.

Use Mailpit or Mailhog as a local SMTP sink. Configure MAIL_HOST to the service name and MAIL_PORT to 1025 in your Laravel .env. Access the web UI on the mapped port to inspect captured emails without risking accidental delivery to real addresses during testing.

Use bind mounts for active development to reflect file changes instantly. Reserve named volumes for dependencies and generated artifacts. Bind mounts provide immediate feedback loops essential for TDD workflows, whereas volumes offer better isolation but require manual synchronization steps unsuitable for rapid iteration.