DevOps & Deployment

Deploying Laravel 12 with Docker: A Complete Step-by-Step Guide

Deploy Laravel 12 applications with Docker. Complete guide covering Dockerfile, docker-compose, multi-stage builds, production optimization, and container orchestration.

Muhammad Waqas

Muhammad Waqas

CEO at CentoSquare

|
03-Nov-2025
10 min read
Deploying Laravel 12 with Docker: A Complete Step-by-Step Guide

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:


Article Tags

Laravel Docker Laravel 12 Docker Docker Laravel deployment Laravel docker-compose Laravel containerization Docker Laravel tutorial Laravel Docker production Laravel multi-stage build Laravel Docker Nginx Laravel Docker MySQL Laravel Docker Redis Docker Laravel best practices

About the Author

Muhammad Waqas

Muhammad Waqas

CEO at CentoSquare

Founder & CEO at CentoSquare | Creator of NeedLaravelSite | Helping Businesses Grow with Cutting-Edge Web, Mobile & Marketing Solutions | Building Innovative Products