Skip to content
Muhammet Şafak
tr
Asked by: Korhan Answered:

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.

Short answer

The root of the problem is singular: things that shouldn’t be there (compiler, package manager, shell) leak into production. I explain how layers are formed and the .dockerignore logic from the ground up in the Docker post; this optimization sits on top of that groundwork.

Why

  1. The cost is twofold: speed and attack surface. A 1.2GB image inflates upload/download time on every build; the compiler, bash, and package manager it carries leave an attack surface production never needs.

  2. The multi-stage idea: a builder stage 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=builder only the binary/app + the runtime it needs to run. Build tools never enter the image.

  3. Go and PHP don’t bottom out at the same place. The Go binary is self-contained (statically linked), so you get an image of a few MB. In PHP there’s no compiled binary; you need the PHP runtime + extensions, so the target size has to be different too.

What to do

  1. For Go, make the final stage scratch or distroless/static. Distroless has no shell or package manager — smaller attack surface; when you need to debug, use the :debug variant.

  2. For PHP, target php:8.x-fpm-alpine. Run composer install --no-dev in the build stage, and copy only vendor/ + the app into the final stage. You won’t hit a few MB, but you drop from hundreds to tens of MB.

  3. Tighten security 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

Share:

Comments

Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.

More Questions

All questions

Search the site

Start typing to search posts, projects and pages.

Esc to close Powered by Pagefind