This article provides an overview of free and open-source minimal container images for bare Linuxes (into which you would copy native binaries compiled with C/C++, Go, or Rust), PHP, Python, Java, C#, and Node.js, from the image vendors Google distroless, Chainguard, Ubuntu, and Azure Linux. It explains why minimal images matter, how they are defined, and their general pros and cons.
Container image security series
This article is part of a multi-part series:
- Part 1: Fallacies of image scanners: explains how scanners work and which false positives/negatives they produce. Also available in German on heise online.
- Part 2: Minimal container images (this article): provides a list of off-the-shelf, free (and paid) minimal images for bare Linux, PHP, Python, Java, C#, and Node.js. Also available in German on heise online.
- Part 3: Building custom minimal container images: how to build your own minimal images based on Chainguard/WolfiOS, Ubuntu Chiseled, and Azure Linux
- Part 4: Choosing the best container image: discusses 8 selection criteria for images, describing what they are, why they matter, and how to evaluate them
Introduction
Many software development teams build or run container images based on “official” images that are hosted on Docker Hub. However, those teams are not aware that there is a significant chance that these images contain (too) many components, which increases true and false positive CVE findings from scanners such as Trivy or Grype. While such official images are typically free, teams incur high costs to analyze each reported positive CVE for correctness (true vs. false positive) or risk that the image contains exploitable CVEs.
Minimal container images are a solution to this problem. Also known as “distroless” or “chiseled” images, they omit components like shells or package managers, which are typically not needed at runtime. They significantly reduce the attack surface, thereby enhancing security: the fewer components that exist in an image, the fewer potential vulnerabilities there are.
This article helps DevOps engineers who are in charge of choosing container images, reducing their research time considerably. It first defines the term “Minimal Images” and distinguishes it from “Slim” images. After explaining the advantages and disadvantages of minimal images, the article provides a list of free and paid minimal images for bare Linux, PHP, Python, Java, C#, and Node.js.
What are minimal images?
Minimal images are images that only contain the bare minimum components needed to run the primary component (exemplary, in the postgres image, the PostgreSQL server would be the primary component). The goal is to reduce the attack surface for hackers as much as possible. The fewer components exist in an image, the fewer potential vulnerabilities an attacker could exploit.
Several alternative terms have emerged for “minimal images”, e.g., “distroless images”, “hardened images”, or “chiseled images”. What all these images have in common is that they typically exclude certain components, such as:
- A (Linux-distro-specific) package manager, e.g.
apt
for Debian/Ubuntu,apk
for Alpine, ordnf
/yum
for RedHat - Various (Linux-distro-specific) packages that are typically installed by default (and present in, say,
ubuntu:latest
) but not required to run an application. Examples are binaries and libraries forperl
,grep
orgzip
(included by Ubuntu). Omitting such components (and the package manager) is what makes an image “distroless” - A shell, e.g.
/bin/bash
or/bin/sh
- Debugging tools, such as
curl

Minimal images vs. slim images
While slim images (e.g. python:3.13-slim
) are typically smaller than the non-slim variant, they do contain a shell and a package manager, and thus they allow an attacker to dig deeper into your system, assuming they got access to your container!
Pros & cons of minimal images
If minimal images only had advantages, everyone would use them.
Pros | Cons |
✅ Faster image startup | ❌ Harder to debug |
✅ Fewer vulnerabilities & triage work | ❌ Increased testing effort |
✅ Improved security | ❌ Increased research time |
Pros:
- Faster image startup: Minimal images are very small, and thus start faster, because downloading and uncompressing them takes less time
- Fewer vulnerabilities & triage work: Vulnerability scanners like Trivy or Grype will, on average, report much fewer vulnerabilities compared to non-minimal images. Consequently, your development team will be able to spend more time on coding, rather than investigating (triaging) potential false positives scanner findings (see previous article)
- Improved security: if an attacker manages to exploit a vulnerability of the software running in the image (e.g., a PostgreSQL vulnerability in a PostgreSQL image), they won’t be able to execute arbitrary shell commands, because there is no shell in the image. Thus, they cannot easily download additional software that they could use to further infiltrate your (virtual) infrastructure. Another advantage is that vulnerability scanners, such as Trivy, correctly identify the components present in most minimal images, which avoids false negatives.
Cons:
- Harder to debug: If you have a production incident, developers or operations teams often want to access the shell in the container to run commands that help them diagnose or solve the incident. However, your deployed minimal images lack a shell! There are workarounds, though, such as kubectl debug or the (more user-friendly) cdebug tool. Both start an ephemeral sidecar container that shares the process namespace and file system with the container you want to debug, making it feel as if you were inside the container.
- Increased testing effort: when you swap the official (non-minimal) base image of your containerized application with a minimal image, you must test for incompatibilities. This is especially true for multi-stage builds that you create for language runtime-based images (e.g., Python, Node.js, or C#/.NET). In a multi-stage build, you install dependencies and compile your app in a “build” stage image, and then copy the resulting files into a “final” stage that uses a minimal image. If your “build” stage uses the official (non-minimal) image, subtle, low-level incompatibilities may exist between your build stage and the final stage. For instance, the minimal final stage may lack specific (native) libraries or binaries that your compiled app requires. Or the path to the language runtime binary is different in the final stage compared to the build stage, causing the container startup (its
ENTRYPOINT
) to fail. Also, since minimal images lack a shell, you cannot run two or more commands on the container startup or run an entrypoint shell script. - Increased research time: Even though the remainder of this article will drastically reduce your research time to find the most suitable minimal image, you may still have to do further research for your specific circumstances. You might find that there is no suitable off-the-shelf image, so you have to build one yourself, which is not trivial.
Available minimal (base) images
At the time of writing, minimal images are unfortunately still a “niche market”. The official maintainers of base images (such as Python) or standalone off-the-shelf software (such as PostgreSQL) don’t bother to build minimal images (or don’t know how), for whatever reason.
Instead, there are third parties who specialize in providing minimal, hardened images. Some of them are free, some are commercial. Commercial examples include Chainguard, Rapidfort, Docker, SecureBuild, Minimus, or Bitnami.
The following sections explore available base image options for various programming language runtimes and for a “bare” Linux. In other words, the images you would use in a FROM
statement in your Dockerfile
, to base your applications on.
When researching alternatives, consider the following selection criteria:
- Are the minimal images frequently rebuilt (say, every few days)? Disregard images where this is not the case. You can use the Docker Tag Monitor to determine the build frequency of specific tags (more details are available in this blog post)
- Are the images cryptographically signed, e.g., with Cosign? If not, you cannot verify whether the image has been tampered with!
- Can vulnerability scanners (Trivy, etc.) correctly identify the components and match them against vulnerability databases? If not, you would not know if the minimal image is vulnerable. See the previous article to learn more about these kinds of false negatives.
For all images presented below, these criteria are met, unless explicitly stated otherwise.
The following table provides an overview of the available open-source minimal image vendors (see columns 2-6) per use case (column 1):
Use case | Google Distroless | WolfiOS / Chainguard | Ubuntu Chiseled | Azure Linux Distroless | Others |
Bare Linux | ✅ (4 variants) | ✅ (2 free variants, 1 paid, which can be built for free yourself) | ⚠️ (build yourself) | ✅ (2 variants) | — |
PHP | ❌ | ✅ (newest upstream) | ❌ | ⚠️ (v8.3 when building yourself) | — |
Python | ✅ (v3.11, no ctypes support) | ✅ (newest upstream) | ✅ (v3.10, v3.12)⚠️ v3.13 when building yourself (Ubuntu 25.04) | ✅ (v3.12) | — |
Java JRE | ✅ (v17, v21) | ✅ (newest upstream) | ✅ (v8, v11, v17, v21) | ⚠️ (Full JDKs for v8, v11, v17, v21) | jlink |
C# / .NET | ❌ | ✅ (newest upstream) | ✅ (.NET v6-10, by Microsoft) | ✅ (.NET v6-10, by Microsoft) | — |
Node.js | ✅ (v20, v22) | ✅ (newest upstream) | ⚠️ (v18 which is End-Of-Life⚠️ (v20.18.1 when building yourself for Ubuntu 25.04) | ⚠️ (v20.14 when building yourself) | — |
Bare Linux
A “Bare Linux” is an image that contains just a few very basic Linux components that your application might need. The assumption is that you build natively compiled binaries, with compilers such as Rust, Go, or C/C++, so no language runtime (such as Python) is needed. Such native binaries typically still require certain components and files to exist in the base image, such as a non-root Linux user, time zone data, SSL Root CA certificates, or various system libraries (e.g., glibc or openssl).
- Google’s distroless:
- The distroless project offers a wide variety of different Debian-based images. While the official README lacks a proper explanation of which components each of the different image variants contains, this article explains it well. Take a look at this figure, where the variant with the fewest components is shown at the top.
- All these distroless images come with several tags: “latest” runs as root user, but there is also a “nonroot” tag, and a “debug-latest” and “debug-nonroot” tag, which both include a shell.
- While you can (theoretically) also build these images yourself, you would first have to sift through the complexity of the Bazel build scripts. Thus, using the pre-built images is recommended, since they are automatically rebuilt whenever Debian releases package updates.
- WolfiOS / Chainguard
- WolfiOS is essentially a huge collection of (open source) Linux packages maintained by Chainguard. They create builds (and rebuild them with included security patches) for the most recent versions of these packages. Chainguard then builds container images from these packages.
- The Chainguard documentation explains which pre-built image variants exist for bare Linuxes (e.g.,
cgr.dev/chainguard/wolfi-base
orcgr.dev/chainguard/static
). Note that some variants are not freely available (“Paid container”). - By default, WolfiOS images run as the root user, but there is a “nonroot” user and group included that you can switch to.
- Part 3 of this series discusses how you can build Chainguard images yourself with little effort, including those that are marked as “Paid container” in the documentation.
- Ubuntu chiseled
- As this blog post explains, Ubuntu introduced a new build tool named Chisel. Chisel is a CLI that you provide with the Ubuntu distro version (e.g., “22.04”) and a list of “slices”, which Chisel assembles into a new root file system that you can copy into an empty
scratch
image. The Ubuntu team split a selected set of official Ubuntu packages into several slices, which you can inspect here. If you are missing slices for a specific package, just open a GitHub issue. - Ubuntu/Canonical does not offer any prebuilt “Bare Linux” images. Part 3 of this series discusses how to build your own chiseled images.
- As this blog post explains, Ubuntu introduced a new build tool named Chisel. Chisel is a CLI that you provide with the Ubuntu distro version (e.g., “22.04”) and a list of “slices”, which Chisel assembles into a new root file system that you can copy into an empty
- Azure Linux Distroless
- Microsoft created Azure Linux in 2020, when it was still called “Common Base Linux (CBL) Mariner”. The reasons are explained here. Microsoft regularly rebuilds Distroless variants of Azure Linux to the MCR for the CPU architectures AMD64 and ARM64.
- Microsoft publishes 3 Bare Linux image variants documented here. Variant 1, minimal, only contains very basic data such as root CA certificates and time zone data. Variant 2, base, includes glibc and OpenSSL. Variant 3, debug, adds “busybox” – so it includes a shell, making it non-minimal.
- The images pushed by Microsoft are digitally signed using Notation. The documentation of the distroless OpenJDK images explains the commands you need to run to verify OpenJDK images with the Notation CLI. Just add any other Azure Linux Distroless image (such as “
mcr.microsoft.com/azurelinux/distroless/base
”) to theregistryScopes
list in thetrustpolicy.json
file (from the documentation) to verify any other Microsoft image. - All variants include a “nonroot” user, but it is not activated by default, so you need to switch to it via a
USER
statement. As explained in part 3 of this series, you can also build customized distroless images yourself.
Finding the most suitable minimal Linux image for your application is essentially a trial-and-error process. You have to try different image variants (that successively contain more components) until your application works as expected, starting with the most minimal variant. For instance, if you choose Google’s distroless project, start with the static
image, then base-nossl
, then base
, then cc
. In case of Chainguard, start with the static
image, then glibc-dynamic
.
Here is an example that demonstrates a multi-stage build using Google’s distroless, including the verification of the authenticity of the base image’s signature:
ARG BASE_IMAGE=gcr.io/distroless/static:nonroot
FROM alpine:latest AS image-verifier
RUN apk add -u cosign
ARG BASE_IMAGE
RUN cosign verify $BASE_IMAGE \
--certificate-identity keyless@distroless.iam.gserviceaccount.com \
--certificate-oidc-issuer https://accounts.google.com
RUN touch /marker
FROM golang:1.24 AS build
# or some other base image
# Insert your commands here that build your binary
FROM $BASE_IMAGE AS final
COPY --from=build /path-to-binary-in-build-stage /path-to-binary-in-final-stage
# force building the image-verifier stage
COPY --from=image-verifier /marker /ignore-me
ENTRYPOINT ["/path-to-binary-in-final-stage"]
CMD ...
Code language: Dockerfile (dockerfile)
PHP
Various PHP images exist that are not minimal (according to the above definition), but quite secure by default and frequently rebuilt. Examples include the Alpine-based tags of the official php image or the ServerSideUp images.
As for open-source minimal images, Chainguard images seem to be the only available option. Ubuntu’s Chiseled project does not offer PHP slices yet, Google’s distroless project does not offer a PHP image either, and while Azure Linux Distroless does offer PHP packages, their use is not documented. Searching the internet for other PHP minimal/distroless images yields only variants that have been unmaintained for several years.
One particular specialty in PHP is PHP extensions that you might need to install into your PHP image. When using non-minimal images, there are Bash scripts that install such extensions, e.g., docker-php-ext-install (included in the official php image) or install-php-extensions. Chainguard’s php image already comes with various pre-installed extensions (see documentation). If it lacks an extension you need, chances are high that Chainguard already prebuilt a WolfiOS package for that extension. The documentation explains how to find the extension package. The WolfiOS packages follow the naming scheme
“php-<php-version>-<extension-name>-<extension-version>
”, e.g. “php-8.4-xml-8.4.6-r0”. Part 3 of this series describes how to build WolfiOS-based images yourself, so you can build a customized minimal PHP image that contains only those PHP extensions you need. Chainguard’s documentation also explains how to use multi-stage builds, allowing you to get rid of composer
in your final image.
Python
The following minimal images exist for Python:
- Google’s distroless Python image
- The distroless
gcr.io/distroless/python3-debian12
image is a Debian 12 (Bookworm)-based base image into which the Python 3 package is installed from the official Debian repository. The specific Python 3 minor/patch version is the one offered by the corresponding Debian release. At the time of writing, Debian Bookworm comes with Python 3.11.2, which is rather old, given that Python 3.13 has been out for over 8 months. - The advantages of this image are that it is regularly rebuilt (see here) and it is digitally signed with Cosign. Its main drawback is that the Python interpreter is stuck at this old 3.11.2 version. Also, you cannot use the ctypes module to communicate with native C libraries. While you might not be using
ctypes
directly in your code, there is a chance that one of your Python dependencies does use it.
- The distroless
- Ubuntu chiseled Python image
- Canonical, the makers of Ubuntu, regularly build and push a Python 3.10 image (based on Ubuntu 22.04, whose official repository contains Python 3.10.12) and a Python 3.12 image (based on Ubuntu 24.04, whose repository contains Python 3.12.3). The prebuilt images are available at ubuntu/python. To verify the specific Python version, run a command such as “
docker run --rm -it --entrypoint='' ubuntu/python:3.12-24.04 python3
” which starts an interactive Python shell (REPL), which also prints the Python version. The Docker Hub page indicates that Canonical will only create and push builds for the Ubuntu LTS versions. Thus, the next version will be published when Ubuntu releases 26.04 in April 2026, most likely with Python 3.13 or 3.14. - The prebuilt images provided by Canonical are not digitally signed.
- Canonical builds the
ubuntu/python
image with a separate tool, rockcraft. As part 3 of this series explains, the downside of this approach is that vulnerability scanners like Trivy are unable to correctly detect the Debian packages in the image, making the image appear more secure than it really is. Consequently, it is recommended to build your own Ubuntu Chiseled Python image.
- Canonical, the makers of Ubuntu, regularly build and push a Python 3.10 image (based on Ubuntu 22.04, whose official repository contains Python 3.10.12) and a Python 3.12 image (based on Ubuntu 24.04, whose repository contains Python 3.12.3). The prebuilt images are available at ubuntu/python. To verify the specific Python version, run a command such as “
- Chainguard’s Python image
- Chainguard offers a minimal run-time Python image. In contrast to the previous solutions, Chainguard offers every existing Python version within days after their release.
- If you want to use a prebuilt image and don’t want to pay Chainguard for a subscription, you can only access the
latest
tag. Doing so for your production images is not recommended because Chainguard quickly adopts the newest versions of open source packages like Python. Consequently, thelatest
tag will point to Python 3.14 (or whatever the newest 3.X release is) shortly after it is publicly available. This is problematic because it is likely that many of your Python dependencies will have compatibility issues with the newest Python version shortly after its release (these incompatibilities typically take a few weeks to be resolved). - Alternatively, you can build your own Chainguard Python 3 image with little effort, as explained in part 3 of this series.
- Azure Linux Distroless Python image
- The Azure Linux package ecosystem includes a Python 3.12 package. Microsoft builds both a distroless image and a non-minimal image that is meant to be used in the build stage of a multi-stage build. It is recommended to use Microsoft’s non-minimal image in your build stage (over the official python image) to avoid compatibility problems discussed in the above section “Pros & cons of minimal images”.
- Microsoft regularly rebuilds its images (see here) and cryptographically signs them with Notation. Alternatively, you can build your own Azure Linux Distroless Python 3 image with little effort, as explained in part 3 of this series.
- The prebuilt images include a
root
andnonroot
variant, as shown on the tags page.
Irrespective of which of the 4 variants you choose, you need to do a multi-stage build to create the image that includes your Python code. None of the minimal images include a Python package manager such as pip
.
Which of these images is best suited depends on your use case. If you need to both control the Python version (by pinning image tags) and use new Python versions, there is no alternative to Chainguard images (or other paid providers such as Bitnami Secure Images). Either you pay for a subscription, or you are willing to set up your own build infrastructure (such as a daily CI/CD pipeline).
Java
The following minimal image options exist for Java Runtime Environments (JRE):
- Custom-built JREs using jlink:
jlink
is a command-line tool (which is part of the JDK) that builds a custom JRE that bundles only those Java modules that your code needs. Various tutorials and examples illustrate how to usejlink
in a multi-stage image build. Even the Eclipse Temurin and Microsoft’s OpenJDK documentation demonstrates how to usejlink
.- To build a truly minimal JRE image that lacks a shell and package manager, the idea is to build your custom JRE with jlink, and then copy it (together with your application’s jar file) into a minimal bare Linux base image (discussed above).
- The main disadvantage of this approach is that vulnerability scanners such as Trivy are unlikely to detect the JRE correctly. Thus, Trivy cannot detect vulnerabilities of the JRE. Also, this approach requires a bit of experimentation to get the
jlink
settings right.
- Google’s distroless Java image:
- At the time of writing, Google’s distroless project provides an OpenJDK 17-based JRE and an Eclipse Temurin 21-based JRE, based on a minimal Debian 12 (Bookworm) image.
- These images are regularly rebuilt (see here) and are digitally signed with Cosign. In contrast to the distroless Python image, the JRE images offer the newest minor/patch versions of the JRE (instead of an old pinned JRE version of the official Debian repository). However, since you can only pin the
latest
tag in the “FROM …” statement in your Dockerfiles, you cannot control which specific minor and patch JRE version your builds will use! In most cases, this should not be problematic, though.
- Ubuntu Chiseled:
- At the time of writing, Canonical regularly builds and pushes OpenJDK-based JREs of the following versions to ubuntu/jre. Beware that the prebuilt images lack cryptographic signatures!
- The availability of other JRE major versions is limited: Canonical only offers those JREs available in the official Ubuntu distro repositories. The ubuntu/jre Docker Hub page indicates that Canonical only creates and pushes builds for the Ubuntu LTS versions. Thus, the next JRE version will be released with Ubuntu 26.04 in April 2026, with the version that Canonical chooses to package at that time.
- Interestingly, Canonical does not build the JRE 17/11/21 images natively with their Chisel CLI (using the JRE Chisels, such as openjdk-21-jre-headless), but they instead use the
jlink
approach discussed above. The consequence is that vulnerability scanners like Trivy cannot detect the JRE correctly, and won’t find vulnerabilities in the JRE. - For this reason, it is recommended to instead build Ubuntu-Chisel-based JRE images yourself, using the Chisel CLI, as discussed in part 3 of this series.
- Chainguard’s JRE image:
- Chainguard offers a minimal JRE, based on OpenJDK. In contrast to Ubuntu’s Chiseled JRE images, Chainguard offers every available JRE version, including 22, 23, or 24. Once a new OpenJDK major version is released, Chainguard will adopt it within days or weeks.
- If you want to use a prebuilt image and don’t want to pay Chainguard for a subscription, you can only access the
latest
tag. Doing so for your production images is not recommended because Chainguard quickly adopts the newest major versions of OpenJDK. Consequently, thelatest
tag will point to OpenJDK 25 (or some other newer major release) shortly after it is available, which might be incompatible with your code. - Alternatively, you can build your own Chainguard JRE image with little effort, as explained in part 3 of this series.
- Azure Linux Distroless JDK image
- Microsoft publishes their builds of the OpenJDK for versions 8, 11, 17, and 21 to “
mcr.microsoft.com/openjdk/jdk:<tag>
“. You can find the source here. There are various variants (including non-minimal ones). For the minimal builds, the<tag>
is “<version>-distroless
” (e.g. “21-distroless
”). - Note that these builds include a full JDK (not just a JRE), so you could consider the image to be “non-minimal”. However, Microsoft’s distroless JDK image is only about 20% larger than Chainguard’s JRE image.
- Microsoft regularly rebuilds the images (see here) to include newer minor/patch versions or security fixes.
- The JDK images are signed with Notation. The documentation explains the commands you need to run to verify the signatures.
- Microsoft publishes their builds of the OpenJDK for versions 8, 11, 17, and 21 to “
If Google distroless or Azure Linux Distroless images don’t work for you, you will have to build your own JRE image using one of the above approaches, unless you are willing to pay for a Chainguard (or other vendors) subscription for prebuilt images.
Also, beware of red herrings like the 21-jre-ubi9-minimal
version tag of the Eclipse Temurin image. That image is not minimal! It contains a package manager, shell, and debug tools. The only advantage of that image is its small size.
C#/.NET
For .NET apps, Microsoft provides free official builds of minimal .NET runtime images (see documentation). For SDK images, no minimal image variants exist, because SDK images are only used during the build stage of a multi-stage build and are not meant to be minimal anyway.
The basic idea is that you replace “normal” (non-minimal) runtime images, such as “mcr.microsoft.com/dotnet/aspnet:9.0
”, with minimal images such as “mcr.microsoft.com/dotnet/aspnet:9.0-noble-chiseled
”. There are several minimal base image flavors you can choose from:
- Ubuntu Chiseled-based images (see documentation) that have “
chiseled
” in their tag name - Azure Linux distroless images (see documentation) that have “
distroless
” in their tag name. They tend to have even fewer (short-term) vulnerabilities found by scanners like Trivy, compared to the Ubuntu Chiseled images
For both base image flavors, there are different tags available (extra
and composite
), which include some additional files that you might need in some cases, as documented here.
Aside from the official Microsoft images, you could also use Chainguard’s aspnet-runtime or dotnet-runtime images, but there is no compelling reason to do so.
Node.js
For Node.js, the minimal image vendors are the same as for Python:
- Google’s distroless Node image
- The distroless project provides Node LTS versions 20 and 22 on a minimal Debian 12 (Bookworm) base image.
- These images are regularly rebuilt (see here) and are digitally signed with Cosign. In contrast to the distroless Python image, the Node images offer the newest minor/patch versions of Node.js (instead of an old pinned Node version from the official Debian repository). However, since you can only pin the
latest
tag in the “FROM …” statement in your Dockerfiles, you cannot control which specific minor and patch version your builds will use! In most cases, this should not be problematic, though.
- Ubuntu chiseled Node image
- Canonical regularly builds and pushes a Node 18.19.1 image (from the Ubuntu 22.04 repository) to ubuntu/node. This prebuilt image is not digitally signed. Also, Node 18 is already End-of-Life, thus this image is basically worthless!
- Like Canonical’s chiseled Python image, the Node image is also built with Rockcraft, and thus vulnerability scanners like Trivy are unable to correctly detect the Node.js Debian packages in the image, making the image appear more secure than it really is. Consequently, it is recommended to build your own Ubuntu Chiseled Node image, as further explained in part 3 of this series. This allows you to get a newer Node version by referencing Ubuntu 24.10 which comes with Node 20.16.0.
- Chainguard’s Node image
- Chainguard’s minimal Node image is not actually minimal. To remain compatible with the official node image, Chainguard’s image includes additional packages, such as the NPM package manager or a basic shell. Thus, it is recommended not to use the prebuilt images, but to build your own Chainguard Node image that excludes these additional packages, as explained in part 3 of this series.
- The primary advantage of Chainguard images is the availability of all (new) Node versions, published shortly after the respective Node.js version becomes available.
- Azure Linux Distroless Node image
- The Azure Linux package ecosystem includes Node.js 20, for which Microsoft builds both a distroless image and a non-minimal image, meant to be used in the build stage of a multi-stage build. It is recommended to use Microsoft’s non-minimal image (over the official node image) in your build stage to avoid compatibility problems discussed in the above section “Pros & cons of minimal images”.
- Microsoft regularly rebuilds its images (see here) and cryptographically signs them with Notation.
- The prebuilt images include a
root
andnonroot
variant, as shown on the tags page. - Similar to Chainguard’s image, the Azure Linux distroless Node image is not strictly minimal because it contains the
npm
package manager. However, you can easily build your own image that excludesnpm
as discussed in part 3 of this series.
If you strictly don’t want to build your own Node base image, then Google’s distroless image should be your first choice. Otherwise, try to build Chainguard or Ubuntu images (in no particular order). In all cases, you will need to create multi-stage builds, as demonstrated here.
Node.js images for frontend projects
If you are using Node.js only for your frontend, then you don’t need a minimal Node image. Instead, you should use a multi-stage build that compiles your application down to a HTML+JavaScript bundle in the “build” stage, and copy that bundle to a web server image (such as NGINX) in the “final” stage. See here for a complete example.
Reproducible base image updates
Many of the images presented here have static tags that point to one specific build today, and to another (updated) build of the image tomorrow. Examples are public prebuilt images such as “cgr.dev/chainguard/static:latest
”, “gcr.io/distroless/nodejs22-debian12:latest
” or “ubuntu/jre:17-22.04_edge
”.
The problem with referencing such static tags in your Dockerfile
‘s FROM
-line is that you might run into reproducibility issues, where the resulting image works today, but tomorrow’s rebuild stops working, even though your code did not change. For instance, if the Google distroless Node.js image changes, there is a high chance that the contained Node minor or patch version was bumped, which can potentially lead to incompatibilities with your code.
To solve this problem, you should pin both the tag and the digest in the FROM
-line, using the format “<image>:<tag>@sha256:<digest>
”. Use a tool such as Renovate Bot or Dependabot, which detects outdated digests and creates a PR that updates the digest. In case the digest update introduces problems (such as the aforementioned incompatibility problem), either the Renovate/Dependabot PR catches it, or your users catch it “in production”, so you can revert the change using Git.
To determine the exact digest value of a tag, there are many different ways. One example is to run “docker pull <image:tag>
”, which prints the image digest after the pull has completed.
Conclusion
Minimal images have clear advantages in terms of security. But adopting them is costly because it requires expertise. You either “outsource” the expertise by paying providers like Chainguard to build and maintain minimal images for you. Or your staff gains expertise, researching which existing free minimal images to use, or how to build them themselves. In the latter case, costs are reflected in your staff salaries.
Currently, the industry of secure container images is still consolidating. Bitnami was the first to go (bought by VMWare, which Broadcom bought). Its free public images have been gutted to the “latest” tag in August (see announcement). And since Chainguard is a start-up, Chainguard soon will also be bought by some mega-corp that will integrate it into their portfolio, removing the availability of free/open source work (i.e., WolfiOS).
In the long run, secure minimal container images will hopefully become the default, with images being made available from the official vendors (instead of third parties).