NovuSpark
All articles

July 4, 2026 · NovuSpark Team

Docker Image Security and Vulnerability Scanning

This is the fifth and final post in our Docker fundamentals series, building on images/containers, production Dockerfiles, networking, and Compose.

Every Docker image is built on layers you didn't write — a base OS image, a language runtime, whatever packages got installed along the way. Every one of those layers can carry known vulnerabilities, and "the Dockerfile builds and the app runs correctly" says nothing at all about whether any of them are present. This post covers how to actually find out, and what to do about what you find.

Scanning an image

docker scout cves my-app:latest
 ✓ Provenance obtained from attestation

 ## Overview

    Analyzed image  my-app:latest
       Vulnerabilities found  14

    │           │ Total │ Critical │ High │ Medium │ Low │
    ├───────────┼───────┼──────────┼──────┼────────┼─────┤
    │ Base image│    12 │        1 │    4 │      6 │   1 │
    │ Your code │     2 │        0 │    1 │      1 │   0 │

docker scout (built into recent Docker Desktop/CLI installs) is one option; trivy, an open-source scanner from Aqua Security, is another widely used, CI-friendly choice:

trivy image my-app:latest

Both tools compare every package in every layer against known-vulnerability databases (the same CVE data security teams already track) and report what they find, along with severity ratings.

Reading a scan result correctly

The critical distinction in that output above — "Base image" vs. "Your code" — matters more than the raw vulnerability count. A scan reporting "14 vulnerabilities" sounds alarming until you notice that 12 of them belong to the base image, not anything your team wrote. This changes what the actual fix is:

  • Base image vulnerabilities are usually fixed by updating to a newer base image tag — the vulnerability was patched upstream, and you're just behind. This is often a one-line Dockerfile change (FROM node:20-slimFROM node:20.15-slim, or simply rebuilding against node:20-slim again if it's a rolling tag) with no code change required at all.
  • Vulnerabilities in your own dependencies need the equivalent of npm audit fix, pip install --upgrade, or whatever your language ecosystem's dependency-update mechanism is — the scan told you a problem exists; fixing it is a normal dependency-update, the same as it would be outside a container.

Minimizing what there is to find in the first place

Every package that's part of an image is something that can carry a vulnerability. This directly connects back to the multi-stage build pattern from the second post in this series: a final image that only contains the compiled application and its runtime dependencies — no build tools, no compilers, no package manager cache — has meaningfully less surface for a scanner to find anything in, by construction, not by accident.

Distroless images push this further than slim or alpine bases: no shell, no package manager, no coreutils — just the application and the minimal runtime it needs.

FROM node:20 AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
 
FROM gcr.io/distroless/nodejs20-debian12
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
CMD ["dist/server.js"]

The trade-off is real: no shell also means docker exec my-app sh for interactive debugging simply doesn't work — there's no shell binary present to run. Distroless is a deliberate choice for production images where the security benefit is worth losing that debugging convenience, not a default for every image in every environment.

Wiring scanning into CI, not just running it manually

A scan run occasionally, by hand, catches problems irregularly. The version that actually holds a standard is one that runs automatically, on every build, and can fail the pipeline:

# .github/workflows/scan.yml
name: Scan Docker image
on: [pull_request]
jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build image
        run: docker build -t my-app:${{ github.sha }} .
      - name: Scan for critical/high vulnerabilities
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: my-app:${{ github.sha }}
          severity: CRITICAL,HIGH
          exit-code: 1

exit-code: 1 is the detail that makes this a real gate rather than a report nobody reads: a build with a critical or high-severity vulnerability fails the pipeline outright, the same way a failing test would, rather than producing a report that sits in a build log no one opens until something goes wrong.

Base image choice as an ongoing decision, not a one-time one

A base image that was clean when you first wrote FROM node:20-slim doesn't stay clean forever — new CVEs get discovered continuously against packages that were already sitting in that image. This is the argument for rebuilding on a schedule even when your own application code hasn't changed at all — a nightly or weekly CI job that rebuilds and rescans your image against current vulnerability data, independent of any actual feature work, catches newly-disclosed vulnerabilities in unchanged base images before they're found some other way.

What to actually remember from this series

  • Scan every image, and read the base-image vs. own-code split — most findings in a typical scan are base-image issues fixed by an update, not a code change.
  • Multi-stage, minimal base images (slim, alpine, or distroless) reduce what there is to find, by construction — this is the same principle from the production-Dockerfile post, now viewed through a security lens.
  • Wire scanning into CI with a real failure threshold, not a report nobody reads.
  • Rebuild and rescan on a schedule, independent of your own code changes — vulnerabilities get discovered in base images that never changed.

That closes out our Docker fundamentals series — from the container model itself through production-ready builds, networking, Compose, and now security. If your team is standardizing container practices across real production workloads, this is exactly the kind of hands-on work we build our DevOps & Automation training around.

Ready when you are

Want training built around your team's real work?

Tell us about your team and what you're trying to solve — we'll recommend a program that fits.