You have a Docker image sitting at 1.2 GB, it takes 12 minutes to build, and you later discover it contains dev tools, Composer cache, and SSH keys. Sound familiar? We see it all the time in projects that come to us for consulting: bloated images, slow builds, vulnerabilities in production.
A poorly written Dockerfile costs time and security. An optimized Dockerfile — using multi-stage builds, layer caching, and security best practices — can shrink images by 70%, speed up builds by 40%, and keep out preventable vulnerabilities.
We, at Meteora Web, don't write Dockerfiles just to make things work. We write them because when you deploy to production, every build second and every extra MB is a cost. We come from accounting — we know that well.
This guide is practical. Read it, apply it, and your next Dockerfile will be faster, smaller, and safer.
Why Multi-Stage Build is a Game Changer
The classic problem: to compile a PHP or Node application you need build tools (Composer, npm, C compilers). But in production those tools are useless. They add weight, introduce attack surface, and slow down builds.
Multi-stage build lets you have multiple stages in a single Dockerfile. Each stage starts from a different base, but only the last stage ends up in the final image. Intermediate stages — where you install tools, compile, download dependencies — are discarded.
Result: slim images, fast builds, zero unnecessary tools in production.
Concrete Example: a PHP Laravel Application
Let's look at a real Dockerfile that starts huge and shrinks it with multi-stage.
# Stage 1: builder
FROM php:8.2-cli AS builder
WORKDIR /app
# Install only extensions needed for Composer
RUN apt-get update && apt-get install -y --no-install-recommends \
libzip-dev \
unzip \
git \
&& docker-php-ext-install zip pdo_mysql \
&& rm -rf /var/lib/apt/lists/*
# Copy Composer globally (or install)
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
# Copy code and install production dependencies
COPY . .
RUN composer install --no-dev --optimize-autoloader
# Stage 2: production
FROM php:8.2-fpm-alpine AS production
WORKDIR /var/www/html
# Copy only vendor and code from builder
COPY --from=builder /app/vendor ./vendor
COPY --from=builder /app/app ./app
COPY --from=builder /app/config ./config
COPY --from=builder /app/public ./public
COPY --from=builder /app/routes ./routes
COPY --from=builder /app/resources ./resources
COPY --from=builder /app/storage ./storage
COPY --from=builder /app/.env .env
# Minimal PHP extensions for runtime
RUN docker-php-ext-install pdo_mysql
EXPOSE 9000
CMD ["php-fpm"]
The final image is at least 200 MB smaller than a single-phase image with tools included.
Layer Caching: Leverage It, Don't Fight It
Docker builds images in layers. Each RUN, COPY, ADD instruction creates a layer. If a layer hasn't changed, Docker reuses it from cache. This is your best friend for speed.
Common mistakes:
- Copying all code before installing dependencies — any code change invalidates cache for subsequent layers, including heavy ones like
RUN composer install. - Not ordering instructions by stability — put rarely changing files first (package.json, composer.lock, config files) and frequently changing code last.
Dockerfile Optimized for Caching
# Maximize cache hits
FROM node:18-alpine AS builder
WORKDIR /app
# 1. Files that change rarely first
COPY package.json package-lock.json ./
RUN npm ci --only=production
# 2. Then only the code that changes often
COPY . .
RUN npm run build
With this approach, if you only modify source code (not dependencies), the RUN npm ci layer is pulled from cache and the build takes seconds, not minutes.
Security: Reducing the Attack Surface in the Dockerfile
A Docker image is not just a container: it's a minimal operating system. Every installed package is a potential vulnerability.
1. Use Official, Pinned Images
FROM php:8.2-fpm is better than FROM php:latest. Latest changes and can break your build. Always use specific tags like php:8.2-fpm-alpine. Alpine Linux reduces attack surface and size.
2. Never Run as Root
# Create a non-privileged user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
If an attacker compromises the PHP process, they won't have root access inside the container.
3. Clean Cache and Temp Packages in the Same RUN
In a single RUN, install, use, and remove. Layers only save the final state.
RUN apt-get update && apt-get install -y --no-install-recommends \
libzip-dev \
&& docker-php-ext-install zip \
&& rm -rf /var/lib/apt/lists/*
4. Don't Copy Sensitive Files (SSH Keys, .env with Credentials)
Use environment variables, secret mounts (Docker BuildKit), or external vaults. Never COPY id_rsa ..
5. Scan Images with Tools like Trivy or Docker Scout
Integrate scanning into your CI/CD pipeline. We use trivy image --severity HIGH,CRITICAL my-image before every deployment.
Combined Best Practice: Multi-Stage + Caching + Security
Here's a complete Dockerfile for a Node.js Next.js application that ties everything together.
# Builder stage
FROM node:18-alpine AS builder
WORKDIR /app
# Dependencies (rarely change)
COPY package.json package-lock.json ./
RUN npm ci --only=production
# Backup node_modules for final stage
RUN cp -R node_modules /prod_modules
# Copy code and build
COPY . .
RUN npm run build
# Production stage
FROM node:18-alpine AS production
# Non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
# Copy only what's needed
COPY --from=builder /app/next.config.js ./
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/package.json ./
# Use node_modules from builder (production only)
COPY --from=builder /prod_modules ./node_modules
EXPOSE 3000
USER appuser
CMD ["npm", "start"]
Analysis: final image ~150 MB (vs ~1 GB with devDependencies), fast builds thanks to caching, zero dev packages, non-root user.
Tools to Check Dockerfile Quality
- Hadolint — linter for Dockerfiles. Run
hadolint Dockerfileto check best practices. - Dive — analyze image layers. Discover what's taking space.
- Docker Scout (built into Docker Desktop) — vulnerability scanning.
In Summary — What to Do Now
- Rewrite your Dockerfile with multi-stage. Separate build and runtime stages.
- Order instructions to maximize caching: stable files first, code later.
- Use official, pinned images and prefer Alpine to reduce surface area.
- Create a non-root user and use it in the CMD.
- Clean cache and remove build tools in the same layer.
- Scan the image with Trivy or Docker Scout before deploy.
Apply these six points and your next deployment will be faster, safer, and cheaper in resources. As we always say: a site (or a container) is measured in revenue and speed, not compliments. Happy containerizing.
Sponsored Protocol