
September 09, 2026
12 min read
By Kokil Thapa | Last reviewed: September 2026
Large Docker images slow every pull, inflate registry bills, and widen your attack surface. If you want to know how to reduce Docker image size with multi-stage builds, the fix is structural: separate compile-time dependencies from what actually runs in production. On a Laravel production Docker setup, I routinely see images drop from 900 MB to under 180 MB once build tools and dev packages stay in earlier stages only.
What Is a Docker Multi-Stage Build and Why Does It Shrink Images?
A multi-stage Dockerfile defines several FROM blocks in one file. Each block is a stage with its own filesystem. Only artifacts you explicitly COPY --from= reach the final image. Everything else is thrown away when that stage finishes.
Single-stage images keep the damage permanently. You install GCC, Node.js, and Composer dev tools. They remain in the layer stack forever. Multi-stage builds break that pattern. Build fat, ship thin.
This matters on real projects. A Nepal-based team deploying to a single EC2 instance pays for every megabyte on every release. Smaller images mean faster GitLab CI pulls and quicker rollbacks during incidents.
The official Docker documentation describes this pattern as the standard way to keep production images minimal while still running complex build steps. The same idea applies whether you ship Laravel 13 on PHP 8.5 or a static frontend built with Vite 8.x.
How Do You Write a Multi-Stage Dockerfile for a Laravel Application?
Laravel apps are a textbook case. You need Node.js 26 LTS to compile frontend assets. You need Composer 2.10 for PHP packages. Neither belongs in the container that runs PHP-FPM behind Apache or Nginx in production.
Step 1: Name your stages explicitly
Named stages make COPY --from= readable and prevent copy-paste errors during refactors. Use names like frontend, vendor, and runtime.
Step 2: Build assets in an isolated Node stage
Install npm 12 dependencies, run npm run build, and copy only the public/build directory forward. The entire Node runtime stays behind.
Step 3: Install Composer dependencies without dev packages
Run composer install --no-dev --optimize-autoloader in a PHP builder stage. Copy the vendor/ tree into runtime. Skip PHPUnit, PHPStan, and other dev-only tools.
Step 4: Assemble a slim PHP-FPM runtime
Start from php:8.5-fpm-alpine or a distroless PHP base. Copy application code, vendor, and compiled assets. Install only runtime extensions such as pdo_mysql and opcache.
# syntax=docker/dockerfile:1
FROM node:26-alpine AS frontend
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY resources/ resources/
COPY vite.config.js ./
RUN npm run build
FROM composer:2.10 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install \
--no-dev \
--no-interaction \
--prefer-dist \
--optimize-autoloader
FROM php:8.5-fpm-alpine AS runtime
RUN apk add --no-cache \
libpng-dev libzip-dev oniguruma-dev \
&& docker-php-ext-install pdo_mysql zip opcache
WORKDIR /var/www/html
COPY --from=vendor /app/vendor ./vendor
COPY --from=frontend /app/public/build ./public/build
COPY . .
RUN chown -R www-data:www-data storage bootstrap/cache
USER www-data
EXPOSE 9000
CMD ["php-fpm"] On production Laravel applications I maintain, committing built assets and skipping Node on the server is another valid pattern. Multi-stage builds still help when CI must produce the image from a clean Git checkout every time. See the companion guide on build pipeline automation best practices for wiring this into GitLab CI.
Which Base Image Choices Make the Biggest Size Difference?
Your final FROM line often matters more than clever layer ordering. A full Debian-based PHP image can weigh 400 MB before your app code arrives. Alpine or distroless bases cut that dramatically.
| Base image type | Typical size | Best for | Trade-offs |
|---|---|---|---|
php:8.5-fpm (Debian) | 350–450 MB | Quick prototypes, broad extension support | Large; includes apt tooling |
php:8.5-fpm-alpine | 80–120 MB | Laravel, Symfony, API services | musl libc; some PECL extensions harder |
| Distroless / scratch + static binary | 20–60 MB | Go, Rust, minimal PHP via static builds | No shell; harder to debug |
| Single-stage with build tools | 800 MB–1.2 GB | Local dev only | Never ship to production |
For most PHP workloads in 2026, Alpine plus multi-stage copy is the sweet spot. If you need deeper hardening after slimming, read about distroless images for security and decide whether the operational cost is worth it for your team size.
What Layer and Cache Tricks Further Reduce Docker Image Size?
Multi-stage builds remove whole toolchains. Layer hygiene removes waste inside each stage. Both together produce images that pull in seconds on a Kathmandu office connection or a constrained CI runner.
Order Dockerfile instructions from least to most volatile
Copy dependency manifests first. Run install commands. Copy application source last. Docker reuses cached layers when lock files do not change. That speeds builds and avoids accidental re-downloads.
Combine RUN instructions and clean package caches in the same layer
Every RUN apt-get install without cleanup in the same layer leaves cache files behind forever. Use one RUN with install and purge chained together.
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
git unzip libpng-dev \
&& docker-php-ext-install gd \
&& apt-get purge -y git \
&& apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/* Use .dockerignore aggressively
Exclude node_modules, vendor, .git, test suites, and local .env files from the build context. Smaller context means faster uploads to the daemon and fewer accidental layer bloat entries.
.git
node_modules
vendor
tests
storage/logs
.env
*.md
docker-compose*.yml Do not copy files you do not need at runtime
Skip README files, CI configs, and frontend source if only compiled assets matter. Use explicit COPY lists instead of COPY . . when the app layout is stable.
- Audit the final image with
docker history --no-trunc IMAGE. - Inspect layer contents with
dive IMAGEordocker exportplustar. - Remove debug symbols and source maps from production asset copies.
- Pin base image digests for reproducible builds across environments.
- Scan the slim image with Trivy before tagging a release candidate.
Container scanning catches vulnerabilities that slimming exposes rather than fixes. Pair size work with the guide on container image scanning with Trivy before you promote images to production.
How Do You Verify and Deploy Slim Multi-Stage Images in Production?
Building a small image is half the job. You still need confidence that the runtime stage behaves identically to what passed QA. Treat verification as part of the Dockerfile design, not an afterthought.
Compare image sizes before and after
docker build -t myapp:multi -f Dockerfile .
docker images myapp:multi --format "{{.Repository}}:{{.Tag}} {{.Size}}"
docker build -t myapp:single -f Dockerfile.single .
docker images myapp:single --format "{{.Repository}}:{{.Tag}} {{.Size}}" Log both numbers in your CI artifact summary. Teams notice regressions fast when the metric is visible on every merge request.
Smoke-test the runtime stage, not the builder
Run health checks against the final tagged image. Confirm PHP extensions load, database connections work, and queue workers start. A missing ext-intl or ext-redis extension often surfaces only at this step.
Tag and promote deliberately
Use immutable tags for releases and mutable tags like latest only for dev environments. Consistent tagging prevents rollback confusion when a slim image replaces a bloated one mid-incident. The article on Docker image tagging strategies covers patterns that work well with GitLab CI and Deployer-style deploys.
Several sister sites I operate share a Deployer 7 plus GitLab CI pipeline on Ubuntu EC2 hosts. Slim images reduced average deploy time because each release pulls less data over SSH-tunnelled registry access. If your team lacks in-house DevOps capacity, Linux system administration support can cover server hardening alongside container rollout.
For local parity, use Docker Compose multi-container setup with a dev target that keeps Node and Xdebug available. Production stays multi-stage and minimal. Dev stays comfortable. That split is intentional.
What Common Mistakes Undo Your Multi-Stage Size Gains?
Teams adopt multi-stage builds and still ship 600 MB images. The pattern is present. The discipline is not. These failures show up repeatedly on client audits.
- Copying the entire builder stage. A blanket
COPY --from=builder / /defeats the purpose. Copy paths explicitly. - Installing dev dependencies in the runtime stage. Never run
composer installwithout--no-devin production stages. - Leaving package managers in runtime.
apt,apk, andnpmbinaries belong in builder stages only. - Ignoring .dockerignore. Uploading a 400 MB
node_modulesfolder into context wastes time and risks stale copies. - Using
latesttags on base images. Pin digests or minor version tags for reproducible slim builds. - Skipping extension audits. Alpine images need explicit
apk addfor libraries behind PHP extensions.
Resource limits matter too. A slim image still misbehaves if the container has no memory cap. Set CPU and memory bounds as described in limit Docker container resources once the image lands on your host.
WordPress and WooCommerce 11.1 projects follow the same logic. Build custom themes with Node in one stage. Copy only compiled CSS and JS into a PHP-Apache runtime. Magento 2.4.x storefronts benefit even more because frontend compilation chains are longer. For enterprise stacks, enterprise application development workflows often standardise on one multi-stage template per framework.
On booking platforms like Adventure Third Pole Trek, Laravel plus Livewire assets compile in CI. The production container never installs Vite or webpack. That keeps the running footprint small on shared hosting budgets common among Nepal tour operators.
If you are starting from bare metal, install Docker on Ubuntu first using the Install Docker on Ubuntu walkthrough. Then adopt multi-stage patterns before your first production push.
Frontend toolchain choices affect builder stage size. Compare bundlers in the Vite vs Webpack for frontend builds article before locking your Node stage Dockerfile.
Validate JSON config files copied into images with the free JSON formatter tool during local prep. Bad config should fail before it reaches a registry.
Packer complements Docker when you need AMI or VM images alongside containers. See Packer build machine images for hybrid infrastructure patterns.
Post-deploy, ongoing support and maintenance should include quarterly image audits. Base images receive security patches. Rebuild slim stages on schedule, not only when features ship.
Performance testing belongs in the same release train. Testing and optimization services catch regressions when a slim image accidentally drops a caching extension or opcode config.
For eCommerce workloads such as Quick And Easy Nepalese Grocery, smaller images speed horizontal scaling during festival-season traffic spikes. Dashain and Tihar order surges are predictable. Image pull time should not be the bottleneck.
Read more DevOps content on the blog index or learn about the author on the about me page. Browse the full project portfolio for other production Laravel deployments that rely on disciplined container practices.
Key Takeaways
- Split compile-time and runtime into separate Dockerfile stages; copy only artifacts with
COPY --from=. - Choose Alpine or distroless final bases instead of full Debian images when extensions allow it.
- Run
composer install --no-devandnpm ciin builder stages; never in production runtime. - Use .dockerignore, combined RUN layers, and explicit COPY lists to avoid hidden bloat.
- Measure with
docker imagesanddive, then scan slim tags with Trivy before deploy. - Pin base image versions and keep dev Compose targets separate from production multi-stage files.
People Also Ask
How much smaller can a multi-stage Docker image get?
Most Laravel and Node-plus-PHP stacks shrink 60–90% compared to single-stage builds. A typical improvement runs from roughly 900 MB down to 150–200 MB when you drop Node, GCC, and dev Composer packages from the final layer.
Can you use multi-stage builds with Docker Compose?
Yes. Reference the final stage with target: runtime in your Compose build config. Keep a separate dev Dockerfile or target that includes debugging tools. Production Compose files should build only the slim runtime stage.
Do multi-stage builds slow down CI pipelines?
First builds can take longer because multiple stages run sequentially. Cached layers usually make subsequent builds faster than single-stage equivalents. The net win shows up at deploy time when registry pulls transfer far less data.
Are multi-stage builds enough for production security?
They reduce attack surface by removing shells and build tools, but they are not a complete security strategy. Pair slim images with regular base-image rebuilds, non-root users, read-only filesystems where possible, and vulnerability scanning on every tag.
Ship Smaller Images on Your Next Release
Knowing how to reduce Docker image size with multi-stage builds changes every downstream metric: faster deploys, lower registry storage, quicker disaster recovery, and fewer packages for attackers to probe. Start with one Laravel or API service Dockerfile, measure the before-and-after size, wire the build into CI, and roll the pattern across remaining services once the smoke tests pass.
Need help refactoring Dockerfiles across a Laravel fleet or setting up GitLab CI plus Deployer on Ubuntu? Contact us to discuss container hardening and production deployment for your stack.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

