Are you tired of the "it works on my machine" syndrome? Looking for a way to streamline your Laravel development environment? Docker is the answer, and this guide will walk you through setting up a powerful, consistent development environment for your Laravel projects.
π Table of Contents
- Introduction to Docker
- Why Docker for Laravel?
- Getting Started
- Project Structure
- Docker Configuration
- Performance Optimizations
- Using Makefile for Simplicity
- Development Tools
- Troubleshooting
- Best Practices
- Conclusion
- Source Code
- Docker & Laravel Glossary
Introduction to Docker
Before diving into the Laravel setup, let's understand what Docker is and why it's revolutionary for development.
Docker is a platform that enables developers to package applications and their dependencies into standardized units called containers. These containers are lightweight, standalone, and executable packages that include everything needed to run the application: code, runtime, system tools, libraries, and settings.
Key Docker concepts:
- Container: A lightweight, standalone, executable package that includes everything needed to run a piece of software
- Image: A blueprint for a container (like a class in programming)
- Dockerfile: A script containing instructions to build a Docker image
- Docker Compose: A tool for defining and running multi-container Docker applications
- Volume: A persistent data storage mechanism that exists outside containers
- Network: A communication system that allows containers to talk to each other
Why Docker for Laravel?
Docker has revolutionized how we develop and deploy applications. For Laravel projects, it provides:
- Consistent environments: No more "it works on my machine" problems
- Easy onboarding: New team members can start developing in minutes, not days
- Production-like environment: Develop in an environment that mirrors production
- Isolated services: Keep your PHP, Nginx, MySQL, and Redis services separated and easily manageable
- Version control for environments: Track changes to your environment alongside your code
- No local dependency conflicts: Each project can use different versions of PHP, MySQL, etc. without conflicts
Getting Started
Creating a New Laravel Project with Docker
You can either create a new Laravel project or use Docker with an existing one. For a new project:
# Clone the example repository
git clone https://github.com/Dommmin/laravel-docker-deploy.git my-project
cd my-project
Project Structure
Our Docker setup follows a modular approach, keeping Docker configuration separate from application code:
project/
βββ docker/ # Docker configuration files
β βββ php/ # PHP configuration
β β βββ Dockerfile # Instructions to build PHP image
β β βββ php.ini # PHP settings
β β βββ www.conf # PHP-FPM settings
β βββ nginx/ # Nginx configuration
β β βββ conf.d/ # Server blocks
β βββ supervisord.conf # Process manager config
βββ docker-compose.yml # Multi-container definition
βββ Makefile # Helper commands
βββ .env # Environment variables
Docker Configuration
Base PHP Image
We're using a custom PHP 8.4 FPM image hosted on Docker Hub (dommin/php-8.4-fpm
). This image comes pre-configured with:
- PHP-FPM optimization
- Common PHP extensions
- Composer
- Node.js and npm
- Git
Dockerfile Explained
Below is our Dockerfile
with comments explaining each line:
# Use our custom PHP 8.4 FPM image as the base
FROM dommin/php-8.4-fpm:latest
ARG USER_ID=1000
ARG GROUP_ID=1000
# Copy our custom PHP and PHP-FPM configuration files
COPY docker/start.sh /usr/local/bin/start.sh
COPY docker/php/php.ini /usr/local/etc/php/conf.d/php.ini
COPY docker/php/www.conf /usr/local/etc/php-fpm.d/www.conf
COPY docker/supervisord.conf /etc/supervisor/supervisord.conf
# Switch to root user to perform privileged operations
USER root
ARG USER_ID=1000
ARG GROUP_ID=1000
RUN chmod +x /usr/local/bin/start.sh
RUN usermod -u ${USER_ID} www-data
RUN groupmod -g ${GROUP_ID} www-data
WORKDIR /var/www
# Switch back to non-root user for security
USER www-data
EXPOSE 9000
Docker Compose Services Explained
Our docker-compose.yml
creates a complete development environment with multiple interconnected services. Let's break down each service and its configuration:
App Service (PHP Application)
app:
build:
context: . # Use the current directory as build context
dockerfile: docker/php/Dockerfile
args:
# Pass host user/group IDs to avoid permission issues
- USER_ID=${USER_ID:-1000}
- GROUP_ID=${GROUP_ID:-1000}
container_name: ${COMPOSE_PROJECT_NAME}_app
command: ["sh", "-c", "/usr/local/bin/start.sh"]
restart: unless-stopped
working_dir: /var/www
volumes:
- ./:/var/www # Mount project directory into container
- ./docker/php/php.ini:/usr/local/etc/php/conf.d/custom.ini
- ./docker/php/www.conf:/usr/local/etc/php-fpm.d/www.conf
- ./docker/supervisord.conf:/etc/supervisor/supervisord.conf
- .env:/var/www/.env
networks:
- laravel-network
ports:
- "5173:5173" # Vite development server port
- "9000:9000" # PHP-FPM port
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
Key points about the app service:
- Uses a custom Dockerfile to build the PHP environment
- Mounts the entire project directory for live code updates
- Exposes port 9000 for PHP-FPM and 5173 for Vite
- Depends on MySQL and Redis being healthy before starting
- Uses supervisor to manage multiple processes (PHP-FPM, Horizon, Vite)
Nginx Service
nginx:
image: nginx:alpine
container_name: ${COMPOSE_PROJECT_NAME}_nginx
restart: unless-stopped
ports:
- "80:80"
volumes:
- ./:/var/www
- ./docker/nginx/conf.d:/etc/nginx/conf.d
depends_on:
- app
networks:
- laravel-network
Key points about the nginx service:
- Uses lightweight Alpine-based Nginx image
- Maps port 80 for web access
- Mounts custom Nginx configuration
- Depends on the app service for PHP processing
MySQL Service
mysql:
image: mysql:8.0
container_name: ${COMPOSE_PROJECT_NAME}_mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
MYSQL_DATABASE: ${DB_DATABASE}
MYSQL_DATABASE_TEST: laravel_test
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
healthcheck:
test: [ "CMD", "mysqladmin", "ping", "-h", "localhost" ]
interval: 10s
timeout: 5s
retries: 5
start_period: 60s
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
networks:
- laravel-network
Key points about the MySQL service:
- Uses MySQL 8.0 with UTF-8 support
- Implements health checks for container orchestration
- Persists data using a named volume
- Exposes port 3306 for database connections
Redis Service
redis:
image: redis:alpine
container_name: ${COMPOSE_PROJECT_NAME}_redis
restart: unless-stopped
healthcheck:
test: [ "CMD", "redis-cli", "ping" ]
interval: 10s
timeout: 5s
retries: 5
start_period: 60s
ports:
- "6379:6379"
networks:
- laravel-network
Key points about the Redis service:
- Uses Alpine-based Redis image for smaller footprint
- Implements health checks
- Exposes port 6379 for Redis connections
MailHog Service
mailhog:
image: mailhog/mailhog:latest
container_name: ${COMPOSE_PROJECT_NAME}_mailhog
restart: unless-stopped
ports:
- "1025:1025" # SMTP port
- "8025:8025" # Web interface port
volumes:
- mailhog_data:/maildir
networks:
- laravel-network
Key points about the MailHog service:
- Provides email testing environment
- Exposes SMTP and web interface ports
- Persists emails using a named volume
Nginx Configuration
The Nginx configuration in docker/nginx/conf.d/default.conf
is optimized for Laravel applications. Let's break down the key sections:
Basic Server Configuration
server {
listen 80;
listen [::]:80;
server_name localhost;
root /var/www/public;
index index.php;
# Logging configuration
access_log /var/www/storage/logs/nginx_access.log;
error_log /var/www/storage/logs/nginx_error.log;
charset utf-8;
client_max_body_size 100M;
client_body_buffer_size 128k;
}
This section sets up:
- HTTP server listening on port 80
- Document root pointing to Laravel's public directory
- Logging configuration for access and error logs
- UTF-8 character encoding
- File upload size limits
Static File Caching
# Cache static files
location ~* \.(?:ico|css|js|gif|jpe?g|png|woff2?|eot|ttf|svg|otf)$ {
expires 1y;
access_log off;
add_header Cache-Control "public, no-transform";
add_header X-Content-Type-Options "nosniff";
try_files $uri =404;
}
This configuration:
- Caches static assets for one year
- Disables access logging for static files
- Adds security headers
- Implements proper MIME type handling
PHP Processing
location ~ \.php$ {
try_files $uri =404;
include fastcgi_params;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass app:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;
fastcgi_param PATH_INFO $fastcgi_path_info;
# PHP-FPM cache settings
fastcgi_cache_use_stale error timeout http_500 http_503;
fastcgi_cache_valid 200 60m;
fastcgi_cache_bypass $http_pragma;
fastcgi_cache_revalidate on;
}
This section:
- Configures PHP-FPM processing
- Sets up FastCGI caching
- Handles PHP file execution
- Implements proper path handling
Gzip Compression
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript;
gzip_vary on;
gzip_comp_level 6;
gzip_min_length 1000;
gzip_proxied any;
This enables:
- Gzip compression for various file types
- Compression level optimization
- Minimum file size for compression
- Proper header handling
PHP-FPM Configuration
The PHP-FPM configuration in docker/php/www.conf
is optimized for performance and security. Let's examine the key settings:
Process Manager Settings
pm = dynamic
pm.max_children = 50
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 10
pm.max_requests = 500
These settings:
- Use dynamic process management
- Limit maximum child processes to 50
- Start with 5 server processes
- Maintain 5-10 spare servers
- Restart processes after 500 requests to prevent memory leaks
Timeouts and Logging
pm.process_idle_timeout = 10s
request_terminate_timeout = 30s
request_slowlog_timeout = 5s
slowlog = /proc/self/fd/2
php_admin_value[error_log] = /proc/self/fd/2
php_admin_flag[log_errors] = on
This configuration:
- Sets process idle timeout to 10 seconds
- Terminates requests after 30 seconds
- Logs slow requests (over 5 seconds)
- Configures error logging to stdout
Security Settings
php_admin_value[memory_limit] = 256M
php_admin_value[disable_functions] = "exec,passthru,shell_exec,system"
These settings:
- Limit PHP memory usage to 256MB
- Disable dangerous PHP functions
- Enhance container security
Performance Optimizations
PHP-FPM Configuration
- Process Manager Settings: Optimized
pm
settings for better resource usage - Worker Configuration: Proper settings for number of workers and requests per worker
- Memory Limits: Adjusted for development needs
Supervisor Configuration for Queue Workers
[supervisord]
nodaemon=true
logfile=/var/www/storage/logs/supervisord.log
pidfile=/var/run/supervisor/supervisord.pid
user=www-data
loglevel=info
[supervisorctl]
serverurl=unix:///var/run/supervisor/supervisor.sock
[unix_http_server]
file=/var/run/supervisor/supervisor.sock
chmod=0700
username=www-data
password=www-data
[program:php-fpm]
command=php-fpm -F
autostart=true
autorestart=true
priority=10
user=www-data
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:horizon]
process_name=%(program_name)s
command=php /var/www/artisan horizon
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/www/storage/logs/horizon.log
stopwaitsecs=3600
stopasgroup=true
killasgroup=true
[program:vite]
command=npm run dev
directory=/var/www
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/www/storage/logs/vite.log
environment=NODE_ENV=development
Nginx Optimization
- Gzip Compression: Reduces response size
- FastCGI Caching: Improves performance for repeated requests
- Static File Serving: Optimized for assets
- Security Headers: Adds protection against common vulnerabilities
Using Makefile for Simplicity
One of the most powerful additions to our Docker setup is the Makefile
. This file provides simple commands that abstract complex Docker operations, making it easier for developers to work with the environment.
What is a Makefile?
A Makefile is a configuration file used by the make
utility. It defines a set of tasks to be executed when you run the make
command with a specific target.
Benefits of Using Make with Docker
- Simplified Commands: Instead of typing long docker-compose commands, use short aliases
- Standardized Workflows: Everyone on the team uses the same commands
- Documentation: The Makefile itself serves as documentation for common operations
- Automation: Chain multiple commands together in a single make target
Our Docker Makefile
.PHONY: up down build install migrate fresh test setup-test-db
# Start the application
up:
docker compose up -d
# Stop the application
down:
docker compose down
# Build containers
build:
@echo "Setting up the project..."
@if [ ! -f .env ]; then \
cp .env.local .env; \
echo "Created .env file from .env.local"; \
fi
docker compose build
# Install dependencies
install:
docker compose exec app composer install
docker compose exec app npm install
# Run migrations
migrate:
docker compose exec app php artisan migrate
# Fresh migrations
fresh:
docker compose exec app php artisan migrate:fresh
# Setup test database
setup-test-db:
docker compose exec mysql mysql -uroot -psecret -e "CREATE DATABASE IF NOT EXISTS laravel_test;"
docker compose exec app php artisan migrate --env=testing
# Run tests
test: setup-test-db
docker compose exec app php artisan test --env=testing
# Run quality tools
quality:
docker compose exec app composer larastan
docker compose exec app composer pint
docker compose exec app npm run format
docker compose exec app npm run types
docker compose exec app npm run lint
# Setup project from scratch
setup: build up
docker compose exec app composer install
docker compose exec app npm install
docker compose exec app php artisan key:generate
docker compose exec app php artisan migrate
@echo "Project setup completed!"
# Show logs
logs:
docker compose logs -f
# Enter app container
shell:
docker compose exec app bash
# Clear all caches
clear:
docker compose exec app php artisan cache:clear
docker compose exec app php artisan config:clear
docker compose exec app php artisan route:clear
docker compose exec app php artisan view:clear
# Start Vite development server
vite:
docker compose exec app npm run dev
Using the Makefile
With our Makefile, setting up a fresh project is as simple as:
# Set up the entire project with one command
make setup
# Start the development environment
make up
# Run database migrations
make migrate
# Access the app container shell
make shell
# Start the Vite development server
make vite
This drastically simplifies developer onboarding. New team members can get a fully functioning environment with just a few commands!
Development Tools
Testing URLs
- Main application:
http://localhost
- MailHog interface:
http://localhost:8025
(for email testing) - Queue monitoring:
http://localhost/horizon
(if Horizon is installed)
Workflow Example
Here's a typical workflow using our Docker setup:
-
Clone the project and start containers:
git clone https://github.com/your-username/your-project.git cd your-project make setup # Builds containers, installs dependencies, runs migrations
-
Make code changes: Edit your Laravel files as usual in your IDE
-
Run migrations after database changes:
make migrate
-
Start frontend asset compilation:
make vite
-
Test emails using MailHog:
- Trigger an email in your application
- View it at
http://localhost:8025
-
Run tests:
make test
-
Shut down when done:
make down
Useful Commands
# View logs in real-time
make logs
# Clear all Laravel caches
make clear
# Get a shell in the app container
make shell
# Completely rebuild the containers
make build
Troubleshooting
Common Issues and Solutions
1. Permission Denied Errors
Error: EACCES: permission denied, open '/var/www/storage/logs/laravel.log'
Solution: Fix permissions and update USER_ID/GROUP_ID in your .env file
# Add to your .env file
USER_ID=$(id -u)
GROUP_ID=$(id -g)
# Then rebuild
make build
2. Port Already in Use
Error starting userland proxy: listen tcp4 0.0.0.0:80: bind: address already in use
Solution: Change the port in docker-compose.yml or stop the service using port 80
# In docker-compose.yml
ports:
- "8080:80" # Change 80 to 8080
3. MySQL Connection Issues
SQLSTATE[HY000] [2002] Connection refused
Solution: Check your .env file settings
DB_CONNECTION=mysql
DB_HOST=mysql # Must match service name in docker-compose.yml
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=your_password
4. Missing Node Modules
If you're getting errors about missing Node modules:
Solution: Reinstall Node modules
make install
5. Docker Containers Not Starting
If containers fail to start, check the logs:
Solution:
docker compose logs
Best Practices
-
Security
- Non-root user in containers
- Proper file permissions
- Environment variable management
- Regular security updates
-
Performance
- Volume mounting optimization
- Cache configuration
- Database optimization
- Queue worker management
-
Development Experience
- Hot reload for frontend
- Debug tools integration
- Easy access to logs
- Simple command execution
Conclusion
A well-configured Docker environment is crucial for modern Laravel development. This setup provides a solid foundation for building an efficient, secure, and maintainable development environment. The combination of Docker and the Makefile creates a powerful, yet easy-to-use system that will boost your productivity.
In our next article, we'll cover optimizing Docker images for production, where we'll explore Alpine-based images and other techniques to create lightweight, secure containers ready for deployment.
Source Code
You can find the complete code for this article on GitHub. Feel free to clone it and use it as a starting point for your own projects.
Docker & Laravel Glossary
- Container: A lightweight, standalone, executable software package
- Image: A template used to create containers
- Docker Compose: A tool for defining and running multi-container applications
- Volume: Persistent storage that exists outside containers
- Dockerfile: A script with instructions to build a Docker image
- Laravel Horizon: A queue monitoring dashboard for Laravel Redis queues
- MailHog: A development tool for email testing
- PHP-FPM: PHP FastCGI Process Manager for handling PHP requests
- Supervisor: A process control system to manage processes
- Makefile: A file containing a set of directives used by the make build automation tool
Follow me on LinkedIn for more Laravel and DevOps content!
Would you like to learn more about Docker with Laravel? Let me know in the comments what topics you'd like to see covered next!