Introduction
Docker revolutionizes application deployment by packaging applications with all dependencies into portable, consistent containers. For Laravel applications, Docker eliminates "works on my machine" problems, simplifies environment management, and enables seamless scaling across development, staging, and production.
Containerizing Laravel applications provides isolation, reproducibility, and efficient resource utilization. Whether you're deploying to a single server or orchestrating microservices across Kubernetes clusters, Docker provides the foundation for modern, cloud-native Laravel deployments.
In this comprehensive guide, we'll build a production-ready Docker setup for Laravel 12, covering multi-stage builds, service orchestration with docker-compose, optimization strategies, security best practices, and deployment workflows.
Understanding Docker Architecture
Docker Components for Laravel
- PHP-FPM Container – Runs Laravel application code
- Nginx Container – Serves static files and proxies PHP requests
- MySQL/PostgreSQL Container – Database service
- Redis Container – Cache and queue backend
- Node Container – Asset compilation (development)
Benefits of Dockerized Laravel
- Consistency – Identical environments across all stages
- Isolation – Services run in separate containers
- Scalability – Easy horizontal scaling
- Portability – Deploy anywhere Docker runs
- Version Control – Infrastructure as code
- Resource Efficiency – Share host kernel
Basic Dockerfile Configuration
1. Development Dockerfile
Create Dockerfile in project root:
FROM php:8.3-fpm
# Install system dependencies
RUN apt-get update && apt-get install -y \
git \
curl \
libpng-dev \
libonig-dev \
libxml2-dev \
zip \
unzip \
libzip-dev \
libpq-dev
# Clear cache
RUN apt-get clean && rm -rf /var/lib/apt/lists/*
# Install PHP extensions
RUN docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd zip
# 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 . /var/www
# Install dependencies
RUN composer install --no-dev --optimize-autoloader --no-interaction
# Set permissions
RUN chown -R www-data:www-data /var/www \
&& chmod -R 755 /var/www/storage
# Expose port 9000
EXPOSE 9000
CMD ["php-fpm"]
2. Nginx Configuration
Create docker/nginx/default.conf:
server {
listen 80;
server_name localhost;
root /var/www/public;
index index.php index.html;
charset utf-8;
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 app:9000;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.(?!well-known).* {
deny all;
}
# Gzip compression
gzip on;
gzip_vary on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
}
Docker Compose Configuration
1. Basic docker-compose.yml
Create docker-compose.yml:
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
image: laravel-app
container_name: laravel-app
restart: unless-stopped
working_dir: /var/www
volumes:
- ./:/var/www
- ./docker/php/local.ini:/usr/local/etc/php/conf.d/local.ini
networks:
- laravel
nginx:
image: nginx:alpine
container_name: laravel-nginx
restart: unless-stopped
ports:
- "8000:80"
volumes:
- ./:/var/www
- ./docker/nginx:/etc/nginx/conf.d
networks:
- laravel
depends_on:
- app
mysql:
image: mysql:8.0
container_name: laravel-mysql
restart: unless-stopped
environment:
MYSQL_DATABASE: ${DB_DATABASE}
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
MYSQL_PASSWORD: ${DB_PASSWORD}
MYSQL_USER: ${DB_USERNAME}
volumes:
- mysql_data:/var/lib/mysql
ports:
- "3306:3306"
networks:
- laravel
redis:
image: redis:alpine
container_name: laravel-redis
restart: unless-stopped
ports:
- "6379:6379"
networks:
- laravel
networks:
laravel:
driver: bridge
volumes:
mysql_data:
driver: local
2. PHP Configuration
Create docker/php/local.ini:
upload_max_filesize=40M
post_max_size=40M
memory_limit=256M
max_execution_time=600
max_input_time=600
3. Running Docker Compose
# Start all services
docker-compose up -d
# View logs
docker-compose logs -f
# Execute commands in containers
docker-compose exec app php artisan migrate
docker-compose exec app php artisan cache:clear
# Stop services
docker-compose down
# Remove volumes
docker-compose down -v
Multi-Stage Production Dockerfile
Optimized Production Build
# Stage 1: Dependencies
FROM composer:latest AS composer
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install \
--no-dev \
--no-scripts \
--no-autoloader \
--prefer-dist
# Stage 2: Frontend Assets
FROM node:18-alpine AS frontend
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 3: Final Production Image
FROM php:8.3-fpm-alpine
# Install production dependencies
RUN apk add --no-cache \
nginx \
supervisor \
mysql-client \
libpng \
libzip \
postgresql-libs
# Install PHP extensions
RUN docker-php-ext-install pdo_mysql pdo_pgsql opcache
# Install Redis
RUN apk add --no-cache --virtual .build-deps \
$PHPIZE_DEPS \
&& pecl install redis \
&& docker-php-ext-enable redis \
&& apk del .build-deps
# Configure PHP for production
COPY docker/php/production.ini /usr/local/etc/php/conf.d/production.ini
# Configure OPcache
RUN echo "opcache.enable=1" >> /usr/local/etc/php/conf.d/opcache.ini \
&& echo "opcache.memory_consumption=256" >> /usr/local/etc/php/conf.d/opcache.ini \
&& echo "opcache.interned_strings_buffer=16" >> /usr/local/etc/php/conf.d/opcache.ini \
&& echo "opcache.max_accelerated_files=10000" >> /usr/local/etc/php/conf.d/opcache.ini \
&& echo "opcache.validate_timestamps=0" >> /usr/local/etc/php/conf.d/opcache.ini
# Set working directory
WORKDIR /var/www
# Copy dependencies from composer stage
COPY --from=composer /app/vendor ./vendor
# Copy application
COPY . .
# Copy built assets from frontend stage
COPY --from=frontend /app/public/build ./public/build
# Generate optimized autoload
RUN composer dump-autoload --optimize
# Optimize Laravel
RUN php artisan config:cache \
&& php artisan route:cache \
&& php artisan view:cache
# Set permissions
RUN chown -R www-data:www-data /var/www \
&& chmod -R 755 /var/www/storage \
&& chmod -R 755 /var/www/bootstrap/cache
# Copy Nginx configuration
COPY docker/nginx/production.conf /etc/nginx/http.d/default.conf
# Copy Supervisor configuration
COPY docker/supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
# Expose port
EXPOSE 80
# Start Supervisor
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
Supervisor Configuration
Create docker/supervisor/supervisord.conf:
[supervisord]
nodaemon=true
user=root
logfile=/var/log/supervisor/supervisord.log
pidfile=/var/run/supervisord.pid
[program:nginx]
command=nginx -g "daemon off;"
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:php-fpm]
command=php-fpm -F
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/artisan queue:work --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/storage/logs/worker.log
stopwaitsecs=3600
Production Docker Compose
Create docker-compose.production.yml:
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile.production
image: laravel-app:production
container_name: laravel-app-prod
restart: always
environment:
- APP_ENV=production
- APP_DEBUG=false
- DB_HOST=mysql
- REDIS_HOST=redis
networks:
- laravel
depends_on:
- mysql
- redis
healthcheck:
test: ["CMD", "php", "artisan", "health:check"]
interval: 30s
timeout: 10s
retries: 3
nginx-proxy:
image: nginx:alpine
container_name: laravel-proxy
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- ./docker/nginx/proxy.conf:/etc/nginx/conf.d/default.conf
- ./ssl:/etc/nginx/ssl
networks:
- laravel
depends_on:
- app
mysql:
image: mysql:8.0
container_name: laravel-mysql-prod
restart: always
environment:
MYSQL_DATABASE: ${DB_DATABASE}
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
MYSQL_USER: ${DB_USERNAME}
MYSQL_PASSWORD: ${DB_PASSWORD}
volumes:
- mysql_data:/var/lib/mysql
- ./docker/mysql/my.cnf:/etc/mysql/conf.d/my.cnf
networks:
- laravel
command: --default-authentication-plugin=mysql_native_password
redis:
image: redis:alpine
container_name: laravel-redis-prod
restart: always
command: redis-server --appendonly yes
volumes:
- redis_data:/data
networks:
- laravel
networks:
laravel:
driver: bridge
volumes:
mysql_data:
redis_data:
Docker Best Practices
1. .dockerignore File
Create .dockerignore:
.git
.env
.env.*
node_modules
vendor
storage/logs/*
storage/framework/cache/*
storage/framework/sessions/*
storage/framework/views/*
public/hot
public/storage
tests
.phpunit.result.cache
2. Layer Caching Optimization
# ✅ Copy only dependency files first
COPY composer.json composer.lock ./
RUN composer install
# Then copy application code
COPY . .
# ❌ Avoid this (rebuilds everything on any file change)
COPY . .
RUN composer install
3. Security Hardening
# Use non-root user
RUN addgroup -g 1000 laravel \
&& adduser -D -u 1000 -G laravel laravel
USER laravel
# Read-only root filesystem
VOLUME ["/var/www/storage", "/var/www/bootstrap/cache"]
# Drop capabilities
RUN apk add --no-cache libcap \
&& setcap 'cap_net_bind_service=+ep' /usr/sbin/nginx
4. Health Checks
Add to Dockerfile:
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s \
CMD php artisan health:check || exit 1
Create health check route:
// routes/web.php
Route::get('/health', function () {
return response()->json(['status' => 'healthy'], 200);
});
Development Workflow
1. Development Docker Compose
Create docker-compose.dev.yml:
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile.dev
volumes:
- ./:/var/www
environment:
- APP_ENV=local
- APP_DEBUG=true
ports:
- "9000:9000"
node:
image: node:18-alpine
working_dir: /var/www
volumes:
- ./:/var/www
command: npm run dev
ports:
- "5173:5173"
2. Useful Docker Commands
# Build without cache
docker-compose build --no-cache
# View resource usage
docker stats
# Clean up unused resources
docker system prune -a
# Execute Laravel commands
docker-compose exec app php artisan migrate:fresh --seed
docker-compose exec app php artisan test
# Access container shell
docker-compose exec app sh
# View container logs
docker-compose logs -f app
# Restart specific service
docker-compose restart nginx
CI/CD Integration
GitHub Actions with Docker
name: Docker Build and Deploy
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile.production
push: true
tags: username/laravel-app:latest
cache-from: type=registry,ref=username/laravel-app:buildcache
cache-to: type=registry,ref=username/laravel-app:buildcache,mode=max
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Deploy to server
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /var/www/laravel-app
docker-compose pull
docker-compose up -d
docker-compose exec -T app php artisan migrate --force
docker-compose exec -T app php artisan config:cache
Kubernetes Deployment
Basic Kubernetes Configuration
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: laravel-app
spec:
replicas: 3
selector:
matchLabels:
app: laravel
template:
metadata:
labels:
app: laravel
spec:
containers:
- name: laravel
image: username/laravel-app:latest
ports:
- containerPort: 80
env:
- name: APP_KEY
valueFrom:
secretKeyRef:
name: laravel-secrets
key: app-key
- name: DB_HOST
value: mysql-service
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 80
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 80
initialDelaySeconds: 10
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: laravel-service
spec:
selector:
app: laravel
ports:
- port: 80
targetPort: 80
type: LoadBalancer
Performance Optimization
1. Image Size Reduction
# Use Alpine instead of full images
FROM php:8.3-fpm-alpine
# Remove build dependencies after use
RUN apk add --no-cache --virtual .build-deps \
autoconf g++ make \
&& pecl install redis \
&& apk del .build-deps
2. Build Caching
# Enable BuildKit
# DOCKER_BUILDKIT=1 docker build .
# Use cache mounts
RUN --mount=type=cache,target=/var/cache/apk \
apk add --update git
3. Resource Limits
# docker-compose.yml
services:
app:
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M
Monitoring and Logging
1. Centralized Logging
services:
app:
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
2. Container Monitoring
# Install cAdvisor
docker run -d \
--name=cadvisor \
--volume=/:/rootfs:ro \
--volume=/var/run:/var/run:ro \
--volume=/sys:/sys:ro \
--volume=/var/lib/docker/:/var/lib/docker:ro \
--publish=8080:8080 \
gcr.io/cadvisor/cadvisor:latest
Conclusion
Dockerizing Laravel 12 applications provides consistent, scalable, and portable deployments. By implementing multi-stage builds, optimizing layers, following security best practices, and integrating with orchestration platforms, you create production-ready containerized applications that scale efficiently.
Key takeaways:
- Use multi-stage builds for optimized production images
- Implement docker-compose for local development
- Follow security best practices and health checks
- Optimize image size and build caching
- Integrate Docker into CI/CD pipelines
- Use orchestration platforms for scaling
- Monitor containers with proper logging
Need help containerizing your Laravel application? NeedLaravelSite specializes in Docker, Kubernetes, and cloud-native deployments. Contact us for expert Laravel development services.
Related Resources: