Images are snapshots. The OpenSSL, glibc and language runtime inside your production image are the versions that existed on the day it was built, and they stay that way no matter how many patches upstream ships. A service whose code hasn’t changed in six months is running six-month-old system libraries. That’s why “how do you keep images patched?” is a common senior-level interview question, and why “we use the official image” isn’t an answer.
Some real numbers from images on a development machine help ground this. Here are the creation dates of a few pulled tags, inspected in October 2026:
for i in alpine:3.20 node:22-bookworm nginx:alpine postgres:16-alpine node:20-bullseye; do
echo "$i created=$(docker image inspect -f '{{.Created}}' $i | cut -c1-10)"
donealpine:3.20 created=2026-04-16
node:22-bookworm created=2026-09-23
nginx:alpine created=2026-09-02
postgres:16-alpine created=2026-08-13
node:20-bullseye created=2026-04-22Two of these stopped moving in April. That’s not a coincidence, and it’s the first thing this guide is about.
Know which tags are still alive
Official images are rebuilt by their maintainers when the upstream distribution publishes security updates or the software has a new patch release. That only happens while the version is supported. Node.js 20 reached end of life at the end of April 2026, so node:20-bullseye was last built a week before that, and no one will rebuild it for the next OpenSSL advisory. Alpine supports each release branch for about two years, and 3.20’s support ended in spring 2026. The tags still pull fine. Nothing warns you. They just stop receiving fixes, and a scanner will show the CVE count climbing month by month.
So the maintenance job starts with an inventory: for each image you build, which base, which version of the language runtime, which distribution release, and when each goes end-of-life. Inside an image, the answers are a command away:
$ docker run --rm alpine:3.20 cat /etc/alpine-release
3.20.10
$ docker run --rm node:22-bookworm sh -c 'node --version; cat /etc/debian_version; dpkg -l | grep -c ^ii'
v22.23.3
12.15
413That’s Node 22, Debian 12 (bookworm), and 413 installed Debian packages, every one a potential CVE entry. Moving to node:22-bookworm-slim or a distroless base reduces that count sharply, which is the most effective maintenance step there is: packages you don’t ship never need patching. The distroless note compares the options.
Rebuilding is the only way patches arrive
Upstream rebuilt node:22-bookworm with Debian’s latest libssl3. Your image built last month still contains the old one. To pick it up you must rebuild, and the build cache works against you in two ways.
First, the local base image. If the builder already has node:22-bookworm, a plain docker build may reuse the copy it has instead of checking the registry for a newer one. docker build --pull forces a check and fetches the new base if the tag moved.
Second, cached RUN steps. A step like RUN apt-get update && apt-get install -y curl is cached by its command text, so it won’t rerun just because the Debian archive has newer packages (the layer cache note explains why). If the base changed, every step after FROM rebuilds anyway. If you rely on your own apt-get or apk installs for patched packages, add --no-cache to scheduled builds.
The practical policy:
- Scheduled rebuilds. Rebuild and redeploy every service at least weekly with
--pull --no-cache, even when nothing in the repository changed. Run the test suite, because a patched base can still break you. - Event-driven rebuilds. When a serious advisory lands (a glibc or OpenSSL CVE, say), rebuild everything that contains the package, which requires knowing what contains it. An SBOM per image (
docker buildx build --sbom=true, ordocker scout sbom) answers that in minutes instead of days. - Don’t patch running containers.
docker exec ... apt-get upgradefixes nothing durable; the next restart from the image brings the old packages back.
Pin, then automate the bump
Rebuilding against a floating tag means production gets whatever the base looked like at build time, untested until it’s deployed. Pinning by digest makes builds deterministic:
FROM node:22-bookworm-slim@sha256:<digest>A pin without automation just freezes you in place, which is worse than floating. The working combination is pin plus a bot. Renovate or Dependabot watches the registry, and when node:22-bookworm-slim gets a new digest it opens a pull request changing the pin. CI builds and tests it, someone merges it, and the deploy goes out like any other change. You get patches within days, a record of exactly when each base changed, and a one-line revert if a base update breaks something. Configure the bot to also propose major-version changes (Node 22 to 24), as separate pull requests that need real review.
Application dependencies follow the same logic. Lockfiles (package-lock.json, poetry.lock, go.sum) make installs reproducible, and the same bots keep them current. The image and its dependencies age together, so treat them with the same process.
Should the Dockerfile run apt-get upgrade?
This comes up in interviews as a judgement question. Docker’s best-practices guide has historically recommended against apt-get upgrade in Dockerfiles, preferring to rely on the base image maintainers. The counter-argument is that it picks up fixes published after the base was last rebuilt. Reasonable positions:
- With an actively maintained official base and frequent rebuilds, skip it. The base gets the same updates within days, and your builds stay closer to what the maintainers tested.
- When you must ship before the base catches up (an urgent CVE), a targeted
apt-get install --only-upgrade libssl3is more controlled than a blanket upgrade. - Never use it as a substitute for moving off an end-of-life base. Once a distribution is out of support, there are no upgrades to install.
Scanning closes the loop
A scanner (Docker Scout, Trivy, Grype) compares an image’s packages against vulnerability databases. Scan in CI to block new critical issues, and scan the registry continuously, because an image that was clean at build time gains CVEs as advisories are published. Expect noise: many findings are in packages your code never calls, or have no fix yet. Prioritise by fix availability and exposure rather than raw counts. The image scanning note goes into triage.
Interview practice
A security review finds that 40 of your 60 services run images with critical OpenSSL CVEs, though the base images upstream were patched three weeks ago. Most services haven’t had code changes in months. Some use FROM python:3.9. Explain how this happened and design a process so it doesn’t recur.
Answer
Images only change when rebuilt. Services without code changes had no reason to trigger CI, so their images still contain the OpenSSL from their last build. Even services that were rebuilt may have reused a cached local base or cached RUN layers. And python:3.9 is past end-of-life (October 2025), so its tag stopped receiving rebuilds; rebuilding against it won’t help.
The process: first, an inventory of base images per service from SBOMs or a registry scan. Pin bases by digest and run Renovate so base updates arrive as pull requests. Add a scheduled weekly pipeline that rebuilds every service with --pull --no-cache, tests, and deploys, independent of code changes. Scan the registry continuously and alert on fixable criticals in running images. Track runtime and distribution EOL dates and schedule migrations (Python 3.9 to a supported version) before they lapse. For the immediate incident, rebuild all 40 now with --pull, starting with internet-facing services, and migrate the 3.9 services as a priority since rebuilding alone won’t fix them.
Keep going
- Docker interview questions
- Docker MCQ practice
- Docker image vulnerability scanning
- Docker image tags and reproducible releases
- Official reference: Building best practices