Check your Dockerfile base images for end of life

Paste your Dockerfile, or drop it with docker-compose.yml. Every 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.
Multi-stage builds and compose files work.

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.

Dockerfile
# 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
docker-compose.yml
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 FROM line, with --platform, digests and ARG defaults declared before it. In a multi-stage build, FROM build refers to an earlier stage and isn’t an image, so it’s skipped, as is scratch.
  • Compose image: lines, with ${TAG:-16} defaults filled in. Services that use build: 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.

The tag it suggests

For each image past or near end of life, the check picks the newest supported line, preferring a long-term support line where the product has one, and builds a tag that keeps your variant: node:18-alpine becomes node:24-alpine, not node:24. A Linux release pinned in the tag (-buster, -alpine3.16) moves to a supported one. For official images the tag is confirmed on Docker Hub before it’s suggested.

The deprecated openjdk image points you to eclipse-temurin, one of the official alternatives the openjdk image page lists.

How it compares with Docker Scout

  • 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.

    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.