Profile    Mohammed Shiroz Status   Loading  
Logo
Share This
Back to blog
Filter by:
Tags
//Article title

Docker Mistakes That Make Your Images Huge and Your Builds Slow

About Post

You change one line of PHP. You rebuild the image. Docker downloads every Composer package again, reinstalls every npm package, rebuilds every asset, and then pushes an image the size of a small operating system to your registry.

Ten minutes later you can test your one-line change.

Slow builds and huge images are rarely Docker's fault. They come from a handful of small mistakes in the Dockerfile, and almost all of them are easy to fix once you understand one idea: layers and the build cache.

The one idea: layers are cached in order

Each instruction in a Dockerfile (COPY, RUN and so on) creates a layer. Docker caches layers, and when you rebuild, it reuses them until it reaches the first instruction whose inputs changed. From that point on, every layer is rebuilt.

So the order of your Dockerfile decides how much work a rebuild does. Keep that in mind, because most of the mistakes below are really the same mistake.

Mistake 1: no .dockerignore

What happens: COPY . . sends your whole project folder to the build, including .git, node_modules, vendor, logs, local uploads and your .env.

Why it hurts: builds start slowly because of the huge build context, any changed log file invalidates the cache, and your local secrets can end up inside the image. Anyone who can pull the image can read them.

The fix:

.git
.env
node_modules
vendor
storage/logs/*
storage/framework/cache/*
public/build
tests
docker-compose*.yml

Dependencies are installed inside the build, so there's no reason to copy your local ones in.

Mistake 2: copying everything before installing dependencies

What happens:

COPY . .
RUN composer install

Why it hurts: any change to any file invalidates the COPY layer, so composer install runs again on every build, even though composer.json didn't change.

The fix: copy the dependency files first, install, and only then copy the rest of the code. Your dependencies change rarely; your code changes constantly. Put the rarely changing things higher up.

Mistake 3: shipping development tools to production

What happens: the production image includes PHPUnit, debug packages, the Node.js toolchain, node_modules and compilers, because they were needed at some point during the build.

Why it hurts: a bigger image means slower pulls and deploys, and every extra package is extra attack surface. Your production container has no business running a test framework.

The fix: composer install --no-dev for production, and a multi-stage build (below) so build tools never reach the final image.

Mistake 4: cleaning up in a separate layer

What happens:

RUN apt-get update && apt-get install -y build-essential
RUN rm -rf /var/lib/apt/lists/*

Why it hurts: layers are additive. The files deleted in the second layer still exist in the first one, so the image doesn't get any smaller. You've just hidden them.

The fix: install and clean up in the same RUN, and use --no-install-recommends to skip optional packages. Better still, use a multi-stage build so build-only packages are never in the final image at all.

Mistake 5: running as root

What happens: no USER instruction, so the app runs as root inside the container.

Why it hurts: if someone finds a way to run code through your app, they're root in that container, which makes everything else they try easier.

The fix: set file ownership with COPY --chown and switch to an unprivileged user (the official PHP images already include www-data). Make sure that user can only write where it needs to, like storage and bootstrap/cache in Laravel.

Mistake 6: latest tags

What happens: FROM php:latest or FROM node:latest.

Why it hurts: the same Dockerfile builds a different image next month. One day latest moves to a new major version, and your build breaks, or worse, it builds fine and fails at runtime.

The fix: pin versions, like php:8.4-fpm-alpine and node:22-alpine. Upgrade on purpose, in a pull request, with tests.

Putting it together: a multi-stage build

Multi-stage builds solve most of these at once. Each stage does one job, and the final stage copies in only the results. Here's a simplified version for a Laravel app with Vite assets:

FROM composer:2 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist
COPY . .
RUN composer dump-autoload --optimize --no-dev

FROM node:22-alpine AS assets
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM php:8.4-fpm-alpine
WORKDIR /var/www/html
COPY --chown=www-data:www-data . .
COPY --from=vendor --chown=www-data:www-data /app/vendor ./vendor
COPY --from=assets --chown=www-data:www-data /app/public/build ./public/build
USER www-data

Composer and Node live only in their own stages. The final image has PHP, your code, production dependencies and compiled assets, and nothing else. Changing a PHP file leaves the composer install and npm ci layers cached. In a real project you'd also install the PHP extensions your app needs in the final stage and handle the web server, but the structure stays the same. (One gotcha: the composer image doesn't have every PHP extension your app requires, so Composer may refuse to install. Checking platform requirements in the final stage instead is a common workaround.)

Order your Dockerfile by how often things change: base image first, then system packages, then dependency files and installs, and your application code last. That single habit fixes most slow builds.

Quick checklist

  • A .dockerignore that excludes .git, .env and local dependencies.
  • Dependency files copied and installed before the rest of the code.
  • No dev dependencies or build tools in the final image.
  • Install and clean up in the same layer, or use stages.
  • A non-root user.
  • Pinned base image versions.
  • Run docker history your-image once in a while to see which layers are heavy.

Which of these was hiding in your Dockerfile? And what's the biggest image you've ever had to slim down?

Comments (0)
Leave your review

Thanks for your valuable comments. Your comments has been updated and appreciate your getting in touch...

01. About Shiroz

Mohammed Shiroz

Hi, I'm Mohammed Shiroz, a software engineer and AI enthusiast from Sri Lanka who turns ideas into intelligent, real-world solutions. With over 9 years of hands-on experience, I currently lead real estate ERP development at Kate Group, a...

03.My Projects

04. Categories

Ready To order Your Project ?

Get in Touch
Close