Why Containerize Laravel Applications?
Containerizing a Laravel application transforms how teams develop, test, and deploy their PHP applications. Rather than managing system-level dependencies, developer machines, and production servers separately, Docker provides a unified abstraction layer that guarantees identical behavior everywhere. This approach eliminates the classic "works on my machine" problem while streamlining onboarding for new team members and simplifying CI/CD pipeline integration.
Our DevOps services team specializes in helping organizations transition their PHP applications to containerized infrastructure. Whether you're running a Laravel application or any other PHP framework, containerization provides the consistency and scalability modern applications demand.
The architecture we're building consists of multiple specialized containers working in concert: a PHP-FPM container to process PHP code, an Nginx container to handle HTTP requests and serve static assets, a MySQL container for data persistence, and Redis for caching and session management. This separation of concerns mirrors production-grade infrastructure patterns while remaining lightweight enough for local development.
Ubuntu 22.04 serves as an excellent host for Docker-based Laravel development due to its stability, extensive documentation, and native support for Docker's installation workflows. The Long-Term Support (LTS) status ensures that security updates remain available throughout the development lifecycle, making it a reliable foundation for hosting containerized applications.
Installing Docker on Ubuntu 22.04
Before containerizing Laravel applications, the host system must have Docker Engine and Docker Compose installed. The installation process on Ubuntu 22.04 involves adding Docker's official repository to ensure access to the latest stable versions while maintaining system integrity through package signing.
Begin by updating the existing package index and installing prerequisites for the repository:
sudo apt update
sudo apt install -y ca-certificates curl gnupg lsb-release
Add Docker's GPG key and repository to ensure package authenticity:
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
Install Docker Engine, containerd, and Docker Compose plugin:
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
Verify the installation by checking Docker version and ensuring the daemon runs correctly:
docker --version
docker compose version
sudo docker run hello-world
The Docker daemon requires sudo privileges for execution, which can be inconvenient during frequent development tasks. Add your user to the docker group to enable command execution without elevation:
sudo usermod -aG docker $USER
newgrp docker
After group membership changes, log out and back in for the new permissions to take effect.
Creating Laravel Project Structure
A well-organized project structure separates application code from Docker configuration, making the codebase maintainable and deployment-ready. The recommended structure places Docker-related files in a dedicated docker directory while keeping Laravel's standard directory structure intact.
Create a new Laravel project or navigate to an existing one:
composer create-project laravel/laravel /path/to/project
cd /path/to/project
Establish the Docker configuration directory hierarchy:
mkdir -p docker/php-fpm docker/nginx docker/mysql docker/common
The resulting structure organizes Docker configurations logically:
project/
├── app/
├── bootstrap/
├── config/
├── database/
├── public/
├── resources/
├── routes/
├── storage/
├── docker/
│ ├── php-fpm/
│ │ ├── Dockerfile
│ │ └── php.ini
│ ├── nginx/
│ │ ├── Dockerfile
│ │ └── default.conf
│ └── mysql/
│ └── my.cnf
├── docker-compose.yml
├── .env
└── ...
This organization allows different services to maintain their own configurations while keeping them version-controlled alongside the application code. The separation also facilitates environment-specific customization without duplicating entire configuration files.
PHP-FPM Container Configuration
The PHP-FPM container serves as the core of the Laravel application, processing PHP requests from the Nginx reverse proxy. Building a custom Dockerfile allows fine-tuning PHP extensions, installing dependencies, and configuring debugging tools specific to Laravel's requirements.
For teams working with web development projects, properly configured PHP containers are essential for maintaining consistent development environments across distributed teams. Our web development expertise ensures your PHP configuration follows industry best practices for performance and security.
Create the PHP-FPM Dockerfile:
FROM php:8.2-fpm-alpine
# Install system dependencies
RUN apk add --no-cache \
curl \
git \
unzip \
libzip-dev \
mysql-client \
nodejs \
npm \
&& docker-php-ext-install \
zip \
pdo_mysql \
pdo_pgsql \
bcmath \
gd \
&& rm -rf /var/cache/apk/*
# Install Composer
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
# Set working directory
WORKDIR /var/www
# Copy application code
COPY . .
# Install dependencies
RUN composer install --no-interaction --optimize-autoloader
# Create non-root user for security
RUN addgroup -g 1000 -S www && \
adduser -S www -u 1000 -G www -h /var/www -s /bin/sh
RUN chown -R www:www /var/www
USER www
EXPOSE 9000
CMD ["php-fpm"]
This Dockerfile uses Alpine Linux for a minimal image size while including all dependencies Laravel requires for operation. Add PHP configuration customization by creating docker/php-fpm/php.ini:
memory_limit = 256M
upload_max_filesize = 64M
post_max_size = 64M
max_execution_time = 120
The PHP-FPM container listens on port 9000 by default, which Nginx uses as its upstream server. This separation allows horizontal scaling of PHP workers while keeping Nginx as a single entry point for request handling.
Nginx Reverse Proxy Setup
Nginx acts as the front-facing web server, handling HTTPS termination, serving static assets, and forwarding PHP requests to the upstream PHP-FPM pool. Creating a dedicated Nginx configuration for Laravel ensures proper URL rewriting, caching policies, and security headers.
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" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied expired no-cache no-store private auth;
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml application/javascript;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass php:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_read_timeout 300;
}
location ~ /\.ht {
deny all;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
# Cache static assets
location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
The Nginx configuration implements several production-grade practices: security headers protect against common attack vectors, gzip compression reduces bandwidth usage and improves response times, and static asset caching through the expires directive decreases server load for frequently requested files.
Create the Nginx Dockerfile:
FROM nginx:alpine
COPY default.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
MySQL Database Container
The MySQL container provides relational database services for Laravel's Eloquent ORM and migration system. Docker Compose manages container lifecycle and networking automatically, making database setup remarkably simple compared to traditional installation methods.
Add MySQL configuration in docker/mysql/my.cnf:
[mysqld]
character-set-client-handshake = FALSE
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
[mysql]
default-character-set = utf8mb4
[client]
default-character-set = utf8mb4
The utf8mb4 character set supports full Unicode including emoji characters, which modern applications often require. This configuration eliminates character encoding issues that can cause data corruption in internationalized applications.
Orchestrating with Docker Compose
Docker Compose coordinates all containers, defining their relationships, networking, and environment configurations in a declarative YAML file. This file serves as infrastructure-as-code documentation that teams can version control and reproduce across environments.
Create docker-compose.yml at the project root:
version: '3.8'
services:
nginx:
build: ./docker/nginx
ports:
- "8080:80"
volumes:
- .:/var/www
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
depends_on:
- php
networks:
- laravel
restart: unless-stopped
php:
build: ./docker/php-fpm
volumes:
- .:/var/www
networks:
- laravel
restart: unless-stopped
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD:-secret}
MYSQL_DATABASE: ${DB_DATABASE:-laravel}
MYSQL_USER: ${DB_USERNAME:-laravel}
MYSQL_PASSWORD: ${DB_PASSWORD:-secret}
volumes:
- mysql_data:/var/lib/mysql
- ./docker/mysql/my.cnf:/etc/mysql/conf.d/my.cnf
networks:
- laravel
restart: unless-stopped
redis:
image: redis:alpine
networks:
- laravel
restart: unless-stopped
networks:
laravel:
driver: bridge
volumes:
mysql_data:
Key configuration patterns:
-
Environment Variables: Externalizing configuration to environment variables allows the same compose file to serve development, staging, and production environments without modification. The
${VAR:-default}syntax provides fallback values when variables aren't set. -
Named Volumes: The
mysql_datavolume persists database data across container restarts and recreations. Without named volumes, data would be lost when containers are removed. -
Dependency Ordering: The
depends_ondirective ensures Nginx waits for PHP-FPM to be ready before attempting connections, preventing race conditions during startup. -
Network Isolation: The
laravelbridge network provides DNS resolution between containers using service names, allowing PHP to connect tomysqlandredisas hostnames. -
Restart Policies:
unless-stoppedensures containers restart automatically after system reboots or Docker daemon restarts, improving availability for development environments.
Launching the Environment
With all configurations in place, build and start the containerized Laravel environment. Docker Compose handles image building, network creation, and container startup in a single command:
docker compose build
docker compose up -d
The build command executes Dockerfile instructions, creating images for PHP-FPM and Nginx services. The -d flag runs containers in detached mode, allowing terminal access for subsequent commands.
Verify all services are running:
docker compose ps
Access the Laravel application by navigating to http://localhost:8080 in a browser. The welcome page indicates successful configuration, while API routes test database connectivity.
Running Artisan Commands:
docker compose exec php php artisan migrate:fresh --seed
docker compose exec php php artisan queue:work
The exec command runs commands in existing containers, using the configured working directory and environment variables automatically.
Container Security Best Practices
Container security requires attention at multiple layers: image construction, container configuration, and runtime behavior. Implementing security measures protects both the application and the host system from potential vulnerabilities.
Non-Root Container Execution: Running containers as non-root users prevents privilege escalation attacks. The Dockerfile creates a dedicated www user with UID 1000, ensuring consistent permissions across environments.
Image Scanning: Integrate container image scanning into CI/CD pipelines to identify vulnerabilities before deployment. Tools like Trivy, Snyk, or GitHub's native scanning can flag outdated packages and known CVEs.
Secrets Management: For production deployments, avoid embedding sensitive values in Dockerfiles or compose files. Use Docker Swarm secrets, external secret management services, or runtime environment injection:
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD_FILE: /run/secrets/db_root_password
secrets:
- db_root_password
Network Segmentation: The bridge network isolates Laravel services from other containers while allowing necessary inter-service communication. Production deployments should consider overlay networks with encryption for multi-host setups.
Monitoring and Health Checks
Containerized applications require different monitoring approaches than traditional deployments. Understanding container logs, health checks, and resource utilization helps maintain application reliability. For teams implementing comprehensive container monitoring strategies, our DevOps services include observability setup and alerting configuration for production environments.
Health Checks: Docker Compose supports health checks that determine container readiness for dependent services:
services:
php:
build: ./docker/php-fpm
healthcheck:
test: ["CMD", "php-fpm-healthcheck"]
interval: 30s
timeout: 3s
retries: 3
start_period: 30s
Key Metrics to Monitor:
- Container resource usage (CPU, memory, disk)
- Application performance metrics (response times, queue length)
- Database query performance and connection pool status
- External service dependencies health
Logging Configuration: Configure Laravel to output JSON-formatted logs for easier parsing by log aggregation systems. Centralized logging with Docker logging drivers enables correlation across services.
Recommended Tools: Prometheus for metrics collection, Grafana for visualization, and Loki or ELK stack for log aggregation provide a complete observability stack for containerized Laravel applications.
Common Issues and Solutions
Day-to-day Laravel development with Docker involves troubleshooting common scenarios that differ slightly from traditional installations.
502 Bad Gateway: Typically indicates PHP-FPM isn't running or Nginx can't connect. Check PHP container logs for startup errors.
docker compose logs php
Connection Refused to MySQL: Verify DB_HOST matches the service name in Docker Compose. Ensure MySQL has finished initializing before application connections.
Permission Denied: File permission issues occur when host UID/GID doesn't match container users. Recreate containers after group membership changes.
Debugging Commands:
# View container logs
docker compose logs -f php
docker compose logs -f nginx
# Execute commands in running containers
docker compose exec php bash
# Check resource usage
docker stats
# Inspect container details
docker inspect container_name
Managing Dependencies:
docker compose exec php composer update
docker compose exec php npm install
docker compose exec mysql mysql -u laravel -p laravel
Container Orchestration
Use Kubernetes or Docker Swarm for multi-host deployments, horizontal scaling, and automated failover.
CI/CD Integration
Docker Compose files integrate naturally with GitHub Actions, GitLab CI, and similar platforms for automated testing and deployment.
SSL/TLS Termination
Terminate HTTPS at the load balancer or reverse proxy. Let's Encrypt provides free certificates through automated ACME protocols.
Disaster Recovery
Configure backup procedures, automate volume snapshots, and establish recovery time objectives for production databases.