CI pipeline drops from twelve to four minutes: the team switched to php:8.3-alpine, removed apt layers, and cut the image from 680 MB to 95 MB. Celebration follows. Three days later, GD fails to load a TrueType font — musl and missing libs — and the thumbnail generator outputs empty PNGs.
Lean images speed pull, deploy, and cold start. They do not automatically simplify your app. Confusing "small Dockerfile" with "understood stack" yields images that boot fast but fail silently on a prod edge case.
Size, layers, and what actually costs time
Docker ships incremental layers. A "heavy" image with good local cache may deploy faster than a "lean" one that invalidates everything each commit because COPY . . sits too high.
| Lever | Typical gain | Risk |
|---|---|---|
| Alpine/slim base | −50 to 80% size | libc incompatibilities, missing packages |
| Multi-stage | Removes toolchain from runtime | Forgetting runtime libs (ssl, tzdata) |
| .dockerignore | Stable cache | Excluding a required build file |
| Combine RUN apt | Fewer layers | Harder line-by-line patching |
Measure pull + start time on your registry and host — not just docker images locally.
A lean untested image externalizes cost to midnight debugging.
Multi-stage: separate build and runtime
Classic PHP/Node pattern:
- Builder stage: composer install, npm run build, compile extensions.
- Runtime stage: copy vendor/, public/build/, compiled binaries only.
Do not copy .git, tests, dev node_modules, or docs. Ensure PHP runtime extensions (pdo_mysql, intl, opcache) install in the final stage, not only builder.
On hosting with private registry (Scaleway, GitLab, Harbor), compact finals also cut storage and egress — especially with ten microservices.
Alpine, slim, distroless: picking libc
Alpine (musl): very small, apk packages. Frequent issues with wkhtmltopdf, some PECL extensions, glibc-linked binaries.
Debian/Ubuntu slim (glibc): slightly heavier, more predictable PHP/Python ecosystem.
Distroless / scratch: minimal, often no shell — great for Go or Java, harder for PHP-FPM where debug matters.
Minimum test matrix: container start, health check, critical business path (image upload, PDF, TLS API call).
.dockerignore and hidden dependencies — the wrong way
An overly aggressive .dockerignore excluding composer.lock or config forces implicit composer update at build — "lean" but non-reproducible images.
Conversely, sending the whole repo without ignore bloats build context and breaks cache on every doc commit.
Explicitly list what enters context: lockfiles, patches, custom extensions. Document system libs in the Dockerfile (comment + RUN apk add --no-cache ...).
Security, CVEs, and "latest" images
FROM node:latest or php:8-apache without a patch tag bloats and obscures updates. Pin php:8.3.12-fpm-bookworm and automate rebuild on CVE.
Scan (Trivy, Grype) the final image — not just the Dockerfile. A minimal alpine base with vulnerable openssl is still risk.
The peak: lightness that hides misunderstanding
Saving pull time is worthless if you spend an hour reconstructing what the old Debian image implicitly installed.
Decide and move forward without blind spots
Document runtime libs, adopt multi-stage, pin bases, smoke-test in CI. Compare alpine vs slim on your stack, not a generic benchmark.
For registry hosting, CI runners, and Docker VPS in Europe, see our directory and compare tool. Guides cover container framing.
Release checklist: pull fresh image, run health, execute a business scenario, scan CVE — before celebrating megabytes saved.
Frequently asked questions
Is Alpine always the right choice for a lean image?
No — test musl vs glibc for your stack; debian-slim or distroless may be more stable.
What does a multi-stage build provide?
Separates build and runtime; the final image does not ship the toolchain.
How do I speed up pull without hurting security?
Layer cache, .dockerignore, pinned tags — speed comes from cache as much as size.
Should production images include a shell?
Debug vs attack surface trade-off; choose based on external observability capability.
A lean image is judged on Friday night deploy — not Monday morning docker images.