• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 12+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » Beyond the Basics: Mastering Multi-Stage Docker Builds for Optimized Laravel Deployments with CI/CD Integration

Beyond the Basics: Mastering Multi-Stage Docker Builds for Optimized Laravel Deployments with CI/CD Integration

Optimizing Laravel Deployments with Multi-Stage Docker Builds

For modern Laravel applications, Docker has become an indispensable tool for consistent development and deployment environments. However, a naive Dockerfile can lead to bloated images, increasing build times, security vulnerabilities, and deployment overhead. Multi-stage builds are the architectural solution to this problem, allowing us to separate build-time dependencies from runtime requirements, resulting in lean, efficient production images.

The Problem with Single-Stage Builds

Consider a typical single-stage Dockerfile for a Laravel application. It often includes installing development tools, compiling assets, and then copying the application code. This approach pollutes the final image with tools like Node.js, npm/yarn, PHP development extensions, and build artifacts that are unnecessary for running the application in production.

Here’s a simplified example of a less-than-ideal single-stage Dockerfile:

# Dockerfile (Single-Stage - Not Recommended)
FROM php:8.2-fpm

# Install dependencies
RUN apt-get update && apt-get install -y \
    git \
    curl \
    libzip-dev \
    unzip \
    libpng-dev \
    libjpeg-dev \
    libfreetype6-dev \
    libonig-dev \
    libxml2-dev \
    zip \
    && docker-php-ext-configure gd --with-freetype --with-jpeg \
    && docker-php-ext-install -j$(nproc) gd \
    && docker-php-ext-install zip \
    && docker-php-ext-install pdo_mysql \
    && pecl install redis \
    && docker-php-ext-enable redis \
    && apt-get clean && rm -rf /var/lib/apt/lists/*

# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

# Install Node.js and npm for asset compilation
RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
    && apt-get install -y nodejs \
    && npm install -g npm@latest

# Set working directory
WORKDIR /var/www/html

# Copy application code
COPY . .

# Install PHP dependencies
RUN composer install --no-dev --optimize-autoloader

# Install Node.js dependencies and compile assets
RUN npm install && npm run build

# Permissions
RUN chown -R www-data:www-data storage bootstrap/cache && chmod -R 775 storage bootstrap/cache

# Expose port
EXPOSE 9000

# Default command
CMD ["php-fpm"]

This Dockerfile installs PHP, Composer, Node.js, npm, and all necessary build tools. The final image will contain all these components, even though Node.js and npm are only needed for the `npm run build` step. This leads to a significantly larger image than necessary.

Architecting with Multi-Stage Builds

Multi-stage builds leverage multiple `FROM` instructions in a single Dockerfile. Each `FROM` instruction begins a new build stage. You can selectively copy artifacts from one stage to another, discarding everything else. This is perfect for separating the build environment from the runtime environment.

Stage 1: The Builder Stage

This stage will be responsible for compiling assets and installing PHP dependencies. We’ll use a base image with all the necessary build tools, including Node.js and Composer.

# Dockerfile (Multi-Stage)

# Stage 1: Builder
FROM php:8.2-fpm AS builder

# Install build dependencies
RUN apt-get update && apt-get install -y \
    git \
    curl \
    libzip-dev \
    unzip \
    libpng-dev \
    libjpeg-dev \
    libfreetype6-dev \
    libonig-dev \
    libxml2-dev \
    zip \
    && docker-php-ext-configure gd --with-freetype --with-jpeg \
    && docker-php-ext-install -j$(nproc) gd \
    && docker-php-ext-install zip \
    && docker-php-ext-install pdo_mysql \
    && pecl install redis \
    && docker-php-ext-enable redis \
    && apt-get clean && rm -rf /var/lib/apt/lists/*

# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

# Install Node.js and npm
RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
    && apt-get install -y nodejs \
    && npm install -g npm@latest

# Set working directory
WORKDIR /var/www/html

# Copy application code (including composer.json and package.json)
COPY . .

# Install PHP dependencies
RUN composer install --no-dev --optimize-autoloader --no-interaction --prefer-dist

# Install Node.js dependencies and compile assets
RUN npm install && npm run build

Stage 2: The Production Stage

This stage will use a minimal PHP-FPM image. We’ll copy only the necessary compiled assets and the application code from the `builder` stage. Crucially, we will *not* install Node.js or any development dependencies here.

# Dockerfile (Multi-Stage) - Continued

# Stage 2: Production
FROM php:8.2-fpm-alpine AS production

# Install runtime dependencies
RUN apk add --no-cache \
    libzip \
    libpng \
    libjpeg-turbo \
    freetype \
    oniguruma \
    libxml2 \
    zip \
    && docker-php-ext-configure gd --with-freetype --with-jpeg \
    && docker-php-ext-install -j$(nproc) gd \
    && docker-php-ext-install zip \
    && docker-php-ext-install pdo_mysql \
    && pecl install redis \
    && docker-php-ext-enable redis

# Set working directory
WORKDIR /var/www/html

# Copy compiled assets from the builder stage
COPY --from=builder /var/www/html/public/build /var/www/html/public/build
COPY --from=builder /var/www/html/storage /var/www/html/storage
COPY --from=builder /var/www/html/bootstrap/cache /var/www/html/bootstrap/cache

# Copy vendor directory from the builder stage
COPY --from=builder /var/www/html/vendor /var/www/html/vendor

# Copy application code (excluding dev dependencies and build tools)
# This is a more granular copy to ensure only necessary files are included.
# Alternatively, you could copy the entire /var/www/html from builder,
# but this is less explicit and might include unintended files.
COPY --from=builder /var/www/html/.env.example /var/www/html/.env.example
COPY --from=builder /var/www/html/artisan /var/www/html/artisan
COPY --from=builder /var/www/html/composer.json /var/www/html/composer.json
COPY --from=builder /var/www/html/package.json /var/www/html/package.json
# ... copy other essential root files as needed ...

# Ensure correct permissions for runtime
RUN chown -R www-data:www-data storage bootstrap/cache && chmod -R 775 storage bootstrap/cache

# Expose port
EXPOSE 9000

# Default command
CMD ["php-fpm"]

Notice the use of `FROM php:8.2-fpm-alpine`. Alpine Linux images are significantly smaller than Debian-based images, further reducing the final image size. We also use `apk add` for package management, which is the standard for Alpine. The critical part is `COPY –from=builder …`, which selectively brings over only the compiled assets and dependencies.

Integrating with CI/CD Pipelines

Multi-stage builds are a perfect fit for CI/CD. Your CI pipeline can build the Docker image, and the resulting artifact will be small and production-ready. Here’s a conceptual example using GitLab CI:

# .gitlab-ci.yml

variables:
  DOCKER_REGISTRY: registry.gitlab.com
  IMAGE_NAME: $CI_PROJECT_PATH/$CI_COMMIT_REF_SLUG
  IMAGE_TAG: $CI_COMMIT_SHA

stages:
  - build
  - deploy

build_docker_image:
  stage: build
  image: docker:20.10.16
  services:
    - docker:20.10.16-dind
  script:
    - echo "Logging into Docker registry..."
    - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY
    - echo "Building Docker image..."
    - docker build --target production -t $DOCKER_REGISTRY/$IMAGE_NAME:$IMAGE_TAG .
    - echo "Pushing Docker image..."
    - docker push $DOCKER_REGISTRY/$IMAGE_NAME:$IMAGE_TAG
  only:
    - main # Or your production branch

deploy_to_production:
  stage: deploy
  script:
    - echo "Deploying image $DOCKER_REGISTRY/$IMAGE_NAME:$IMAGE_TAG to production..."
    # Your deployment commands here (e.g., kubectl apply, docker-compose up, etc.)
    # Example: Update Kubernetes deployment
    # - kubectl set image deployment/my-laravel-app my-laravel-app=$DOCKER_REGISTRY/$IMAGE_NAME:$IMAGE_TAG
  environment:
    name: production
    url: https://your-production-url.com
  when: on_success
  only:
    - main # Or your production branch
  needs:
    - build_docker_image

Key points in this CI configuration:

  • We use the official `docker:dind` service to run Docker within Docker.
  • The `docker build –target production` command is crucial. It tells Docker to only build up to the stage named `production`, effectively discarding all intermediate stages and their artifacts.
  • The resulting image is tagged and pushed to a container registry.
  • The deployment stage then uses this lean, production-ready image.

Further Optimizations and Considerations

Caching Strategies

Docker’s build cache can significantly speed up subsequent builds. Ensure your Dockerfile is structured to take advantage of this:

  • Copy `composer.json` and `package.json` first, then run `composer install` and `npm install`. If these files haven’t changed, Docker will use the cached layer, avoiding re-downloading dependencies.
  • Copy the rest of your application code last.

In the multi-stage example, we copy `composer.json` and `package.json` in the `builder` stage, then run `composer install` and `npm install`. This cache layer is then leveraged when copying from the `builder` stage to the `production` stage, as long as the `vendor` directory and compiled assets are also copied correctly.

Environment Variables

Avoid hardcoding environment-specific configurations in your Dockerfile. Use `.env` files or Docker secrets for sensitive information. For non-sensitive configurations, you can use `ARG` and `ENV` instructions, but be mindful of what’s baked into the image.

# Example using ARG for build-time configuration
ARG APP_ENV=production
ENV APP_ENV=${APP_ENV}

# Example for runtime configuration (often handled by orchestration)
ENV APP_URL=http://localhost

Security Best Practices

Always use specific image tags (e.g., `php:8.2-fpm-alpine`) instead of `latest` to ensure reproducible builds. Regularly scan your images for vulnerabilities. Running as a non-root user (`www-data` in our example) is also a critical security measure.

Nginx Integration

For a complete deployment, you’ll typically pair your PHP-FPM container with an Nginx container. The Nginx container would serve static assets directly and proxy API requests to the PHP-FPM container. Here’s a minimal Nginx configuration:

# nginx.conf
server {
    listen 80;
    server_name your-domain.com;

    root /var/www/html/public;
    index index.php index.html index.htm;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass php-fpm:9000; # Assuming php-fpm container is named 'php-fpm'
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    # Serve static assets directly from storage/app/public if mounted
    location /storage/ {
        alias /var/www/html/storage/app/public/;
        expires 30d;
        add_header Cache-Control "public";
    }

    # Prevent access to hidden files
    location ~ /\. {
        deny all;
    }
}

This Nginx configuration assumes your PHP-FPM container is accessible via the service name `php-fpm` on port `9000`. The `try_files` directive is essential for Laravel’s routing.

Conclusion

Mastering multi-stage Docker builds is a fundamental step towards building efficient, secure, and maintainable Laravel applications. By separating build-time concerns from runtime requirements, you significantly reduce image size, improve build times, and enhance your deployment pipeline’s robustness. This architectural pattern is not just a best practice; it’s a necessity for any serious production deployment.

Primary Sidebar

A little about the Author

Having 12+ Years of Experience in Software Development, Vinay is a principal software architect, senior systems engineer, and elite technical consultant. He specializes in bespoke PHP/WordPress development, high-performance Magento 2 & Shopify architectures, custom plugin/theme development from scratch, and legacy code modernization (including VB6, VB.NET, PyQt, and Crystal Reports). Known for solving complex database bottlenecks, speed optimization (Core Web Vitals), and advanced security code auditing, Vinay engineers production-ready systems designed to scale under heavy concurrent load conditions.



Chat on WhatsApp

Recent Posts

  • From Monolith to Microservices: A Deep Dive into Laravel’s Evolution with Docker Swarm and AWS Lambda for Scalable PHP Applications
  • Leveraging PHP 8.3 JIT and Laravel Octane for Blazing-Fast Microservices: A Performance Deep Dive
  • Kubernetes-Native PHP 8+ Deployments: Orchestrating Microservices with Laravel Octane and Docker Swarm
  • Beyond the Basics: Mastering Multi-Stage Docker Builds for Optimized Laravel Deployments with CI/CD Integration
  • Orchestrating Microservices with Laravel, Docker, and AWS ECS: A High-Availability Architecture for Modern Web Applications

Categories

  • apache (1)
  • Business & Monetization (390)
  • Centos (4)
  • Comparisons & Decision Making (55)
  • Debian (2)
  • Debugging & Troubleshooting (664)
  • Desktop Applications (14)
  • DevOps (9)
  • DevOps & Cloud Scaling (962)
  • Django (1)
  • Laravel (6)
  • Migration & Architecture (192)
  • Mobile Applications (24)
  • MySQL (1)
  • Performance & Optimization (873)
  • PHP (7)
  • PHP Development (49)
  • Plugins & Themes (244)
  • Programming Languages (10)
  • Python (20)
  • Ruby on Rails (1)
  • Security & Compliance (650)
  • SEO & Growth (492)
  • Server (118)
  • Softwares (1)
  • Ubuntu (9)
  • Uncategorized (6)
  • VB6 & VB.NET (8)
  • Web Applications & Frontend (19)
  • Web Assembly (Wasm) (2)
  • WordPress (22)
  • WordPress Plugin Development (728)
  • WordPress Theme Development (357)

Recent Posts

  • From Monolith to Microservices: A Deep Dive into Laravel's Evolution with Docker Swarm and AWS Lambda for Scalable PHP Applications
  • Leveraging PHP 8.3 JIT and Laravel Octane for Blazing-Fast Microservices: A Performance Deep Dive
  • Kubernetes-Native PHP 8+ Deployments: Orchestrating Microservices with Laravel Octane and Docker Swarm

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (873)
  • WordPress Plugin Development (728)
  • Debugging & Troubleshooting (664)
  • Security & Compliance (650)
  • SEO & Growth (492)

Our Products

  • ERP & LMS Systems (4)
  • Directories & Marketplaces (4)
  • Healthcare Portals (3)
  • Point of Sale (POS) (2)
  • E-Commerce Engines (2)

Our Services

  • E-Commerce Development (10)
  • WordPress Development (8)
  • Python & Desktop GUI (7)
  • General Consulting (7)
  • Legacy Modernization (5)
  • Mobile App Development (4)

Copyright © 2026 · Vinay Vengala