Patching containers: rebuild images, don't patch in place
You patch a container by building a new image from an updated base and replacing the running containers with it. Don't run apt upgrade inside a running container: the fix vanishes the next time it starts. And patch the host too, because every container uses the host's kernel.
Why patching in place fails
Docker's documentation puts it plainly: images are immutable, and a build is a snapshot taken at that moment. A container started from an image gets a thin writable layer on top. If you run a package update inside it:
- the fix lives only in that one container, not in the image;
- a restart, reschedule or scale-out starts fresh containers from the old, vulnerable image;
- your scanners and your image registry still report the old packages;
- you can no longer say what is actually running.
So the unit you patch is the image. The steps are always the same: rebuild, scan, push, redeploy, and check that the old containers are gone.
Rebuild from an updated base
Most fixes reach you through the base image (the FROM line). Docker Official Images and other maintained base images are updated regularly with fixes. Your build only picks those up if it pulls the new base and doesn't reuse old cached layers:
docker build --pull --no-cache -t registry.example.com/myapp:2026-11-03 .
docker push registry.example.com/myapp:2026-11-03
--pull makes Docker check for and download a newer base image. --no-cache rebuilds every layer, so a cached RUN apt-get install step can't keep old packages. Docker recommends rebuilding regularly, not only when a scanner complains. A weekly scheduled build in CI plus an on-demand build for urgent advisories works for most teams.
If the base publisher hasn't shipped a fixed image yet, you can upgrade packages during the build. The fix then lives in your image:
# Debian/Ubuntu base
RUN apt-get update && apt-get upgrade -y && rm -rf /var/lib/apt/lists/*
# Alpine base
RUN apk upgrade --no-cache
Remove the line once the base image catches up, so your images stay close to what the publisher tests.
Application dependencies are separate. npm, pip, Composer, Maven and Go modules don't change when the base image changes. Update your lock files and rebuild. See Node.js and PHP.
Pin base images, then refresh the pins
Tags are mutable: debian:13 or alpine:3.22 points to a new image every time the publisher rebuilds. A digest is fixed. Docker shows this form:
FROM alpine:3.21@sha256:a8560b36e8b8210634f77d9f7f9efd7ffa463e380b75e2e74aff4511df3ef88c
Pinning makes builds repeatable, but a pinned digest never gets fixes on its own. Use pins only together with a process that updates them, such as a dependency bot that opens pull requests for new digests, or a scheduled job. In that setup --pull doesn't change anything; the pin update is the patch. docker scout recommendations also shows newer base image versions for an image.
Avoid latest in production. Kubernetes' documentation warns that it makes it hard to track which version runs and to roll back. Use a specific tag and/or a digest.
Scan before and after
Scanners list the packages in an image and match them against vulnerability data. Three common free command-line options:
# Trivy: show only HIGH and CRITICAL issues that have a fix
trivy image --severity HIGH,CRITICAL --ignore-unfixed registry.example.com/myapp:2026-11-03
# Grype: update its database, then fail (exit code 2) on high or worse, fixed issues only
grype db update
grype registry.example.com/myapp:2026-11-03 --only-fixed --fail-on high
# Docker Scout: fixable critical and high CVEs
docker scout cves --only-fixed --only-severity critical,high registry.example.com/myapp:2026-11-03
All three can fail a CI job: Trivy with --exit-code 1, Grype with --fail-on, Docker Scout with --exit-code. Filtering to fixed issues keeps the list actionable. A CVE with no fix yet can't be patched by rebuilding, so track it and decide whether a workaround is needed. Results differ between tools because they use different data sources, so pick one and use it consistently. More on scanners is in vulnerability scanning.
Scan what runs, not just what you build. Scan images in your registry on a schedule. New CVEs appear against images that were clean when you built them.
Roll out on Kubernetes
Point the workload at the new image. A new tag or digest triggers a rolling update:
kubectl set image deployment/myapp myapp=registry.example.com/myapp:2026-11-03
kubectl rollout status deployment/myapp
Better still, change the tag or digest in your manifests or Helm values in Git and let your deployment pipeline apply it. That way the next apply doesn't put the old image back.
If you reuse the same tag, existing pods keep the old image. For any tag other than latest, the default pull policy is IfNotPresent, so a node that already has the tag won't pull again. kubectl rollout restart deployment/myapp restarts all pods of the deployment, but nodes that already have the tag reuse their old copy unless the pull policy is Always. That's another reason to use unique tags or digests.
For plain Docker or Compose hosts, pull and recreate the containers. This restarts the services:
docker compose pull
docker compose up -d
Then check what actually runs: docker ps on hosts, or kubectl get pods -o jsonpath='{..image}' on clusters.
Distroless and Alpine notes
- Distroless
- Google's distroless images hold only your app and its runtime dependencies. There is no shell and no package manager, so you can't patch inside them even if you wanted to. Rebuild is the only path. Current images are based on Debian 13 and have names like
gcr.io/distroless/static-debian13. Fewer packages means fewer CVEs to patch. A:debugtag adds a busybox shell for troubleshooting; don't run it in production. - Alpine
- Alpine images are very small (under 6 MB), so they have little to patch. Alpine uses
apkand the musl C library instead of glibc, so some binaries built for glibc won't run. Each Alpine branch has its own support period; rebuild onto a supported branch before yours ends. See Alpine Linux.
The host kernel is shared
Containers are not virtual machines. Docker's documentation says that all containers on a host share the same kernel. A kernel flaw, such as a privilege escalation, can let code in any container affect the host and every other container. Rebuilding images does not fix it; patching the host does.
- Patch the container host's OS and kernel like any other server. Use your distribution's guide in Linux.
- On Kubernetes, drain one node at a time, update and reboot it (reboot), then uncordon it.
- Keep the container runtime (Docker Engine, containerd, runc) updated too; it runs as root on the host.
- Reduce damage from a kernel bug: run containers as non-root, drop capabilities and avoid
--privileged. See server hardening.
Sources
- Building best practices, Docker Docs
- What is a container?, Docker Docs
- docker scout cves, Docker Docs
- Container Image, Trivy documentation
- trivy image, Trivy documentation
- Grype README, Anchore (GitHub)
- Grype CLI reference, Anchore
- Images, Kubernetes documentation
- kubectl set image, Kubernetes documentation
- kubectl rollout restart, Kubernetes documentation
- Distroless container images, GoogleContainerTools (GitHub)