TL;DR: Multi-stage build, minimal runtime base, and a .dockerignore. Compile in a stage that has the toolchain and copy only the artifact into a distroless or Alpine runtime. Whether 100MB is achievable depends on the runtime and assets; measure the layers before promising that target.
How to approach it
Say where the weight actually is before optimising, then give the change in order of return. Finish on why size matters beyond storage, since that is what makes it a senior answer rather than a tip.
A strong answer
First find the weight:
docker history --no-trunc myapp:1.2 | head -20
That lists image history with layer sizes, in history order rather than sorted by size. Look for a full OS base image, a language SDK, a package manager cache, and the source tree, none of which the running process needs.
Multi-stage build is the structural fix and it does most of the work:
FROM golang:1.27.1 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app ./cmd/server
FROM gcr.io/distroless/static-debian12
COPY --from=build /app /app
USER nonroot:nonroot
ENTRYPOINT ["/app"]
The build stage keeps the compiler and module cache; the final image contains one binary. The final size depends on the binary, libraries and runtime data. Use a supported toolchain and pin production base images by reviewed digest; Go’s release history lists support and patch versions. For an interpreted language the runtime has to come along, so the target is more like 80 to 150MB with Alpine or a slim base, and the win comes from excluding dev dependencies and build tools rather than everything.
Base image choice in order of size and awkwardness. Distroless has no shell and no package manager, which reduces bundled tooling but is not universally smallest or most secure. A shell-based exec session is unavailable, so you need ephemeral debug containers. Alpine is small and uses musl rather than glibc, which occasionally breaks native extensions and has caused real DNS resolution differences. A slim Debian variant is the safe middle. Pick deliberately and say why.
Layer ordering matters for build speed rather than final size, but it belongs in the answer: put the least frequently changing instructions first, so dependency installation is cached and only the source copy invalidates. Copying the whole source before installing dependencies means every code change reinstalls everything.
.dockerignore stops the build context carrying .git, node_modules, test fixtures and local artifacts into the daemon, which both slows the build and often ends up inside the image by accident.
Now why it matters beyond the number. Every node that lacks the required cached layers must pull them, so pull time is on the critical path of every scale-up and every reschedule, which is exactly when you are already under pressure. On a 200-node cluster a 1.2GB image is a lot of bandwidth per rollout. And image size correlates with attack surface: a shell, a package manager and a hundred OS packages are all things a scanner will flag and an attacker can use.
What interviewers probe next
"How do you debug a distroless container?" kubectl debug with an ephemeral container sharing the process namespace. The tooling lives in the debug image, not in production.
"What is a scratch image?" Empty. Works for a static binary with no CA certificates or timezone data; add those explicitly or TLS calls fail in a way that looks unrelated.
"Does a smaller image start faster?" The pull is faster, which dominates on a cold node. The process start is unchanged.
Common mistakes
Deleting files in a later RUN layer to shrink the image. Layers are additive, so the data is still in the earlier layer and the image is no smaller.
Choosing Alpine reflexively and meeting musl incompatibilities in production rather than in test.
Optimising the number to look good while ignoring that the real payoff is pull time during a scale-up.