Overview
Image scanning builds a software bill of materials for every layer of a container image, OS packages and application dependencies alike, and checks it against a continuously updated vulnerability database, catching known-CVE versions of things the image includes before it's ever deployed. Scanning is necessary but not sufficient: Docker's own build guidance is just as much about not creating vulnerabilities to find in the first place, minimal base images with fewer packages to ever appear in a scan, multi-stage builds that drop build tools from the final image, and running as a non-root user so a successful compromise still has to escalate privilege to do real damage.
Quick Reference
| Practice | What it does | Why it matters |
|---|---|---|
| SBOM + vulnerability scan | Matches every package in the image against known CVEs | Finds known-vulnerable versions before deployment |
| Minimal base image (Alpine, distroless) | Ships fewer OS packages | Fewer things that can ever show up in a scan |
| Multi-stage build | Drops build tools/compilers from the final image | Smaller attack surface, smaller image |
Non-root USER | Runs the process without root privileges | A compromise still has to escalate privilege |
Syntax
# Multi-stage build - the final image contains only the compiled
# output, not the compiler or build-time dependencies.
FROM golang:1.22 AS build
WORKDIR /src
COPY . .
RUN go build -o app .
FROM alpine:3.19
COPY --from=build /src/app /usr/local/bin/app
USER 1000:1000
ENTRYPOINT ["app"]Examples
# Scan an image before it's pushed to a registry, not after;
# the failing exit code is what actually gates the pipeline.
docker scout cves my-app:latest --exit-code --only-severity critical,highA scanner finds known-vulnerable packages, not application logic bugs
Image scanning matches package versions against a CVE database; it won't catch an application's own injection vulnerability or business-logic flaw. It's one layer of a pipeline, not a substitute for SAST/DAST or secure coding practices.
Visual Diagram
Common Mistakes
- Adding a scanner to the pipeline but only recording findings instead of failing the build on critical/high severity results.
- Building on a full general-purpose base image out of familiarity, carrying far more scannable surface than the application actually needs at runtime.
- Leaving the final image running as root because it was simpler during development, and never revisiting it before production.
- Scanning once at build time and assuming the image stays safe indefinitely; new CVEs are published against already-built images constantly, so a stale, unrebuilt image drifts vulnerable over time.
Performance
- Scanning adds real time to a build pipeline proportional to the number of packages in the image, a smaller, minimal image is also a faster image to scan.
- Multi-stage builds can be slower to author but produce faster-to-pull, faster-to-scan, faster-to-start final images, since build-time-only dependencies never ship.
Best Practices
- Scan every image before it's pushed to a registry, and configure the pipeline to fail on critical/high severity findings, not just report them.
- Default to a minimal or distroless base image, and justify a larger one rather than the other way around.
- Use multi-stage builds so compilers and build tools never end up in the image that actually runs in production.
- Run as a non-root user via
USER, and avoidsudoinside the container entirely. - Rebuild and rescan images on a schedule, not just once at initial build, since new vulnerabilities are discovered against already-shipped packages continuously.