BR-ANAC-DOV-MOD-1 study materials
BR-ANAC-DOV-MOD-1 study resources

Multi-Stage Build Produces Bloated, Vulnerable Image

BR-ANAC-DOV-MOD-1 · Put your knowledge to work in context.

All case studies

Multi-Stage Build Produces Bloated, Vulnerable Image

A Go service is built with a single-stage Dockerfile: FROM golang:1.22, copy source, go build, then run the binary. The resulting image is 1.4 GB. A scan flags 340 CVEs, mostly in build tooling and OS packages the runtime never uses. The binary is statically linked. The team also wants reproducible builds and a smaller attack surface, but a colleague warns that switching to a distroless base will break their debugging workflow, which relies on docker exec into the container to inspect files and run curl. Deployments run on Linux amd64 only. The CI runner has no network access during the build stage beyond a pre-populated module cache.

1. Design a multi-stage build that addresses size, CVE count, and reproducibility, and explain which problems each stage separation solves.

Read the worked answer

Use a builder stage and a minimal runtime stage:

FROM golang:1.22 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o /out/api ./cmd/api

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/api /api
USER nonroot
ENTRYPOINT ["/api"]

Why this works: only the final stage ships. The 1.4 GB toolchain, compilers, and their CVEs stay in the builder and never reach the registry. A static distroless base has almost no packages, so the CVE count collapses to the base image's own surface. -trimpath and pinned base digests improve reproducibility; -s -w strips symbols for a smaller binary. CGO_ENABLED=0 guarantees the static link the distroless/static base assumes.

Because the CI runner has no network beyond the module cache, go mod download must resolve from that cache; verify GOFLAGS=-mod=mod and a mounted cache path, or the build stage will fail despite a correct Dockerfile.

2. The debugging workflow depends on shell access inside the running container. How do you preserve diagnosability without reintroducing the bloat, and what does this imply for security?

Read the worked answer

Distroless images have no shell, so docker exec -it ... sh will fail by design. That absence is a security feature: fewer binaries means fewer exploitation paths, and no shell means an attacker who lands in the container has less to work with.

Preserve diagnosability without weakening the runtime image:

  • Ephemeral debug containers (Kubernetes) or a sidecar with tooling, attached only during incidents.
  • A separate debug image built FROM distroless ... AS debug that adds busybox or a debugger, used locally and never deployed.
  • Structured logs and metrics so most diagnosis does not require shell access; add a /healthz and pprof endpoint gated to internal traffic.
  • docker cp to pull files out for inspection.

Security implications: keeping a shell and curl in production expands the attack surface and re-adds the very packages the scan flags. The right trade is to move debugging to ephemeral, audited mechanisms rather than bake tooling into every deployed container. Document the debug procedure so on-call engineers are not blocked mid-incident.