
August 14, 2026
11 min read
Table of Contents
By Kokil Thapa | Last reviewed: August 2026
If you need to ship reliable PHP applications in 2026, you must know how to dockerize a Laravel app for production multi-stage builds. Single-stage Dockerfiles create bloated, insecure images that slow deployments and increase attack surfaces. Multi-stage builds solve this by separating build-time dependencies from runtime artifacts, producing lean containers under 150MB that start in seconds. This approach is now standard practice for any serious Laravel developer deploying to Kubernetes, ECS, or traditional VPS infrastructure.
Why Should You Dockerize a Laravel App for Production Multi-Stage Builds?
Multi-stage builds are not optional for production Laravel deployments—they are the baseline requirement. When you dockerize a Laravel app for production multi-stage builds correctly, you eliminate three critical problems that plague single-stage approaches.
First, image size drops dramatically. A naive Laravel Dockerfile often exceeds 800MB because it includes Node.js, npm cache, Composer cache, and development tools. Multi-stage builds discard these after extracting only compiled assets and vendor dependencies. I regularly see production images between 90MB and 140MB using Alpine-based PHP-FPM. Smaller images mean faster CI/CD pipelines, reduced registry storage costs, and quicker horizontal scaling during traffic spikes.
Second, security posture improves substantially. Build tools like npm, git, and compilers contain known vulnerabilities. By excluding them from the final image, you reduce the CVE surface area. Security scanners consistently report fewer findings on multi-stage Laravel images. For legal-tech portals handling sensitive client documents, this reduction matters for compliance and trust.
Third, reproducibility becomes guaranteed. Every build starts from identical base images with pinned versions. The "works on my machine" problem disappears because the container encapsulates the exact PHP extensions, system libraries, and configuration your application requires. On projects like Nepal Gift Card and Adventure Third Pole Trek, this consistency eliminated an entire category of deployment failures caused by environment drift.
How Do You Write an Optimized Multi-Stage Dockerfile for Laravel 12?
The Dockerfile structure determines everything about your production image quality. Below is a battle-tested template I use across client projects running Laravel 11 and 12 on PHP 8.3 and 8.4. Each stage has a specific purpose, and layer ordering maximizes Docker cache efficiency.
Complete Production Dockerfile
# syntax=docker/dockerfile:1.7
FROM composer:2.7 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist
FROM node:22-alpine AS frontend
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --ignore-scripts
COPY resources ./resources
COPY vite.config.js tailwind.config.js postcss.config.js ./
RUN npm run build
FROM php:8.4-fpm-alpine AS base
RUN apk add --no-cache \
nginx \
supervisor \
libpng-dev \
libjpeg-turbo-dev \
freetype-dev \
oniguruma-dev \
libxml2-dev \
zip \
unzip \
git && \
docker-php-ext-configure gd --with-freetype --with-jpeg && \
docker-php-ext-install -j$(nproc) \
pdo_mysql \
mbstring \
xml \
opcache \
bcmath \
pcntl \
gd && \
apk del .build-deps && \
rm -rf /var/cache/apk/*
COPY --from=vendor /app/vendor /app/vendor
COPY --from=frontend /app/public/build /app/public/build
WORKDIR /app
COPY . .
RUN cp .env.example .env && \
php artisan optimize && \
php artisan config:cache && \
php artisan route:cache && \
php artisan view:cache && \
chown -R www-data:www-data /app/storage /app/bootstrap/cache && \
chmod -R 775 /app/storage /app/bootstrap/cache
COPY docker/nginx.conf /etc/nginx/http.d/default.conf
COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY docker/php.ini /usr/local/etc/php/conf.d/99-production.ini
EXPOSE 8080
USER www-data
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] Critical Design Decisions Explained
- Dependency stages first: Composer and npm stages run before copying application code. Changing your controllers does not invalidate the vendor layer cache. This alone saves 2-4 minutes per rebuild during active development.
- --no-dev flag: Never install PHPUnit, Faker, or debugging packages in production. These add 30-50MB and introduce unnecessary code paths.
- Alpine base: php-fpm-alpine produces images roughly 60% smaller than Debian-based equivalents. The tradeoff is musl libc instead of glibc, which rarely affects Laravel applications but test thoroughly if you use binary extensions.
- Optimization at build time: Running
php artisan optimize,config:cache,route:cache, andview:cacheduring build means every container starts pre-warmed. Avoid running these in entrypoint scripts—they execute on every container start and waste precious seconds during scaling events. - Non-root user: Running as www-data prevents privilege escalation attacks. Ensure storage/ and bootstrap/cache/ ownership is set before switching users.
This pattern scales well whether you are building a simple brochure site or a complex platform like high-performance Laravel e-commerce stores. The key discipline is treating the Dockerfile as infrastructure code that deserves the same attention as your application logic.
What Is the Best Nginx and PHP-FPM Configuration for Containerized Laravel?
Embedding Nginx and PHP-FPM in a single container simplifies deployment for small-to-medium applications. For larger systems, separate services offer better scaling granularity, but the embedded approach works excellently for most Laravel projects I ship.
Nginx Configuration for Laravel Containers
server {
listen 8080 default_server;
root /app/public;
index index.php;
charset utf-8;
client_max_body_size 64M;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php/php-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_buffering on;
fastcgi_buffer_size 16k;
fastcgi_buffers 16 16k;
fastcgi_read_timeout 300;
}
location ~ /\.(?!well-known).* {
deny all;
}
access_log off;
error_log /dev/stderr warn;
} Supervisor Process Management
[supervisord]
nodaemon=true
user=root
logfile=/dev/null
pidfile=/var/run/supervisord.pid
[program:php-fpm]
command=/usr/local/sbin/php-fpm --nodaemonize
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:nginx]
command=/usr/sbin/nginx -g "daemon off;"
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0 Logging to stdout/stderr is mandatory. Container orchestrators capture standard streams; writing to files inside containers creates orphaned logs that vanish when the pod terminates. For deeper insight into structuring backend services properly, review building REST APIs in Laravel the right way, as API-first architectures pair naturally with containerized deployments.
How Do You Handle Environment Variables and Secrets in Dockerized Laravel?
Secret management causes more production incidents than almost any other Docker topic. Never bake secrets into images. The .env file copied during build should contain only non-sensitive defaults sufficient for artisan optimize to succeed. Real credentials come from the orchestration layer at runtime.
Runtime Environment Injection Pattern
- Build-time .env: Include APP_NAME, APP_ENV=production, CACHE_DRIVER=file, SESSION_DRIVER=file. Exclude database passwords, API keys, mail credentials.
- Runtime override: Pass secrets via Docker environment variables, Kubernetes Secrets, AWS Parameter Store, or HashiCorp Vault. Laravel's env() helper reads process environment before falling back to .env values.
- Config caching caveat: If you run
config:cacheduring build, env() calls outside config/ files break. Either skip config caching (acceptable for most apps) or ensure all env() usage lives in config/ files where it gets resolved at cache-generation time. - Entrypoint flexibility: For advanced setups, use an entrypoint script that generates .env from injected secrets before starting Supervisor. This supports platforms that cannot inject individual environment variables easily.
# docker-compose.prod.yml snippet
services:
laravel:
image: registry.example.com/myapp:v2.4.1
environment:
- DB_HOST=mysql.internal
- DB_PASSWORD=${DB_PASSWORD}
- REDIS_HOST=redis.internal
- MAIL_MAILER=smtp
- MAIL_PASSWORD=${MAIL_PASSWORD}
volumes:
- laravel-storage:/app/storage
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/up"]
interval: 30s
timeout: 5s
retries: 3 This separation ensures compromised images do not leak credentials and allows the same image artifact to serve staging, production, and disaster-recovery environments with different configurations. For teams managing multiple client projects, this pattern reduces operational overhead significantly compared to maintaining separate builds per environment.
What Are Common Mistakes When Dockerizing Laravel Applications?
After reviewing dozens of Laravel Dockerfiles from teams across Nepal and internationally, certain anti-patterns appear repeatedly. Avoiding these saves hours of debugging and prevents production incidents.
| Mistake | Impact | Correct Approach |
|---|---|---|
| Running as root user | Privilege escalation risk; file permission conflicts on mounted volumes | Add USER www-data after setting ownership of storage/ and bootstrap/cache/ |
| Installing dev dependencies | +40MB image size; unnecessary attack surface; slower autoloader | Always use composer install --no-dev in vendor stage |
| Skipping PHP extension verification | Runtime crashes when missing extensions are called | Run php -m in CI pipeline; fail build if required extensions absent |
| Using COPY . . without .dockerignore | .git/, tests/, node_modules/ included; cache invalidation on every commit | Create comprehensive .dockerignore excluding non-runtime files |
| Running migrations in entrypoint | Race conditions during rolling deploys; failed containers block startup | Run migrations as separate CI/job step before deploying new image tag |
| Hardcoding ports or paths | Cannot reuse image across environments; brittle configuration | Use environment variables for configurable values; sensible defaults only |
The migration mistake deserves special emphasis. Running php artisan migrate --force in an entrypoint script seems convenient but creates dangerous race conditions during zero-downtime deployments. Two containers might attempt schema changes simultaneously, or a new container might start before migrations complete and serve errors. Treat migrations as a discrete deployment phase, not a container startup task. This aligns with how mature teams handle modern Laravel architecture best practices where deployment concerns are separated from application runtime.
Deploying Your Dockerized Laravel Application Successfully
When you dockerize a Laravel app for production multi-stage builds, the final validation step matters as much as the Dockerfile itself. Before promoting any image to production, verify these checkpoints:
- Image size audit: Run
docker imagesand confirm the final tag is under 200MB. If larger, inspect layers withdiveto find bloat. - Security scan: Integrate Trivy or Grype into CI. Block merges on HIGH or CRITICAL findings in the final stage.
- Health endpoint: Implement a dedicated
/uproute that checks database connectivity and returns 200. Configure orchestrator health checks against this endpoint, not the homepage. - Log format: Confirm structured JSON logging is active. Unstructured logs make production debugging painful at scale.
- Permission test: Exec into a running container as www-data and verify write access to storage/logs, storage/framework/cache, and storage/framework/sessions.
For teams transitioning from traditional VPS deployments, the learning curve is real but manageable. Start with Docker Compose for staging, validate the workflow, then graduate to ECS, Cloud Run, or Kubernetes for production. The investment pays compounding returns through faster deployments, consistent environments, and simplified onboarding for new developers joining your team.
If you are planning to dockerize a Laravel app for production multi-stage builds and want hands-on guidance tailored to your infrastructure, reach out to discuss your deployment architecture. I help teams across Nepal and globally modernize their Laravel hosting with practical, maintainable container strategies that respect budget and operational reality.

