
August 14, 2026
11 min read
Table of Contents
By Kokil Thapa | Last reviewed: August 2026
Setting up a consistent Docker Compose for local Laravel dev environment eliminates the "works on my machine" problem that plagues team development and solo projects alike. Instead of wrestling with Homebrew conflicts, mismatched PHP extensions, or global Composer pollution, you define your entire stack—PHP 8.4, Nginx, MySQL 8.4, Redis—in declarative YAML and spin it up with one command. This guide provides a battle-tested configuration I use daily for Laravel development, optimized for Laravel 12.x performance and realistic production parity.
How do you structure Docker Compose for local Laravel dev environment services?
The most common mistake in containerized Laravel setups is cramming everything into a single monolithic container. In practice, separating concerns mirrors how you deploy to production (where I typically use Apache/Nginx + PHP-FPM on Ubuntu servers) and makes debugging significantly easier. For a functional Docker Compose for local Laravel dev environment, you need four distinct services communicating over an internal Docker network.
This separation gives you independent lifecycle control. Need to test a different Redis version? Swap only the cache service image tag. Database migration failing? Inspect the db logs without wading through PHP output. On legal-tech portals I've built like Court Marriage In Nepal, this architecture let me replicate staging issues locally within minutes because the container boundaries matched the server process boundaries exactly.
Essential service definitions
Your docker-compose.yml should declare explicit dependencies and health checks. Without health checks, Laravel's container often starts before MySQL accepts connections, causing cryptic "Connection refused" errors during php artisan migrate.
<!-- docker-compose.yml -->
services:
app:
build:
context: .
dockerfile: docker/php/Dockerfile
volumes:
- .:/var/www/html
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
environment:
- DB_HOST=db
- REDIS_HOST=redis
networks:
- laravel-net
web:
image: nginx:1.27-alpine
ports:
- "8080:80"
volumes:
- .:/var/www/html
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
depends_on:
- app
networks:
- laravel-net
db:
image: mysql:8.4-lts
environment:
MYSQL_DATABASE: laravel
MYSQL_ROOT_PASSWORD: secret
volumes:
- db-data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
timeout: 5s
retries: 10
networks:
- laravel-net
redis:
image: redis:7.4-alpine
networks:
- laravel-net
volumes:
db-data:
networks:
laravel-net:
driver: bridge Note the named volume db-data. Bind-mounting the host filesystem for MySQL causes permission nightmares on Linux and catastrophic performance degradation on macOS/Windows due to filesystem translation layers. Named volumes keep database I/O inside Docker's optimized storage driver.
What PHP 8.4 Dockerfile configuration does Laravel 12 actually require?
Laravel 12.x requires PHP 8.2 minimum, but PHP 8.4 is the current stable release in 2026 and offers meaningful performance improvements for CPU-bound tasks like serialization and validation. Your Dockerfile must install the exact extensions Laravel expects—missing bcmath or intl will cause silent failures in currency formatting or locale handling, problems I've debugged repeatedly on eCommerce projects like Nepal Gift Card.
# docker/php/Dockerfile
FROM php:8.4-fpm-bookworm
RUN apt-get update && apt-get install -y \
git curl zip unzip libpng-dev libjpeg62-turbo-dev \
libfreetype6-dev libwebp-dev libzip-dev libicu-dev \
&& rm -rf /var/lib/apt/lists/*
RUN docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp \
&& docker-php-ext-install -j$(nproc) \
pdo_mysql \
mbstring \
bcmath \
intl \
gd \
zip \
opcache
COPY --from=composer:2.7 /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/html
USER www-data
EXPOSE 9000
CMD ["php-fpm"] The -j$(nproc) flag parallelizes extension compilation, cutting build time roughly in half on multi-core machines. Always pin the Composer version explicitly; pulling composer:latest can introduce breaking changes mid-project. The www-data user directive prevents file permission conflicts when bind-mounted volumes are written by both the container and your host IDE—a recurring pain point on shared development teams.
Nginx configuration for Laravel routing
Laravel's front controller pattern requires every non-file request to route through index.php. A misconfigured try_files directive causes 404s on every route except the homepage. This is the exact Nginx config I use:
# docker/nginx/default.conf
server {
listen 80;
index index.php index.html;
root /var/www/html/public;
charset utf-8;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass app:9000;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_buffer_size 32k;
fastcgi_buffers 16 16k;
}
location ~ /\.(?!well-known).* {
deny all;
}
} The $realpath_root variable resolves symlinks correctly, which matters when you later adopt zero-downtime deployment patterns similar to what I describe in my CI/CD pipeline guide. The buffer size tuning prevents "upstream sent too big header" errors when Laravel returns large JSON responses from API endpoints.
How do you handle file permissions and volume mounts in Laravel Docker containers?
File permission mismatches between host and container are the single most frequent failure mode in Dockerized Laravel development. When your host UID doesn't match the container's www-data UID (typically 33 on Debian-based images), Artisan commands create files owned by root that your IDE can't edit, and vice versa.
- Match UIDs at build time. Add
ARG USER_ID=1000andARG GROUP_ID=1000to your Dockerfile, then runusermod -u $USER_ID www-data && groupmod -g $GROUP_ID www-data. Pass these from your host:docker compose build --build-arg USER_ID=$(id -u) --build-arg GROUP_ID=$(id -g). - Create storage directories before mounting. Run
mkdir -p storage/framework/{cache,sessions,views} bootstrap/cacheon the host before firstdocker compose up. Missing directories cause Laravel to attempt creation as root inside the container. - Use ACLs on Linux hosts. If UID matching isn't feasible, set default ACLs:
setfacl -Rdm u:www-data:rwx storage bootstrap/cache. This grants the container user write access regardless of ownership. - Never mount vendor/ or node_modules/. These directories contain platform-specific binaries. Let Composer and npm install inside the container, or use a separate anonymous volume to shadow the host directory.
On macOS and Windows, Docker Desktop's VirtioFS (macOS) or WSL2 backend (Windows) handles permission translation automatically in 2026, but bind-mount performance remains 3–5× slower than native Linux I/O. For Laravel applications with heavy file operations—like the document workflows in Notary Nepal—I recommend developing inside WSL2 on Windows or using Mutagen for bidirectional sync on macOS rather than direct bind mounts.
How do you configure Xdebug 3 in Docker Compose for local Laravel dev environment?
Xdebug 3 changed configuration fundamentally from version 2. The old xdebug.remote_host directives are ignored silently, leading developers to believe Xdebug is broken when it's simply misconfigured. For Docker Compose for local Laravel dev environment, you need three things: correct INI settings, proper host gateway resolution, and IDE path mapping.
# docker/php/xdebug.ini (mounted into /usr/local/etc/php/conf.d/)
zend_extension=xdebug.so
xdebug.mode=debug
xdebug.client_host=host.docker.internal
xdebug.client_port=9003
xdebug.start_with_request=yes
xdebug.log=/tmp/xdebug.log On Linux hosts, host.docker.internal doesn't resolve by default. Add this to your app service in docker-compose.yml:
extra_hosts:
- "host.docker.internal:host-gateway" Enable Xdebug conditionally via environment variable so you're not paying the 20–30% performance penalty during normal development. Override the INI at runtime: docker compose exec app php -d xdebug.mode=off artisan tinker. For browser-triggered debugging, use the Xdebug Helper extension to set the XDEBUG_TRIGGER cookie only when needed.
Docker vs Sail vs Herd: Which local Laravel environment should you choose in 2026?
Laravel Sail simplified Docker adoption enormously, but it's not always the right tool. Understanding the trade-offs prevents costly rework when your project outgrows Sail's assumptions. Here's how they compare for real-world Docker Compose for local Laravel dev environment scenarios:
| Criteria | Custom Docker Compose | Laravel Sail | Laravel Herd |
|---|---|---|---|
| Setup Time | 2–4 hours initial | <10 minutes | <5 minutes |
| Production Parity | Exact (you control everything) | Good (opinionated defaults) | Poor (native binaries, not containers) |
| Multi-PHP Versions | Trivial (separate Dockerfiles) | Possible but awkward | Built-in toggle |
| Team Consistency | Guaranteed (committed YAML) | Guaranteed (published stubs) | Varies per developer machine |
| CI Integration | Direct reuse of compose file | Sail supports testing | Not applicable |
| Custom Services | Add anything to YAML | Publish & modify stubs | Limited to bundled services |
| Performance (macOS) | Moderate (VirtioFS) | Moderate (same backend) | Fastest (no virtualization) |
| Learning Curve | Steep (Docker fundamentals) | Gentle (Laravel-centric) | Minimal (GUI-driven) |
Choose custom Docker Compose when your production infrastructure uses containers, you need services Sail doesn't bundle (Elasticsearch, MinIO, custom queues), or you're building multi-tenant SaaS applications where environment isolation matters. Choose Sail for greenfield Laravel projects with standard stacks and teams new to containerization. Choose Herd for quick prototyping, WordPress work alongside Laravel, or when you're solo and value speed over parity.
In my experience maintaining multiple client projects simultaneously—including legal-tech platforms with strict compliance requirements and eCommerce sites needing Elasticsearch—custom Docker Compose pays its setup cost within weeks. The ability to version-control the exact runtime environment means onboarding a new developer takes cloning a repo and running docker compose up, not following a wiki page that hasn't been updated since PHP 8.1.
Optimizing Docker Compose for local Laravel dev environment performance
Container overhead is real, but most slowness comes from misconfiguration rather than inherent Docker limitations. Apply these optimizations in order of impact:
- Enable OPcache with JIT. PHP 8.4's JIT compiler provides 10–25% throughput improvement for compute-heavy Laravel middleware. Add
opcache.jit=1255andopcache.jit_buffer_size=256Mto your php.ini. Verify withphp -r "print_r(opcache_get_status());". - Use tmpfs for cache and sessions. Add
tmpfs: [/tmp, /var/www/html/storage/framework/cache]to your app service. RAM-backed filesystems eliminate disk I/O for ephemeral data that doesn't need persistence across restarts. - Exclude vendor/ from bind mounts on macOS. Use an anonymous volume overlay: add
- vendor-data:/var/www/html/vendorto volumes and declarevendor-data:under top-level volumes. Composer installs happen at native container speed instead of crossing the VirtioFS boundary. - Set COMPOSER_HOME to tmpfs. Prevents repeated filesystem traversal for package metadata:
environment: [COMPOSER_HOME=/tmp/composer]. - Use BuildKit cache mounts. In your Dockerfile, replace
RUN composer installwithRUN --mount=type=cache,target=/root/.composer composer install. Subsequent builds reuse downloaded packages even after Dockerfile changes invalidate the layer cache.
For Laravel applications serving significant traffic—even locally during load testing—these optimizations routinely cut response times by 40–60%. On a recent e-commerce build handling complex cart calculations, enabling JIT and tmpfs brought average API response from 180ms down to 95ms inside the same container.
Next Steps for Your Laravel Docker Workflow
A properly configured Docker Compose for local Laravel dev environment transforms development from environment management into actual feature work. Start with the four-service architecture above, validate permissions before writing application code, and enable Xdebug only when actively debugging. As your project matures, extend the compose file with Mailpit for email testing, Meilisearch for full-text search, or MinIO for S3-compatible object storage—all without touching your host machine.
If you're setting up Docker for a team, need help migrating an existing Laravel application to containers, or want to align your local environment with production infrastructure, reach out directly. I've helped Nepal-based agencies and international clients standardize their Laravel development workflows, reducing onboarding time from days to hours and eliminating environment-related bug reports entirely.

