Most Laravel Docker tutorials give you a Dockerfile that produces an 800MB image. Mine is 120MB. Here is exactly how — and why it matters.
The problem with a single-stage Dockerfile
When you install Composer, Node.js, npm, and all your dev dependencies in one layer, every single one of those tools ends up in your production image. Your image carries gigabytes of software that has zero business being on a production server.
The solution: multi-stage builds
Docker lets you define multiple stages in one Dockerfile. Stage 1 does all the heavy lifting — installing Composer, running npm, compiling your Vite assets. Stage 2 starts completely fresh, copies only the compiled output, and ships nothing else.
# Stage 1: builder — installs everything, compiles assets
FROM php:8.3-cli AS builder
RUN apt-get update && apt-get install -y git nodejs npm \
&& curl -sS https://getcomposer.org/installer | php \
&& mv composer.phar /usr/local/bin/composer
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader
COPY package.json package-lock.json ./
RUN npm ci && npm run build
COPY . .
# Stage 2: production — starts clean, copies compiled output only
FROM php:8.3-fpm-alpine AS production
RUN apk add --no-cache nginx supervisor
COPY --from=builder /app /var/www/html
EXPOSE 80
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]
The result
| Single-stage | Multi-stage | |
|---|---|---|
| Image size | ~800MB | ~120MB |
| Build tools in prod | Yes (security risk) | No |
| Deploy speed | Slow | Fast |
What goes in each stage
Stage 1 needs: Composer, Node.js, npm, git, dev headers for PHP extensions.
Stage 2 needs: PHP-FPM, Nginx, Supervisor, and the compiled app. Nothing else.
The order of COPY matters for caching
Copy composer.json BEFORE the rest of your code. Docker caches each layer — if your PHP files change but composer.json didn't, Docker skips reinstalling all your packages. This alone saves 2–3 minutes per build.
COPY composer.json composer.lock ./ # cached unless deps change
RUN composer install --no-dev
COPY . . # only this layer re-runs on code changes
Try it yourself
Build once with a naive Dockerfile, then build with the multi-stage version. Run docker images and compare the sizes. The difference will change how you think about Docker forever.
I wrote a full 50-page guide covering this entire stack — Docker, GitHub Actions CI/CD, Kubernetes, and SQLite as the production database. Link in my profile if you want the complete setup.
Top comments (0)