Docker from Basics to Production · Production-Ready Images
Small and Secure Images
Measure what a multi-stage build and hardening change: size, package count, user and writable paths.
What you'll build
Two images of the same Go service, a single-stage one and a multi-stage distroless one, compared on size, number of OS packages, user and whether a shell is available to an attacker.
The idea
Every package in an image is something that can have vulnerabilities, something scanners will flag, and something an attacker can use after a compromise. Reduce it:
- Multi-stage builds keep compilers and build tools out of the runtime image.
- Distroless or scratch images for static binaries contain no shell or package manager.
- Non-root by default (distroless provides a
nonrootuser, UID 65532). - Read-only root filesystem at run time.
- Rebuild regularly so base image security fixes reach you, and scan images in CI (Docker Scout, Trivy, Grype).
The code
trap 'docker image rm -f rc-fat:1 rc-slim:1 > /dev/null 2>&1' EXIT
cat > main.go <<'EOF'
package main
import (
"fmt"
"os"
)
func main() { fmt.Printf("hello, running as uid %d\n", os.Getuid()) }
EOF
printf 'module example.com/hello\n\ngo 1.26\n' > go.mod
cat > Dockerfile.single <<'EOF'
FROM golang:1.26-alpine
WORKDIR /src
COPY . .
RUN go build -o /app .
CMD ["/app"]
EOF
cat > Dockerfile.multi <<'EOF'
FROM golang:1.26-alpine AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /app .
FROM gcr.io/distroless/static-debian13:nonroot
COPY --from=build /app /app
ENTRYPOINT ["/app"]
EOF
docker build -q -f Dockerfile.single -t rc-fat:1 . > /dev/null
docker build -q -f Dockerfile.multi -t rc-slim:1 . > /dev/null
for img in rc-fat:1 rc-slim:1; do
printf '%-10s size %-8s ' "$img" "$(docker image ls $img --format '{{.Size}}')"
docker run --rm --read-only "$img" | tr -d '\n'
printf ' | shell available: '
docker run --rm --entrypoint sh "$img" -c 'echo yes' 2>/dev/null || echo no
done
echo "apk packages in the single-stage image: $(docker run --rm rc-fat:1 sh -c 'apk info 2>/dev/null | wc -l')"
Output
rc-fat:1 size 412MB hello, running as uid 0 | shell available: yes rc-slim:1 size 8.99MB hello, running as uid 65532 | shell available: no apk packages in the single-stage image: 17
Run this on your computer — see “Get Started” for setup.
Common mistakes
- Shipping the build image ("it works") with compilers, source code and caches.
- Using
latestbase images, so a rebuild silently changes the OS underneath you. - Disabling scanner findings instead of rebuilding on a patched base.
- Assuming small means secure: an outdated small image is still vulnerable.
Key points
- Multi-stage builds and minimal runtime bases cut size and attack surface.
- Run as non-root with a read-only root filesystem.
- Rebuild and scan images regularly.
Further reading: Multi-stage builds · Distroless images · Docker Scout
Practice questions
Why does removing the shell from a runtime image improve security?
Answer
An attacker who gets code execution has fewer tools to explore, download payloads or escalate; it also removes packages that could carry vulnerabilities.
How do base image security fixes reach your application image?
Answer
Only when you rebuild on the updated base and redeploy; schedule regular rebuilds and scanning rather than waiting for code changes.
What does -ldflags="-s -w" do in a Go build?
Answer
It strips the symbol table and DWARF debug information, making the binary smaller.
Open in the app to mark lessons complete, take notes and track your progress.