How do I shrink Docker images with multi-stage builds and distroless?
Question
Our Go or PHP Docker images we ship to production reach 1.2GB. This slows CI/CD (upload/download) and creates an attack surface, since the image carries unnecessary libraries like compilers and bash. How do I use a multi-stage build to leave the build tools in the first stage and ship only a minimal distroless or alpine-based image (say 50MB) to production?
Answer
Short answer: a 1.2GB image ships your whole build toolchain to production — both slow and a big attack surface. A multi-stage build fixes both; you put only the artifact and the runtime in the final stage.
The root of the problem is singular: things that shouldn’t be there (compiler, package manager, shell) leak into production.
- The multi-stage idea: a
builderstage produces, the final stage only copies. Put the compiler and dependencies in the first stage and produce the artifact. In the final stage,COPY --from=builderonly the binary/app + the runtime it needs to run. Build tools never enter the image. - For Go, the final stage can be
scratchordistroless/static. The Go binary is self-contained (statically linked), so you get an image of a few MB. Distroless has no shell or package manager — smaller attack surface; when you need to debug, use the:debugvariant. - PHP is more nuanced: the runtime is unavoidable. There’s no compiled binary in PHP; you need the PHP runtime + extensions. Target
php:8.x-fpm-alpine, runcomposer install --no-devin the build stage, and copy onlyvendor/+ the app into the final stage. You won’t hit a few MB, but you drop from hundreds to tens of MB. - Security matters as much as size. Run the container as non-root, exclude junk with
.dockerignore, pin the base image by digest, and let the registry layer-cache the dependency stage so it isn’t refetched on every build.
Bottom line: I’d use multi-stage + distroless/static/scratch for Go, fpm-alpine for PHP, run non-root, and keep .dockerignore tight. That turns a 1.2GB image into tens of MB — faster CI/CD, smaller attack surface. I explain how image layers work and the .dockerignore logic from the ground up in the hub post; that setup lays the groundwork for this optimization.
Related Reading
Comments
Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.