Introduction
Deploying a Laravel application to production can be a complex endeavor, especially when you need to manage dependencies, environment variables, and scaling across multiple servers. Docker Compose offers a streamlined approach by packaging the entire stack — web server, database, queue workers, and caching layers — into a single, reproducible configuration file. In this comprehensive guide, we will walk through the process of setting up a Laravel project with Docker Compose for a production‑ready environment. You will learn how to define services, configure volumes, manage environment variables, and integrate CI/CD pipelines. By the end of this tutorial, you will have a robust, scalable, and secure deployment pipeline that can be replicated across development, staging, and production environments.
Core Concepts
Before diving into the technical details, it is essential to understand the foundational concepts that underpin Docker Compose and its synergy with Laravel.
- Containers: A container is a lightweight, stand‑alone, executable package that includes everything needed to run a piece of software, including the code, runtime, libraries, and system libraries.
- Service: In a Docker Compose file, a service represents a container that performs a specific function, such as serving HTTP requests (NGINX), running the Laravel application (PHP‑FPM), or managing a database (MySQL).
- Volumes: Volumes provide persistent storage for containers, allowing data to survive container restarts. For Laravel, this is crucial for persisting database files, uploaded assets, and log files.
- Environment Variables: Docker Compose supports the use of .env files to inject configuration values into containers, enabling you to separate configuration from code and manage different environments (development, staging, production) with ease.
- Networks: Compose creates an isolated network for services to communicate securely. Laravel containers can talk to the database container via a predefined network alias, simplifying connection strings.
These concepts form the backbone of a reliable Docker‑based Laravel deployment. Understanding them will help you troubleshoot issues and make informed decisions when extending your setup.
Architecture Overview
The architecture of a Docker‑Compose‑powered Laravel application typically consists of four primary services:
nginx– a high‑performance web server that terminates SSL, serves static assets, and forwards PHP requests to the PHP‑FPM container.php– the PHP‑FPM container that executes Laravel's PHP code. It is built from an official PHP image with required extensions (e.g., PDO, Redis, ZIP) pre‑installed.mysql– the relational database management system that stores the application's data.redis– an in‑memory data store used for caching, session storage, and queue management.
Optional services may include worker for processing background jobs, mailhog for email testing, and phpmyadmin for database administration. The following diagram illustrates the flow of requests:
client --> nginx --> php-fpm --> Laravel Application | +--> mysql (database) +--> redis (cache & queue)By decoupling each component into its own container, you gain the ability to scale individual services independently, roll back changes safely, and maintain a consistent environment across all stages of the development lifecycle.
Step‑by‑Step Guide
Below is a practical, step‑by‑step walkthrough of creating a production‑ready Docker Compose setup for a Laravel project.
- Project Initialization: Begin by creating a new Laravel project or cloning an existing repository. Ensure that the
composer.jsonfile is up‑to‑date and that the.envfile contains the correct application key and database credentials. - Create a
Dockerfilefor PHP: Write a Dockerfile that extends the official PHP image, installs required extensions (e.g.,pdo_mysql,redis,zip), and copies the application code into the container. Example: - Define Services in
docker-compose.yml: Create adocker-compose.ymlfile at the root of the project. Below is a minimal but production‑focused configuration: - Configure NGINX: Create an
nginx.conffile that defines server blocks for HTTP and HTTPS (if using TLS). The configuration should root the static files to/var/www/html/publicand proxy PHP requests to thephpservice via upstream. - Set Up Environment Variables: Create a
.envfile in the project root with production‑specific values. Example: - Build and Start Containers: Run the following commands to build images and start services:
- Run migrations and seed data: Exec into the
phpcontainer and execute Laravel artisan commands:
FROM php:8.2-fpmWORKDIR /var/www/htmlRUN apt-get update && apt-get install -y \ git \ unzip \ libpng-dev \ libonig-dev \ libjpeg-dev \ libfreetype6-dev \ && docker-php-ext-configure gd --with-freetype --with-jpeg \ && docker-php-ext-install -j$(nproc) pdo_mysql mbstring exif gd zip \ && pecl install redis && docker-php-ext-enable redisCOPY . /var/www/htmlRUN composer install --no-dev --optimize-autoloaderversion: '3.8'services: nginx: image: nginx:alpine container_name: laravel_nginx ports: - "80:80" - "443:443" volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro - ./src:/var/www/html:ro depends_on: - php networks: - laravel_net php: build: ./docker/php container_name: laravel_php volumes: - ./src:/var/www/html environment: - APP_ENV=production - APP_KEY=${APP_KEY} - DB_HOST=mysql - DB_DATABASE=${DB_DATABASE} - DB_USERNAME=${DB_USERNAME} - DB_PASSWORD=${DB_PASSWORD} networks: - laravel_net mysql: image: mysql:8.0 container_name: laravel_mysql environment: - MYSQL_ROOT_PASSWORD=secret - MYSQL_DATABASE=${DB_DATABASE} - MYSQL_USER=${DB_USERNAME} - MYSQL_PASSWORD=${DB_PASSWORD} volumes: - mysql_data:/var/lib/mysql networks: - laravel_net redis: image: redis:7-alpine container_name: laravel_redis volumes: - redis_data:/data networks: - laravel_net worker: build: ./docker/php container_name: laravel_worker command: ["php","artisan","queue:work","--sleep=3","--tries=3"] volumes: - ./src:/var/www/html environment: - APP_ENV=production - DB_HOST=mysql - DB_DATABASE=${DB_DATABASE} - DB_USERNAME=${DB_USERNAME} - DB_PASSWORD=${DB_PASSWORD} networks: - laravel_netnetworks: laravel_net: driver: bridgevolumes: mysql_data: redis_data:user nginx;worker_processes auto;error_log /var/log/nginx/error.log warn;pid /var/run/nginx.pid;events { worker_connections 1024;}http { include /etc/nginx/mime.types; default_type application/octet-stream; sendfile on; keepalive_timeout 65; upstream php-fpm { server php:9000 max_fails=0 max_conns=1000; keepalive 16; } server { listen 80; server_name localhost; root /var/www/html/public; index index.php index.html; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { include fastcgi_params; fastcgi_pass php-fpm; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } location ~ /\.ht { deny all; } }}APP_ENV=productionAPP_KEY=base64:YourBase64KeyHereAPP_DEBUG=falseDB_CONNECTION=mysqlDB_HOST=mysqlDB_PORT=3306DB_DATABASE=laravelDB_USERNAME=laravel_userDB_PASSWORD=laravel_passREDIS_HOST=redisREDIS_PASSWORD=nulldocker compose builddocker compose up -dVerify that all containers are healthy using docker compose ps. You should see nginx, php, mysql, redis, and worker listed as up.
docker compose exec php php artisan migrate --forcedocker compose exec php php artisan db:seed --forceFinally, set up a CI/CD pipeline (e.g., GitHub Actions) that runs docker compose build and docker compose push to a registry, ensuring that every code change triggers a fresh image build and deployment.
Real‑World Examples
Below are two concrete scenarios that illustrate how Docker Compose can be leveraged in practical projects.
Example 1: Multi‑Tenant SaaS Application – A SaaS platform serving multiple customers requires isolated database schemas and per‑tenant rate limiting. By extending the base docker-compose.yml with per‑tenant service overrides, you can spin up dedicated database containers for each tenant while sharing the same NGINX and PHP images. This approach reduces infrastructure costs and simplifies scaling.
Example 2: Event‑Driven Architecture – A real‑time notification system uses Laravel Horizon alongside a Redis queue. In this setup, the worker service is configured with a higher concurrency level, and Redis is persisted with a larger memory allocation. The NGINX server is configured to expose a health‑check endpoint that reports queue depth, enabling automated scaling triggers.
Production Code Examples
Below are several ready‑to‑copy snippets that demonstrate best‑practice implementations.
Dockerfile for PHP (with Xdebug disabled for production):
FROM php:8.2-fpmRUN apt-get update && apt-get install -y \ libzip-dev \ libonig-dev \ && docker-php-ext-install zip pdo_mysql mbstring \ && pecl install redis && docker-php-ext-enable redisWORKDIR /var/www/htmlCOPY . /var/www/htmlRUN composer install --no-dev --optimize-autoloader && \ php artisan config:cache && \ php artisan route:cache && \ php artisan view:cacheNGINX Upstream Configuration (optimized for performance):
upstream php-fpm { server php:9000 max_fails=0 max_conns=1000; keepalive 16;}Laravel Queue Worker Dockerfile (lightweight):
FROM php:8.2-cliWORKDIR /var/wwwCOPY . /var/wwwRUN composer install --no-dev --optimize-autoloader && \ php artisan horizon:install && \ rm -rf /var/www/vendor/* && \ composer install --no-dev --optimize-autoloaderThese snippets are intentionally minimal yet production‑ready, emphasizing security, caching, and performance.
Comparison Table
| Feature | Docker Compose | Docker Swarm | Kubernetes |
|---|---|---|---|
| Complexity | Low – single YAML file | Medium – requires orchestration commands | High – manifests, Helm charts |
| Scaling Granularity | Service‑level only | Service‑level with rolling updates | Pod‑level with auto‑scaling |
| Learning Curve | Beginner‑friendly | Intermediate | Advanced |
| Use‑Case Fit for Laravel | Ideal for small‑to‑medium deployments | Suitable for larger clusters | Best for enterprise‑scale platforms |
While Docker Compose shines for straightforward deployments, enterprises with massive scale may eventually migrate to Swarm or Kubernetes for advanced features like rolling updates and service discovery across clusters.
Best Practices
Adhering to a set of best practices will keep your Docker‑Compose Laravel stack maintainable and performant.
- Pin Image Versions: Always specify exact image tags (e.g.,
php:8.2-fpm) to avoid accidental upgrades that could introduce breaking changes. - Use Non‑Root Users: Create a dedicated non‑root user inside your PHP image and assign appropriate file permissions to avoid privilege escalation attacks.
- Separate Config Files: Keep environment‑specific settings in separate
.envfiles (e.g.,.env.production) and load them conditionally during container startup. - Leverage Build Args for Build‑Time Secrets: Pass secrets like
APP_KEYvia build arguments only when necessary, and never commit them to source control. - Implement Health Checks: Add
healthcheckdirectives to critical services to enable Docker to automatically restart failed containers. - Limit Container Resources: Set CPU and memory limits in the Compose file to prevent a single service from monopolizing host resources.
- Regularly Clean Up Unused Images: Use
docker system prunein CI pipelines to avoid disk bloat over time.
Common Mistakes
Even experienced developers encounter pitfalls when first adopting Docker Compose for Laravel. Below are the most frequent errors and how to avoid them.
- Hard‑coding Secrets in Compose Files: Avoid placing passwords directly in
docker-compose.yml. Use environment files or Docker secrets instead. - Neglecting Volume Permissions: When mounting source code into containers, ensure the file ownership matches the container's user to prevent permission denied errors.
- Skipping Migrations in Production: Always run
php artisan migrate --forceas part of the deployment script; forgetting this step can lead to schema mismatches. - Over‑Reusing Cached Configs: After changing environment variables, clear config caches with
php artisan config:clearto avoid stale settings. - Missing Health Checks: Without health checks, Docker cannot detect a failing service, leading to prolonged downtime.
By recognizing these mistakes early, you can save hours of debugging and ensure a smoother production rollout.
Performance Tips
Optimizing performance in a Docker‑Compose Laravel environment involves both container configuration and application‑level tuning.
- Enable OPcache: Add
opcache.enable=1and related directives to the PHP FPM configuration to reduce script compilation overhead. - Use Alpine Variants: Where possible, base images on Alpine Linux to reduce image size and improve I/O speed.
- Cache Routes and Config: Run
php artisan route:cacheandphp artisan config:cachein the Docker build stage to avoid runtime recompilation. - Adjust NGINX Worker Settings: Increase
worker_processesandworker_connectionsbased on the number of CPU cores to handle higher traffic. - Leverage Redis for Session Storage: Configure Laravel to store sessions in Redis, reducing database load and improving response times.
Implementing these tips can yield measurable reductions in request latency and server resource consumption.
Security Considerations
Security is paramount when exposing a Laravel application to the internet. The following measures are recommended:
- Run Containers as Non‑Root: Create a dedicated user inside each container and set file permissions to restrict access.
- Enable TLS Termination at NGINX: Use Let's Encrypt certificates or a commercial provider to encrypt traffic between clients and the load balancer.
- Limit Database Access: Expose the MySQL port only to the internal Docker network, never to the host directly.
- Regularly Update Base Images: Pull the latest security patches for PHP, NGINX, and MySQL images on a scheduled basis.
- Implement Rate Limiting: Use NGINX's
limit_reqmodule to mitigate abuse and brute‑force attacks.
These practices collectively reduce the attack surface and protect sensitive data.
Deployment Notes
When moving from a staging environment to production, consider the following checklist:
- Verify that all environment variables are correctly set in the production
.envfile. - Run database migrations and seed data in a controlled manner.
- Set
APP_DEBUG=falseand enable error logging to a centralized system (e.g., Papertrail). - Configure a reverse proxy or load balancer (e.g., AWS ALB) if you plan to run multiple replicas.
- Schedule regular backups of database volumes using
mysqldumpor cloud‑based snapshot solutions.
Following this checklist helps ensure a smooth transition and minimizes the risk of downtime.
Debugging Tips
Even well‑configured stacks can encounter issues. Here are some practical debugging techniques:
- Inspect Container Logs: Use
docker compose logs -f phpto stream real‑time output from the PHP container. - Exec into Containers: Run
docker compose exec php bashto interactively explore the environment and run artisan commands. - Use Xdebug for Local Development: Mount the Xdebug extension in a development‑only compose file to step through code.
- Check File Permissions: Ensure that the
storageandbootstrap/cachedirectories are writable by the container user. - Validate NGINX Configuration: Run
nginx -tinside the NGINX container to catch syntax errors before reload.
These tips can shorten the debugging cycle and help you quickly pinpoint root causes.
FAQ
What is the difference between Docker Compose and Docker Swarm?
Docker Compose is a tool for defining and running multi‑container Docker applications on a single host using a simple YAML file. Docker Swarm extends Docker's native clustering capabilities, providing orchestration, scaling, and rolling updates across multiple hosts. For small‑to‑medium Laravel deployments, Compose is usually sufficient; larger, multi‑host environments may benefit from Swarm or Kubernetes.
Can I use Docker Compose for a production environment?
Yes, Docker Compose can be used in production, especially for modest workloads or when you need a reproducible, version‑controlled deployment pipeline. However, for high‑availability requirements, consider moving to Docker Swarm or Kubernetes.
How do I manage environment variables securely?
Store secrets in a separate .env.production file that is excluded from version control (add to .gitignore). In CI/CD pipelines, inject these variables as build arguments or Docker secrets rather than hard‑coding them.
Is it necessary to run migrations inside a container?
Yes. Running migrations inside the PHP container ensures that the database schema is updated in the same environment where the application code resides, maintaining consistency and avoiding version drift.
What is the best way to cache Laravel configurations?
Use the built‑in Artisan commands php artisan config:cache, php artisan route:cache, and php artisan view:cache. Execute these commands during the Docker image build stage to bake the cached files into the image.
How can I monitor the health of my containers?
Add healthcheck directives in the Compose file for critical services. You can also integrate monitoring tools like Prometheus and Grafana to collect metrics and trigger alerts.
Do I need to expose ports in production?
Only expose the ports that must be accessible from outside the host. Typically, NGINX listens on port 80/443 for HTTP/HTTPS traffic, while internal services communicate over an internal network without publishing ports.
Can I use Docker Compose with CI/CD pipelines?
Absolutely. Many CI systems (GitHub Actions, GitLab CI, CircleCI) provide Docker‑in‑Docker services that allow you to run docker compose build and docker compose push as part of your deployment workflow.
Conclusion
By now you should have a clear understanding of how to leverage Docker Compose to create a production‑grade Laravel deployment that is secure, performant, and easy to maintain. The patterns and best practices outlined in this guide can be adapted to a variety of use cases, from small blogs to complex multi‑tenant SaaS platforms. Remember to keep your configuration files under version control, automate testing, and continuously monitor your stack for optimal results.
Ready to take your Laravel application to the next level? Start by adding a docker-compose.yml to your repository today and experience the benefits of reproducible, scalable deployments.