Docker Compose in 2026: The Complete Guide for PHP and Laravel Developers

calendar_today July 10, 2026
person info@softcrony.com
folder DevOps

Docker Compose has become the standard way to run local development environments for PHP and Laravel projects. One command starts your entire stack — PHP, MySQL, Redis, Nginx, queue workers — consistently across every developer’s machine.

This guide covers everything you need for a production-grade Docker Compose setup for Laravel in 2026.

Why Docker Compose for Laravel?

The classic problem: “It works on my machine.” Docker Compose eliminates this by defining your entire environment as code. Every developer runs identical containers, and your staging environment matches production exactly.

  • No more PHP version conflicts between developers
  • MySQL, Redis, and other services start with one command
  • New team members are productive in minutes, not hours
  • Local environment matches staging and production exactly

Project Structure

your-laravel-app/
├── docker/
│   ├── php/
│   │   └── Dockerfile
│   ├── nginx/
│   │   └── default.conf
│   └── mysql/
│       └── my.cnf
├── docker-compose.yml
├── docker-compose.prod.yml
└── ... (Laravel files)

Step 1 — PHP Dockerfile

Create docker/php/Dockerfile:

FROM php:8.3-fpm-alpine

# Install system dependencies
RUN apk add --no-cache \
    git \
    curl \
    libpng-dev \
    libxml2-dev \
    zip \
    unzip \
    nodejs \
    npm

# Install PHP extensions
RUN docker-php-ext-install \
    pdo_mysql \
    mbstring \
    exif \
    pcntl \
    bcmath \
    gd \
    xml \
    opcache

# Install Redis extension
RUN pecl install redis && docker-php-ext-enable redis

# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

# Set working directory
WORKDIR /var/www

# Copy application files
COPY . .

# Install PHP dependencies
RUN composer install --no-dev --optimize-autoloader

# Set permissions
RUN chown -R www-data:www-data /var/www/storage /var/www/bootstrap/cache

EXPOSE 9000
CMD ["php-fpm"]

Step 2 — Nginx Configuration

Create docker/nginx/default.conf:

server {
    listen 80;
    server_name localhost;
    root /var/www/public;
    index index.php index.html;

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";
    add_header X-XSS-Protection "1; mode=block";

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    error_page 404 /index.php;

    location ~ \.php$ {
        fastcgi_pass php:9000;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
        fastcgi_hide_header X-Powered-By;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }

    # Cache static assets
    location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff|woff2)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}

Step 3 — docker-compose.yml

version: '3.9'

services:
  # Nginx Web Server
  nginx:
    image: nginx:alpine
    container_name: laravel_nginx
    restart: unless-stopped
    ports:
      - "80:80"
    volumes:
      - .:/var/www
      - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - php
    networks:
      - laravel

  # PHP-FPM
  php:
    build:
      context: .
      dockerfile: docker/php/Dockerfile
    container_name: laravel_php
    restart: unless-stopped
    volumes:
      - .:/var/www
      - ./docker/php/php.ini:/usr/local/etc/php/conf.d/custom.ini
    environment:
      - APP_ENV=local
    networks:
      - laravel
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_started

  # MySQL Database
  mysql:
    image: mysql:8.0
    container_name: laravel_mysql
    restart: unless-stopped
    ports:
      - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
      MYSQL_DATABASE: ${DB_DATABASE}
      MYSQL_USER: ${DB_USERNAME}
      MYSQL_PASSWORD: ${DB_PASSWORD}
    volumes:
      - mysql_data:/var/lib/mysql
      - ./docker/mysql/my.cnf:/etc/mysql/conf.d/my.cnf
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - laravel

  # Redis Cache
  redis:
    image: redis:7-alpine
    container_name: laravel_redis
    restart: unless-stopped
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
    networks:
      - laravel

  # Laravel Queue Worker
  queue:
    build:
      context: .
      dockerfile: docker/php/Dockerfile
    container_name: laravel_queue
    restart: unless-stopped
    command: php artisan queue:work --sleep=3 --tries=3 --max-time=3600
    volumes:
      - .:/var/www
    depends_on:
      - php
      - redis
    networks:
      - laravel

  # Laravel Scheduler
  scheduler:
    build:
      context: .
      dockerfile: docker/php/Dockerfile
    container_name: laravel_scheduler
    restart: unless-stopped
    command: sh -c "while true; do php artisan schedule:run --verbose --no-interaction && sleep 60; done"
    volumes:
      - .:/var/www
    depends_on:
      - php
      - mysql
    networks:
      - laravel

  # Mailpit (local email testing)
  mailpit:
    image: axllent/mailpit:latest
    container_name: laravel_mailpit
    ports:
      - "8025:8025"
      - "1025:1025"
    networks:
      - laravel

volumes:
  mysql_data:
  redis_data:

networks:
  laravel:
    driver: bridge

Step 4 — Environment Variables

Update your .env file for Docker:

APP_URL=http://localhost

DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=laravel
DB_PASSWORD=secret

REDIS_HOST=redis
REDIS_PASSWORD=secret
REDIS_PORT=6379

CACHE_STORE=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis

MAIL_MAILER=smtp
MAIL_HOST=mailpit
MAIL_PORT=1025

Step 5 — Common Commands

# Start all containers
docker compose up -d

# Stop all containers
docker compose down

# Run artisan commands
docker compose exec php php artisan migrate
docker compose exec php php artisan db:seed
docker compose exec php php artisan cache:clear

# Run composer
docker compose exec php composer install

# Run npm (if needed)
docker compose exec php npm install
docker compose exec php npm run build

# View logs
docker compose logs -f php
docker compose logs -f queue

# Access MySQL
docker compose exec mysql mysql -u laravel -p laravel

# Rebuild after Dockerfile changes
docker compose build --no-cache php
docker compose up -d

Production Configuration

Create a separate docker-compose.prod.yml that overrides development settings:

version: '3.9'

services:
  php:
    build:
      target: production
    environment:
      - APP_ENV=production
      - APP_DEBUG=false

  nginx:
    ports:
      - "443:443"
    volumes:
      - ./docker/nginx/prod.conf:/etc/nginx/conf.d/default.conf
      - /etc/letsencrypt:/etc/letsencrypt:ro

  # Remove mailpit in production
  mailpit:
    profiles:
      - dev

Run in production:

docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

Performance Tips

Use Alpine imagesphp:8.3-fpm-alpine is 80% smaller than the full Debian image.

Enable OPcache in production — add to your PHP configuration:

opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=64
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0

Use health checks — as shown in the MySQL service above. This prevents Laravel from starting before the database is ready.

Named volumes for persistence — always use named volumes for database data, never bind mounts.

If you need Docker Compose set up for your Laravel project or want a containerized deployment pipeline built for your team, our DevOps team at Softcrony can help.

Leave a comment