DEV Community

Akandwanaho Alvin
Akandwanaho Alvin

Posted on

How I built a 120MB production Laravel Docker image (and why yours is probably 800MB)

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"]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)