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.

The root of the problem is singular: things that shouldn’t be there (compiler, package manager, shell) leak into production.

  1. 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.
  2. For Go, the final stage can be scratch or distroless/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 :debug variant.
  3. 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, 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.
  4. 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

Tags: #ci-cd#docker#infrastructure
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