Check your Dockerfile base images for end of life
FROM line and image: is checked against endoflife.date: which base images are end of life or close to it, which Linux release is under them, and the tag to move to.Checked against OSV.dev, CISA KEV, FIRST EPSS, npm, PyPI, deps.dev, endoflife.date and Docker Hub, live.
Sample: A Dockerfile and a compose file
A multi-stage build for a frontend, and a compose file for the services next to it. Every image here is an old but common choice.
# Build the frontend FROM node:18-alpine AS web WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build # Serve it FROM nginx:1.20-alpine COPY --from=web /app/dist /usr/share/nginx/html
services:
web:
build: .
ports:
- "8080:80"
api:
image: python:3.8-slim-buster
command: python -m app
db:
image: postgres:12
cache:
image: redis:latest
6 findings: 5 critical and 1 low priority
5 base images from docker-compose.yml, Dockerfile
Critical: Base image node:18-alpine is end-of-life
Dockerfile:2node:24-alpine
Critical: Base image nginx:1.20-alpine is end-of-life
Dockerfile:10nginx:1.31-alpine
Critical: Base image python:3.8-slim-buster is end-of-life
docker-compose.yml:7python:3.15-slim-trixie
Critical: Base image python:3.8-slim-buster runs on end-of-life Debian 10 (Buster)
docker-compose.yml:7python:3.15-slim-trixie
Critical: Base image postgres:12 is end-of-life
docker-compose.yml:10postgres:18
Low: Unpinned base image redis:latest
docker-compose.yml:12redis:8.10
Last checked against endoflife.date and Docker Hub.
What it reads
- Every
FROMline, with--platform, digests andARGdefaults declared before it. In a multi-stage build,FROM buildrefers to an earlier stage and isn’t an image, so it’s skipped, as isscratch. - Compose
image:lines, with${TAG:-16}defaults filled in. Services that usebuild:come from your own Dockerfile, so they’re skipped. - Official images such as node, python, golang, ruby, php, postgres, mysql, mariadb, mongo, redis, nginx and httpd, Java images (eclipse-temurin, amazoncorretto), .NET images from mcr.microsoft.com, the plain debian, ubuntu and alpine images, and common vendor images such as the bitnami ones, grafana/grafana and valkey/valkey.
How it compares with Docker Scout
| Tool | What it reads and checks | Use it when |
|---|---|---|
| Docker Scout | A built image: it lists the packages inside it and matches them against vulnerability data, so it finds CVEs in OS packages and libraries. | You have the built image and want every vulnerable package inside it. |
| This check | The Dockerfile and compose text, with nothing to pull or build. Says whether each base runtime, database and Linux release is still supported, flags unpinned tags, and suggests the tag to move to. | Before you build, in review, or to hand your coding agent a list of FROM lines to update. |
A built image: it lists the packages inside it and matches them against vulnerability data, so it finds CVEs in OS packages and libraries.
Use it when: You have the built image and want every vulnerable package inside it.
This check
The Dockerfile and compose text, with nothing to pull or build. Says whether each base runtime, database and Linux release is still supported, flags unpinned tags, and suggests the tag to move to.
Use it when: Before you build, in review, or to hand your coding agent a list of FROM lines to update.
Questions
Does it scan the image for CVEs?
No. It reads the Dockerfile text and checks what your base images are built on. To find vulnerable OS packages and libraries inside a built image, run an image scanner such as Docker Scout on it. The two answer different questions, and an end-of-life base image is the one a scanner can’t fix for you: no more patches are coming.
Why is python:3.8-slim-buster flagged twice?
Because two things under it are past end of life: Python 3.8, and Debian 10 (buster), the operating system the tag pins. Moving to a supported Python on a current Debian release fixes both, which is what the suggested tag does.
What counts as unpinned?
No tag, latest, or a tag that only names a variant or channel (lts, alpine, stable). Those move to a new version without a change to your Dockerfile, so a rebuild can bring a new major version. It’s low priority, with a tag to pin.
Should I pin by digest?
A digest makes builds reproducible. Keep the tag next to it (node:22-alpine@sha256:…): the tag is how we, and you, can tell which version it is and whether it’s still supported.
Why aren’t Rust images checked?
The Rust project supports only its newest release, so every pinned rust:1.x image would read as end of life. Rust crates are checked from Cargo.toml instead.
Go further than one check
Map your repo into an architecture diagram with live health on every part, get an alert when a new critical finding lands, and hand your coding agent a brief to build from. Free to start.