
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
You need to Dockerize a Laravel app for production multi-stage builds because single-stage images ship gigabytes of build tools, source maps, and dev dependencies into your runtime environment. This bloat increases attack surface, slows CI/CD pipelines, and wastes bandwidth during deploys to regions like Nepal where egress costs matter. The solution is a disciplined multi-stage Dockerfile that separates compilation from execution, producing a minimal, read-only artifact that passes security audits and scales reliably on Kubernetes or bare-metal VPS.
How do you structure a Dockerfile to Dockerize a Laravel app for production multi-stage builds?
The key to a successful production image is isolation. Never run composer install or npm run build in your final stage. These commands pull in hundreds of megabytes of compilers, headers, and debug symbols that have no business existing in a container serving HTTP traffic. When I audit teams' infrastructure for SOC 2 compliance, finding build tools in production images is an immediate flag—it expands the vulnerability surface unnecessarily.
Define explicit stages with named contexts
Name every stage so you can reference them in COPY --from directives without relying on fragile numeric indices. This also makes the Dockerfile self-documenting for new engineers joining your team.
# Stage 1: Install PHP and Node dependencies
FROM composer:2 AS vendor-deps
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --prefer-dist --ignore-platform-reqs
FROM node:20-alpine AS asset-builder
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 Note the --no-scripts flag in Composer. Laravel’s post-install scripts often try to clear caches or generate keys, which fail when the full application context isn’t present yet. Defer these to an entrypoint script at runtime. Similarly, --ignore-platform-reqs prevents failures when building on Alpine but targeting a glibc-based runtime, though matching base OS across stages avoids subtle extension mismatches.
Assemble the final runtime layer
Your final stage should start from the smallest possible base that includes both PHP-FPM and Nginx. The php:8.4-fpm-alpine image is ideal, but you must add Nginx manually since official PHP images don’t bundle it. Alternatively, use community-maintained images like webdevops/php-nginx, but verify their update cadence before adopting in production.
FROM php:8.4-fpm-alpine AS runtime
RUN apk add --no-cache nginx shadow && \
docker-php-ext-install pdo_mysql opcache bcmath && \
usermod -u 1000 www-data && \
mkdir -p /var/www/html/storage/framework/{sessions,views,cache} && \
chown -R www-data:www-data /var/www/html
WORKDIR /var/www/html
COPY --from=vendor-deps /app/vendor ./vendor
COPY --from=asset-builder /app/public/build ./public/build
COPY . /var/www/html
COPY docker/nginx.conf /etc/nginx/http.d/default.conf
COPY docker/php.ini /usr/local/etc/php/conf.d/99-production.ini
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
USER www-data
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s CMD curl -f http://localhost:8080/up || exit 1
ENTRYPOINT ["entrypoint.sh"] This configuration runs as www-data (UID 1000), exposes port 8080 (non-privileged), and includes a health check endpoint. The /up route should be defined in your Laravel app to return 200 without database dependencies, enabling safe load balancer probes during deployments. For deeper guidance on securing this setup, see my article on Ubuntu security hardening, which covers filesystem permissions applicable inside containers too.
Why does image size matter when you Dockerize a Laravel app for production multi-stage builds?
Image size directly impacts deploy velocity, cold start latency, and storage costs. In environments with limited bandwidth—common in South Asian data centers or edge locations—a 1GB image takes minutes to pull versus seconds for a 200MB one. During rolling updates on Kubernetes, large images cause prolonged transition windows where old and new pods coexist, increasing memory pressure and risk of OOM kills.
| Metric | Single-Stage Image | Multi-Stage Optimized | Impact |
|---|---|---|---|
| Total Size | 1.1 – 1.4 GB | 160 – 220 MB | 85% reduction in registry storage & egress fees |
| Pull Time (100 Mbps) | ~90 seconds | ~15 seconds | Faster autoscaling & recovery |
| CVE Count (Trivy High+) | 45 – 120 | 3 – 12 | Simpler compliance evidence collection |
| Attack Surface | GCC, Node, Python, Git | PHP-FPM, Nginx, libc | Fewer exploit paths for RCE |
Beyond raw size, layer caching efficiency matters. By isolating dependency installation from source code copying, unchanged composer.lock files reuse cached layers even when application code changes. This cuts CI build times from 4+ minutes to under 30 seconds for typical commits. Always order instructions from least-frequent to most-frequent change: OS packages → extensions → Composer deps → NPM deps → application source.
How do you handle secrets and environment variables securely in production containers?
Never bake .env files into your Docker image. Secrets embedded in layers persist forever in registry history, even after deletion. Instead, inject configuration at runtime via orchestrator-native mechanisms: Kubernetes Secrets mounted as env vars or files, AWS Parameter Store fetched by entrypoint scripts, or HashiCorp Vault agent sidecars. Your Dockerfile should contain zero sensitive values.
Your entrypoint script should validate required variables exist before starting services. Fail fast with descriptive errors rather than letting Laravel boot with null DB connections. Also run php artisan config:cache and route:cache here—not in the Dockerfile—since cached configs embed resolved env values and break if reused across environments. For teams managing multiple databases, refer to PostgreSQL administration essentials for connection pooling patterns compatible with containerized PHP apps.
What runtime optimizations are critical after you Dockerize a Laravel app for production multi-stage builds?
A correctly built image is necessary but insufficient. You must tune PHP-FPM, Nginx, and Laravel itself for container constraints. Default configurations assume bare-metal resources and will bottleneck under load or waste memory in auto-scaled environments.
- PHP-FPM Process Manager: Set
pm = dynamicwithpm.max_childrencalculated as (container_memory_limit - 50MB) / average_process_size. For a 512MB limit with 40MB workers, cap at 11 children. Usepm.start_servers = pm.min_spare_servers = 2to reduce cold-start latency without over-provisioning. - Opcache Configuration: Enable
opcache.enable_cli=1for queue workers and schedulers. Setopcache.memory_consumption=128andopcache.interned_strings_buffer=16. Crucially, disableopcache.validate_timestampsin production—your filesystem is immutable, so revalidation wastes CPU cycles. - Nginx Buffer Tuning: Match
fastcgi_buffer_sizeandfastcgi_buffersto your typical response size. Laravel API responses average 8–16KB; setfastcgi_buffer_size 16kandfastcgi_buffers 4 16k. Oversized buffers consume RAM per connection; undersized ones trigger disk buffering and latency spikes. - Queue Worker Isolation: Run queues in separate containers or processes, not alongside FPM. Use
php artisan queue:work --daemonwith--max-jobs=1000to prevent memory leaks. Scale workers independently based on queue depth metrics, not HTTP traffic.
Monitoring is non-negotiable. Instrument your container with Prometheus exporters for PHP-FPM and Nginx metrics. Track fpm_active_processes, fpm_listen_queue_len, and nginx_http_requests_total to detect saturation before users experience errors. My guide on Prometheus metrics monitoring fundamentals details which signals predict Laravel container failures.
How does multi-stage compare to single-stage for Laravel production deployments?
Some teams still use single-stage builds for simplicity, arguing that disk space is cheap. This ignores operational realities beyond storage costs. Below is a direct comparison based on production incidents I’ve resolved in 2025–2026.
The multi-stage approach requires upfront investment in Dockerfile design and entrypoint scripting, but pays dividends immediately upon first production incident. Rollback speed alone justifies the effort: when a bad deploy causes P0 errors, waiting 90 seconds per node for image pulls extends outage duration unacceptably. In regulated environments, the reduced CVE count simplifies audit evidence collection and shortens review cycles.
Production Readiness Checklist for Containerized Laravel
Before shipping your multi-stage Laravel image, verify these items. Missing any one has caused outages in systems I’ve inherited or audited.
- Read-only root filesystem: Mount
/tmpandstorage/as writable volumes; setreadOnlyRootFilesystem: truein Kubernetes PodSecurityContext. Prevents attackers from writing webshells. - Graceful shutdown handling: Configure PHP-FPM
process_control_timeout=10and Nginxworker_shutdown_timeout 10s. Ensure entrypoint traps SIGTERM and waits for in-flight requests. - Structured logging: Output JSON logs to stdout/stderr. Never write to files inside the container. Aggregate externally via Fluent Bit or Vector.
- Resource limits: Set CPU/memory requests and limits in orchestration config. Test with load to avoid throttling or OOMKills during peak.
- Dependency pinning: Lock PHP, Nginx, and Alpine versions explicitly. Avoid
latesttags. Rebuild monthly to incorporate security patches.
When you properly Dockerize a Laravel app for production multi-stage builds, you gain more than smaller images—you establish a foundation for secure, observable, and compliant operations. If your team needs help implementing this pattern or passing a security audit with containerized PHP workloads, reach out to discuss your specific architecture.