The Docker Ubuntu image is a reliable, well-maintained base for containers, but pulling ubuntu:latest and installing packages carelessly ships a larger, more vulnerable container than you need — the security work is in pinning the tag, minimizing installed packages, and scanning the result. Ubuntu is one of the most popular base images on Docker Hub for good reason: familiar tooling, broad package availability, and a predictable release cadence. Using it well means treating the base image as something you control and audit, not a default you never revisit.
Every package in your base image is attack surface and a potential CVE. A Docker image for Ubuntu that carries a full package set inherits every vulnerability in those packages, whether or not your application uses them. The goal is a container that contains what you need and nothing else.
Pin the tag, never run latest
The first fix is the cheapest. ubuntu:latest is a moving target — it points at whatever the newest release is at pull time, so a build that passed yesterday can pull a different base today and behave differently. Pin to a specific LTS version tag:
# not this
FROM ubuntu:latest
# this — reproducible, explicit
FROM ubuntu:24.04
For stronger guarantees, pin by digest so the exact image bytes are locked regardless of what a tag later points to:
FROM ubuntu:24.04@sha256:<digest-from-your-registry>
Digest pinning means a rebuild fetches byte-identical content, which matters for both reproducibility and supply-chain integrity. You update the digest deliberately when you choose to take a new base, rather than drifting silently.
Keep the package footprint minimal
The most common Dockerfile mistake is leaving the apt cache and recommended packages in the final image. Each adds size and CVEs. Install only what you need, disable recommended extras, and clean the cache in the same layer so it does not persist:
FROM ubuntu:24.04
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates \
curl \
&& rm -rf /var/lib/apt/lists/*
Three details matter here. --no-install-recommends skips packages apt would otherwise pull in "helpfully," often doubling your install. rm -rf /var/lib/apt/lists/* in the same RUN removes the package index cache so it never lands in a layer. And combining update, install, and clean in one RUN avoids a stale cache layer and prevents the classic bug where a cached apt-get update layer serves outdated package versions on rebuild.
Do not run as root
By default a container process runs as root, and the Ubuntu base image is no exception. If your application is compromised, running as root widens what an attacker can do — especially if the container is misconfigured with host mounts or extra capabilities. Create and switch to an unprivileged user:
RUN groupadd --system app && useradd --system --gid app app
USER app
After the USER app line every subsequent instruction and the container's main process run as that unprivileged account. This is one of the highest-value, lowest-effort container hardening steps and it applies to any Ubuntu-based image.
Consider whether you need full Ubuntu at all
Ubuntu is comfortable, but comfort has a size cost. If your workload is a static binary or a language runtime, a smaller base cuts both image size and CVE count dramatically. Options worth weighing:
ubuntu:24.04— full familiarity, largest surface. Good when you need apt and general tooling.- A slim or minimal variant — trims documentation and locale data for a smaller footprint while keeping apt.
- Distroless or
scratch— no shell, no package manager, minimal surface, for compiled applications that need nothing but the binary and its runtime.
The tradeoff is debuggability: a distroless image has no shell to exec into. For many production services that is an acceptable price for a container with almost no attack surface. Choose Ubuntu deliberately because you need its tooling, not by reflex.
Scan before you ship
A pinned, slimmed Ubuntu image can still inherit CVEs from its packages the day a new advisory drops. Scanning is not optional, and it belongs in CI so a vulnerable image fails the build:
# scan the built image for known CVEs
docker build -t myapp:ci .
trivy image --severity HIGH,CRITICAL --exit-code 1 myapp:ci
The --exit-code 1 makes a high or critical finding break the pipeline instead of producing a report nobody reads. Rescan on a schedule too, because an image that was clean at build time accumulates known vulnerabilities as new advisories are published against packages it already contains. Beyond OS packages, if your application layers on language dependencies, software composition analysis covers the libraries a container image scanner may only partially see. For a fuller container hardening walkthrough, our academy has a dedicated track.
FAQ
Should I use ubuntu:latest in a Dockerfile?
No. ubuntu:latest is a moving tag that can pull a different base image on each build, making builds non-reproducible. Pin to a specific LTS tag like ubuntu:24.04, or pin by digest for byte-identical rebuilds and stronger supply-chain integrity.
How do I make a Docker Ubuntu image smaller?
Install only required packages with --no-install-recommends, remove the apt cache with rm -rf /var/lib/apt/lists/* in the same RUN layer, and consider a slim or distroless base if your workload does not need a full package manager. Fewer packages means smaller size and fewer CVEs.
Does the Docker Ubuntu image run as root by default?
Yes. Unless you specify otherwise, the container process runs as root. Create an unprivileged user with useradd and switch to it with USER so a compromised application does not have root inside the container.
How do I check a Docker Ubuntu image for vulnerabilities?
Scan it in CI with a container scanner such as Trivy, using --severity HIGH,CRITICAL --exit-code 1 so serious findings fail the build. Rescan on a schedule as well, since new CVEs are published against packages your image already contains after it was built.