πŸš€ Mastering Docker for Laravel: A Beginner's Guide to Setup

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

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:

  1. 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
    
  2. Make code changes: Edit your Laravel files as usual in your IDE

  3. Run migrations after database changes:

    make migrate
    
  4. Start frontend asset compilation:

    make vite
    
  5. Test emails using MailHog:

    • Trigger an email in your application
    • View it at http://localhost:8025
  6. Run tests:

    make test
    
  7. 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

  1. Security

    • Non-root user in containers
    • Proper file permissions
    • Environment variable management
    • Regular security updates
  2. Performance

    • Volume mounting optimization
    • Cache configuration
    • Database optimization
    • Queue worker management
  3. 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!

Comments (0)
Leave a comment

Β© 2025 All rights reserved.