Your Docker Base Images Are Quietly Dead: The FROM-Line Audit Nobody Runs

Last updated: July 16, 2026  ·  For developers and platform engineers

A container image is an operating system frozen in time. That's the whole trick — and the whole problem. Your application code ships daily. Your CI pipeline rebuilds on every merge. Your deploys are so automated you barely think about them. But the FROM line at the top of the Dockerfile? Somebody chose that in 2022, it worked, and nobody has looked at it since.

docker build will never warn you. There is no deprecation notice, no yellow banner, no exit code 1. The build succeeds today exactly like it succeeded three years ago — except the operating system inside stopped receiving security patches somewhere along the way, and every layer you stack on top inherits that silence.

The uncomfortable version: you can have a perfectly modern app — latest framework, patched dependencies, green CI — running on a base OS that has been end of life for years. The registry doesn't care. The build doesn't care. The attackers do.

Why Dead Tags Keep Working

Registries are archives, not lifecycle enforcers. debian:10, ubuntu:20.04, centos:7, node:18-alpine — all of these still pull. All of them still build. Pinning to an old tag was even considered good practice, because it made builds reproducible. It still does. It also means your build reproduces a 2022-era package set, faithfully, forever, with zero new security fixes applied.

Multi-stage builds bury the problem one level deeper. Teams look at their sleek final image and feel good. But the distinction that matters is builder versus runtime: an EOL image in a build stage is a contained risk — it compiles your code and gets discarded. An EOL image in the final stage is the operating system your app actually runs on in production, listening on a port, parsing untrusted input with unpatched libraries. Builder images matter less. Runtime images matter completely.

The Tags You Probably Have, and When They Died

These aren't exotic. They're the most common base images of the early 2020s, and every one of them is past end of life:

Image tag What's inside End of life Status
debian:10, debian:buster-slim Debian 10 "Buster" September 10, 2022 EOL
centos:7 CentOS 7 June 30, 2024 EOL
python:3.8, python:3.8-slim Python 3.8 October 7, 2024 EOL
node:18, node:18-alpine Node.js 18 April 30, 2025 EOL
ubuntu:20.04, ubuntu:focal Ubuntu 20.04 LTS May 31, 2025 EOL
alpine:3.19 Alpine Linux 3.19 November 1, 2025 EOL

Note the compounding cases: node:18-alpine pairs an EOL runtime with whatever Alpine release it was built on. And Alpine cuts releases every six months with roughly two years of support — alpine:3.19 died November 1, 2025, barely two years after it shipped. Alpine tags go stale faster than almost anything else in your stack.

The FROM-Line Audit, Step by Step

Step 1 — Inventory every FROM line

One command, run at the root of your repos (or across a checkout of your whole org):

grep -rh "^FROM" --include="Dockerfile*" . | sort | uniq -c | sort -rn

You get a frequency-sorted list of every base tag in use. Most teams running this for the first time find tags they'd have sworn were retired years ago. Keep an eye out for FROM lines referencing internal base images too — those inherit from something, and that something has an expiry date.

Step 2 — Map tags to actual versions

Translate each tag into the OS or runtime it freezes: debian:buster is Debian 10, ubuntu:focal is Ubuntu 20.04, node:18-alpine is Node.js 18 plus an Alpine release. Untagged or latest-tagged images need a docker run --rm <image> cat /etc/os-release to identify. For stages, only the final FROM in each Dockerfile defines the runtime image — flag those first.

Step 3 — Check every version against EOL data

For a handful of images, the EOL checker does it interactively, and the scanner can sweep a whole environment. For a bulk audit, script it against the API:

curl https://api.endoflife.ai/v1/status/debian/10

One call per product/version pair, machine-readable status back. Loop it over the output of step 2 and you have an org-wide EOL report in minutes.

Step 4 — Fail builds automatically from now on

A one-off audit rots at the same speed as the images it audited. The fix is making EOL status a build failure: the EOL Runtime Check GitHub Action on the GitHub Marketplace fails a workflow when it detects an end-of-life runtime or base, and there are CI examples for other systems at /api. The goal is that the next debian:10 never reaches main — because the pipeline, not a human, is doing the remembering.

Fixing What You Find

In order of preference:

  1. Bump the tag. node:18-alpinenode:22-alpine, python:3.8-slimpython:3.12-slim. For minor jumps this is usually anticlimactic — rebuild, run the test suite, ship. The fear of touching the FROM line is generally much larger than the diff.
  2. Rebase to a supported LTS. Where the jump is bigger — Ubuntu 20.04 to 24.04, Debian 10 to 12 — treat it as a small migration: rebuild, check system-library-dependent code (native extensions, OpenSSL versions), and pin the new LTS tag.
  3. Slim and distroless variants are worth adopting for the smaller attack surface — but be clear about what they do. Fewer packages means fewer potential vulnerabilities, not patched ones. A distroless image built on an EOL release is still an EOL image. It's a better lock on a condemned building.
  4. Genuinely stuck? Some legacy containers can't move — a vendored binary that only links against CentOS 7-era glibc, an app whose rewrite is budgeted for next year. For those, commercial extended support exists precisely as a bridge: paid security patches for the EOL base while you finish the real migration. A bridge, not a residence.

The Part Your Scanner Won't Tell You

Here's the kicker that makes this audit worth running even if you already scan images: vulnerability scanners enumerate known CVEs against the packages in your image. What many of them won't surface is that the distribution behind those packages has stopped evaluating vulnerabilities at all. When an OS goes end of life, new CVEs may never be analyzed, backported, or in some cases even recorded against those package versions — so the scan comes back deceptively quiet. A clean report on a dead base image isn't a clean bill of health; it's an absence of coroners. We've written about this CVE blind spot in depth — for containers, the FROM line is where it hides.

Frequently Asked Questions

Do Docker images stop working when the base OS reaches end of life?

No — and that's exactly the problem. Tags like debian:10 or centos:7 remain pullable and buildable indefinitely. The registry keeps serving them; nothing fails. The image simply stops receiving security patches, so every new CVE against the base OS goes permanently unfixed inside your container.

How do I find every base image used across my repositories?

Grep for FROM lines across all Dockerfiles: grep -rh "^FROM" --include="Dockerfile*" . | sort | uniq -c | sort -rn. That gives you a frequency-sorted inventory of every base tag in use. Then map each tag to its underlying OS or runtime version and check each one on the checker or via the API.

Do distroless or slim images fix an EOL base image?

No. Distroless and slim variants reduce attack surface by shipping fewer packages, but they're still built from a specific OS release. If that release is end of life, the packages that remain are just as unpatched as they'd be in the full image. Smaller surface, same expiry date.

How do I automatically block EOL base images in CI?

Use the EOL Runtime Check GitHub Action from the GitHub Marketplace to fail builds on end-of-life runtimes and bases, or call the API directly from any CI system — e.g. curl https://api.endoflife.ai/v1/status/debian/10. CI integration examples live at /api.

Related Resources