Build Optimized Docker Images with Multi-Stage Dockerfiles
You will write a Dockerfile, build it with multi-stage layers, and scan the result for vulnerabilities. By the end you will produce a small, cache-friendly image with a minimal attack surface.
Learning Objectives
- Author a Dockerfile using layer-cache-friendly ordering.
- Shrink images with a multi-stage build.
- Scan images for known CVEs with Trivy.
- Time: ~3 hours · Difficulty: Intermediate · Prereqs: Docker and a small app to containerize.
Architecture Overview
graph LR
Src[Source code] --> B[Build stage
compile/deps] B -->|copy artifact| R[Runtime stage
slim base] R --> Img[Final image] Img -->|Trivy scan| Scan[CVE report]
compile/deps] B -->|copy artifact| R[Runtime stage
slim base] R --> Img[Final image] Img -->|Trivy scan| Scan[CVE report]
Environment Setup
You will need: Docker with BuildKit and Trivy (install docs).
Before you begin: have a buildable app (a small Go or Node service works well).
Step-by-Step Execution
01
Write a multi-stage Dockerfile
Compiling in a build stage and copying only the artifact into a slim base keeps the runtime tiny.
# Dockerfile
FROM golang:1.22 AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o app .
FROM gcr.io/distroless/static
COPY --from=build /src/app /app
ENTRYPOINT ["/app"]
02
Build with cache and a tag
DOCKER_BUILDKIT=1 docker build -t tyf/app:1.0 .BuildKit caches layers, speeding up rebuilds.
03
Compare image size
$ docker images tyf/app
REPOSITORY TAG SIZE
tyf/app 1.0 12.3MB <- distroless runtime
04
Scan for vulnerabilities
trivy image --severity HIGH,CRITICAL tyf/app:1.0Progress So Far
graph LR
A[01 Dockerfile] -->|done| B[02 Build]
B -->|done| C[03 Size check]
C -->|done| D[04 Trivy scan]
style A fill:#1a4a1a,stroke:#00ff00,color:#fff
style B fill:#1a4a1a,stroke:#00ff00,color:#fff
style C fill:#1a4a1a,stroke:#00ff00,color:#fff
style D fill:#1a4a1a,stroke:#00ff00,color:#fff
Testing & Validation
docker run --rm tyf/app:1.0 --version && trivy image --exit-code 1 --severity CRITICAL tyf/app:1.0The app should run and Trivy should exit 0 (no criticals). If so, you have a small, scanned image.
Troubleshooting
- Image still huge: ensure the final stage uses a slim/distroless base and copies only the artifact.
- Cache never hits: order Dockerfile steps from least to most frequently changing.
- Trivy DB error: run with internet access on first use to fetch the vulnerability DB.
Extension Ideas
- Push to a registry and sign the image with cosign.
- Add the scan to a pipeline in Full CI/CD Pipeline.
- Run it on a cluster in Pods & Deployments.
Key Results
- Produced a ~12MB runtime image via multi-stage build.
- Cut rebuild time with BuildKit layer caching.
- Scanned for HIGH/CRITICAL CVEs before shipping.
- Minimized attack surface with a distroless base.