
September 09, 2026
12 min read
By Kokil Thapa | Last reviewed: September 2026
A Docker Compose multi-container setup for local development replaces the old pattern of installing PHP, MySQL, and Redis directly on your laptop. One YAML file defines every service, network, and volume your app needs. Run docker compose up and the full stack starts in seconds. On real client projects I maintain with Laravel development workflows, this approach has saved hours of "works on my machine" debugging. The guide below walks through a production-grade stack you can copy today.
docker-compose.yml file to define app, database, cache, and web server containers on a shared network. Run docker compose up -d to start the full stack with one command.What Is a Docker Compose Multi-Container Setup for Local Development?
Docker Compose is a tool that reads a YAML manifest and orchestrates multiple containers as one application. Each container runs a single process — PHP-FPM, MySQL, Redis, or Nginx. Compose wires them together with internal DNS, shared volumes, and environment variables.
Think of it as infrastructure-as-code for your laptop. A new developer clones the repo, runs two commands, and gets an identical environment. No manual PHP version switches. No conflicting MySQL ports. No guessing which extensions are installed.
For web application development, this matters because modern stacks rarely run on PHP alone. A typical Laravel 13 project needs PHP 8.3+, MySQL or PostgreSQL, Redis for cache and queues, and sometimes Node.js 26 LTS for Vite 8.x asset builds. Compose handles all of that declaratively.
How Do You Create a docker-compose.yml for Local Development?
Start with a project root directory. Place docker-compose.yml alongside your application source. The file has three top-level keys: services, networks, and volumes.
Step 1: Define the PHP Application Service
The app container runs PHP-FPM. Mount your source code as a bind mount so file changes reflect immediately without rebuilding the image.
services:
app:
build:
context: .
dockerfile: docker/php/Dockerfile
container_name: laravel-app
restart: unless-stopped
working_dir: /var/www/html
volumes:
- ./:/var/www/html
- ./docker/php/local.ini:/usr/local/etc/php/conf.d/local.ini
networks:
- app-network
depends_on:
db:
condition: service_healthy
redis:
condition: service_started Your Dockerfile should target PHP 8.5 (or 8.3 minimum for Laravel 13). Install common extensions: pdo_mysql, redis, gd, zip, intl, and opcache.
FROM php:8.5-fpm
RUN apt-get update && apt-get install -y \
git curl zip unzip libpng-dev libonig-dev libxml2-dev \
&& docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd intl zip \
&& pecl install redis && docker-php-ext-enable redis
COPY --from=composer:2.10 /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/html Step 2: Add the Web Server
Nginx terminates HTTP and forwards PHP requests to the app container via FastCGI. The service name app becomes a DNS hostname on the internal network.
nginx:
image: nginx:1.27-alpine
container_name: laravel-nginx
restart: unless-stopped
ports:
- "8080:80"
volumes:
- ./:/var/www/html
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
networks:
- app-network
depends_on:
- app A minimal Nginx config for Laravel:
server {
listen 80;
root /var/www/html/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass app:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
} Step 3: Configure Database and Cache
MySQL 9.7 (or 8.4 LTS if you prefer wider hosting compatibility) stores application data. Redis 8.10 handles cache, sessions, and queue backends.
db:
image: mysql:9.7
container_name: laravel-db
restart: unless-stopped
environment:
MYSQL_DATABASE: laravel
MYSQL_ROOT_PASSWORD: secret
MYSQL_USER: laravel
MYSQL_PASSWORD: secret
volumes:
- db-data:/var/lib/mysql
networks:
- app-network
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:8.10-alpine
container_name: laravel-redis
restart: unless-stopped
networks:
- app-network
networks:
app-network:
driver: bridge
volumes:
db-data:
driver: local Match your Laravel .env to these service hostnames:
DB_HOST=db
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=laravel
DB_PASSWORD=secret
REDIS_HOST=redis
REDIS_PORT=6379
CACHE_STORE=redis
QUEUE_CONNECTION=redis How Do You Run and Manage Multi-Container Stacks Day to Day?
Once your YAML file is in place, daily workflow revolves around a handful of Compose commands. These replace manual service restarts and port juggling.
- Start the stack:
docker compose up -druns all services in detached mode. - Install dependencies:
docker compose exec app composer install - Run migrations:
docker compose exec app php artisan migrate - Build frontend assets:
docker compose exec app npm install && npm run dev - View logs:
docker compose logs -f app - Stop everything:
docker compose down - Reset database volume:
docker compose down -v(destroys data — use carefully)
Add a Makefile or shell alias if your team runs the same commands repeatedly. On projects I've shipped with Laravel and Livewire booking systems, a simple make up target reduced onboarding from half a day to twenty minutes.
For deeper networking and volume behaviour, see the dedicated guide on Docker networking and volumes. Bind mounts sync code instantly. Named volumes persist database files across container restarts.
Running Artisan, Composer, and Tests Inside Containers
Never install PHP or Composer on your host if the container is your runtime. Every command goes through docker compose exec:
docker compose exec app php artisan queue:work
docker compose exec app php artisan test
docker compose exec app composer require spatie/laravel-permission Interactive shells work the same way: docker compose exec app bash. This guarantees the PHP version and extensions match production.
Docker Compose vs Laravel Sail: Which Should You Choose?
Laravel Sail is Laravel's official Docker wrapper built on Compose. Raw Compose gives you full control. Both run containers — the difference is abstraction level and portability across frameworks.
| Criteria | Raw Docker Compose | Laravel Sail |
|---|---|---|
| Setup effort | Manual YAML + Dockerfiles | composer require laravel/sail + publish |
| Framework lock-in | Works with Symfony, WordPress, custom PHP | Laravel-only |
| Customization | Full control over every layer | Constrained to Sail's service catalogue |
| PHP version | You choose (8.3, 8.4, 8.5) | Sail runtime tags (check current Sail docs) |
| Learning value | Teaches Docker fundamentals | Hides Compose details behind ./vendor/bin/sail |
| CI/CD parity | Same compose file in CI pipelines | Requires Sail binary or extracted compose file |
| Multi-project teams | One pattern for all stacks | Each Laravel repo has its own Sail config |
My recommendation: learn raw Compose first. Sail is convenient for greenfield Laravel 13 projects where you want speed over control. For mixed teams running WordPress, Magento 2.4.x, and Laravel side by side, a shared Compose pattern pays off quickly. Read the full comparison in Laravel Sail vs Docker Compose.
What Are Common Mistakes in Docker Compose Local Development?
After setting up Compose stacks on dozens of production-bound projects, the same problems appear repeatedly. Most are configuration issues, not Docker bugs.
Port Conflicts and Binding Errors
Binding 3306:3306 fails if MySQL already runs on the host. Map to a non-standard host port instead: 3307:3306. Access from host tools via 127.0.0.1:3307. Inside the Compose network, services still connect on port 3306 using the hostname db.
File Permission Problems on Linux
PHP-FPM runs as www-data inside the container. Files created by Artisan (storage/logs, bootstrap/cache) may become root-owned on the host. Fix this in your Dockerfile:
RUN groupadd -g 1000 devgroup && useradd -u 1000 -g devgroup devuser
USER devuser Match UID 1000 to your host user. On Ubuntu dev machines this is usually correct out of the box. For server-side deployment patterns, see Ubuntu server setup and Linux system administration practices.
Environment Variable Drift
Your .env file must use Docker service names as hostnames — not 127.0.0.1. A common mistake is copying a local Valet or XAMPP .env into a Compose project unchanged. The app container cannot reach 127.0.0.1:3306 because that points to itself, not the MySQL container.
Use a .env.example with Compose-ready defaults. Document the difference clearly in your README. Validate JSON config files with a JSON formatter before committing API fixture data.
Missing Health Checks and Race Conditions
Without health checks, the app container starts before MySQL accepts connections. Laravel throws SQLSTATE[HY000] [2002] Connection refused. The fix is depends_on with condition: service_healthy, as shown in the YAML above. Never use sleep 10 in entrypoint scripts — it is unreliable and slows every startup.
Resource Hogging
MySQL and Elasticsearch containers can consume all available RAM on a 8 GB laptop. Set limits in Compose:
db:
deploy:
resources:
limits:
memory: 512M
cpus: "1.0" See limiting Docker container resources for tuning guidance. On budget laptops common in Nepal (Rs 80,000–120,000, ~USD 600–900), resource limits prevent Docker from freezing the host OS.
How Do You Extend a Multi-Container Setup for Real Projects?
Basic stacks cover CRUD apps. Production-bound local environments need mail catching, search engines, queue workers, and sometimes a separate Node container for Vite 8.x builds.
Compose Profiles for Optional Services
Profiles let you define services that only start when requested. A mail catcher avoids sending real emails during development:
mailpit:
image: axllent/mailpit:latest
profiles: ["mail"]
ports:
- "8025:8025"
- "1025:1025"
networks:
- app-network Start with: docker compose --profile mail up -d. Full details in Docker Compose profiles and overrides.
Queue Workers as Separate Services
Run a dedicated worker container instead of remembering to start queue:work manually:
queue:
build:
context: .
dockerfile: docker/php/Dockerfile
command: php artisan queue:work --sleep=3 --tries=3
volumes:
- ./:/var/www/html
networks:
- app-network
depends_on:
- app
- redis This mirrors how I structure API development projects that process webhooks and payment callbacks asynchronously.
Adding Node.js for Frontend Builds
If your host does not have Node.js 26 LTS installed, add a Node service or use a multi-stage approach. A dedicated Node container keeps the PHP image lean:
node:
image: node:26-alpine
profiles: ["frontend"]
working_dir: /var/www/html
volumes:
- ./:/var/www/html
command: sh -c "npm install && npm run dev"
ports:
- "5173:5173"
networks:
- app-network For Vue or Alpine frontends paired with Laravel Blade, see the Vue with Laravel setup guide. On legal-tech portals like Mijar Law Associates, I run the same Compose base across sister sites with only .env differences.
Parity with Production
Local Compose should mirror production topology, not production hardware. If production runs Apache + PHP-FPM on Ubuntu 24 with MySQL 8.4 LTS, match those versions locally. If production uses Docker on Ubuntu with Deployer 7, your Compose file becomes the reference spec for the production Dockerfile.
Do not run Docker in production on a single VPS unless you have orchestration requirements. For most custom software projects I deliver, production stays on bare PHP-FPM with Compose reserved for local dev and CI. The official Docker Compose documentation and Compose networking guide are the authoritative references for syntax changes.
Key Takeaways
- Define each service (PHP-FPM, Nginx, MySQL, Redis) in one
docker-compose.ymlwith explicit networks and volumes. - Use service hostnames (
db,redis) in.env— never127.0.0.1from inside containers. - Bind-mount application code for live reload; use named volumes for database persistence.
- Add health checks and
depends_onconditions to prevent startup race errors. - Run all CLI commands through
docker compose execto match container PHP and extensions. - Use Compose profiles and override files to keep optional services (mail, search, Node) out of the default stack.
People Also Ask
Do I need Docker Desktop to use Docker Compose?
On macOS and Windows, Docker Desktop includes Compose. On Linux, install the Docker Engine and Compose plugin separately (docker compose, not the legacy docker-compose hyphenated binary). Ubuntu 22.04 and 24.04 both support the plugin via Docker's official apt repository.
Can Docker Compose replace my local PHP installation entirely?
Yes, for project work. You only need Docker on the host. All PHP, Composer, and Node versions live inside containers. Some developers keep a host PHP for quick scripts — that is optional, not required.
How do I debug PHP inside a Docker Compose container?
Install Xdebug in your PHP Dockerfile and map port 9003 for step debugging. Point your IDE's path mappings from the host project root to /var/www/html inside the container. Trigger debugging with docker compose exec app php artisan serve or on your Nginx-served pages.
Is Docker Compose suitable for production deployment?
Compose works for small production setups and staging environments. For high-availability production, most teams move to orchestrators or bare-metal PHP-FPM with Deployer. Compose excels at local development parity and CI test environments — which is its primary strength.
Build Your Local Stack and Ship With Confidence
A well-structured Docker Compose multi-container setup for local development removes environment drift and gets new developers productive on day one. Start with the four-service stack in this guide — PHP-FPM, Nginx, MySQL, and Redis — then layer profiles for mail, search, and frontend tooling as your project grows. The same patterns scale from a solo Laravel app to multi-site pipelines like those described in Docker Compose for local Laravel.
If you want help architecting a Compose stack that mirrors your production environment, or migrating an existing team off XAMPP and Valet, get in touch — or browse the portfolio for examples of Laravel systems built with these workflows. For related reading, see local Laravel dev with Sail and Docker and about my development approach.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

