Why I’m building my Images using Wolfi

Why I’m building my Images using Wolfi

Leader posted 7 min read

I’m working in software supply chain security, with a specific focus on secure Kubernetes deployments. In a DevOps capacity, I’m focused specifically on ensuring that my container images have as few software vulnerabilities as possible. I believe Wolfi is the best free and open source way to achieve this. This blog post should hopefully demonstrate the value proposition of the Wolfi project.

Getting started with Wolfi

This exercise assumes you already have the Docker runtime installed in order to launch your first Wolfi instance. If docker is already installed, simply open your terminal and pull the base Wolfi image from Chainguard (the creators of Wolfi). This will drop you straight into a Wolfi shell:

docker run -it cgr.dev/chainguard/wolfi-base

At this point, docker downloaded the minimal Wolfi image and started an interactive session. You are now inside the Wolfi OS container. Wolfi uses the apk package manager (same as Alpine Linux), but it uses glibc for better compatibility with modern software. So, within your wolfi shell session, you can simply run the command:

apk update

You'll often hear that wolfi is "distroless-adjacent", or similar phrasing. In short, this means that the distro starts almost empty. If you want to use it for Python development specifically, as an example, you could run the below commands:

apk add python-3.11
python3 --version

If you want to leave the container, simply run the exit command. Because of the --rm flag (if you use it), the container will disappear, keeping your Macbook clean. While this process can be achieved manually by shelling into running containers, in reality, this is now how we would use Wolfi for Projects. Instead, the real power of Wolfi on a Mac is building ultra-secure, tiny images for your own apps, and to be created using a Dockerfile like the example below:

mkdir wolfi-app && cd wolfi-app && cat <<EOF > Dockerfile
FROM cgr.dev/chainguard/wolfi-base
# Install only what you need
RUN apk add nodejs-20
WORKDIR /app
COPY . .
CMD ["node", "index.js"]
EOF

This Dockerfile has a line that says COPY . . and CMD ["node", "index.js"]. Yet, we haven’t created those files yet. This Dockerfile expects a JavaScript file to exist, so let's create a quick one in order to prevent the build from failing:

echo "console.log('Hello from Wolfi running on my Mac!');" > index.js

Now, tell Docker to read your Dockerfile and package it up into an image named my-wolfi-app

docker build -t my-wolfi-app .

The -t command tags / names your image so you don't have to remember a random container ID number. Similarly, the (.) simply tells Docker to look in the current folder for the Dockerfile. Once the build is created, you can run your custom Wolfi container:

docker run my-wolfi-app

So what have we actually done so far? We got a boring helloworld style message from JavScript. Why does that matter? Well, by creating that Dockerfile, we have created a reproducible environment.

  • From a security standpoint, unlike a standard Mac environment, this container only has nodejs and the bare essentials required to run that helloworld-style application. We’re here to reduce the software dependency bloat, ultimately reducing the overall software supply chain risk surface.
  • From a portability perspective, you could send that Dockerfile and index.js to a friend, and they would run the exact same environment with the exact same version of Node, regardless of what they have installed on their machine. This needs to work globally, not just on my machine.

If you, like myself, are on any of the M1, M2, or M3 Apple Silicon chips, Wolfi is fantastic because it has native aarch64 support. This means your containers will run at native speeds without needing Rosetta emulation.

Wait, why use this over Ubuntu or Alpine?

As we mentioned from a security standpoint, most standard distros often carry bloat (extra libraries and dependencies) that hackers can exploit. Wolfi is stripped down to the bare essentials and is updated almost daily to fix vulnerabilities (CVEs) faster than almost any other OS. Saying that, when we built our generic Node.js web app, we introduced a lot of software dependencies into the build process. The container ended up being 192 MB in total size.

docker images | grep my-wolfi-app

Comparing Wolfi build sizes versus standard distros

Comparing image sizes is one of the best ways to show off why developers are flocking to Wolfi. It’s significantly leaner than traditional, thick distributions like Ubuntu (39.6 MB), but maintains the compatibility that sometimes breaks in Alpine (3.6 MB).

I’m running all the steps below on my Macbook to pull a few standard images. First of all, Wolfi is a distroless / (un)distro setup, so Wolfi will work the same on Mac as it will work on any other underlying distribution. So let’s use Docker to download the images and use the docker images command to compare them.

docker pull ubuntu:latest
docker pull debian:stable-slim
docker pull alpine:latest
docker images | grep -E "wolfi|ubuntu|debian|alpine"

What you’ll likely see:

  • Debian Slim: 141 MB
  • Ubuntu: 180 MB
  • Wolfi (base): 21.9 MB
  • Alpine: 13.6MB

The argument here is that Wolfi is nearly as small as Alpine (the gold standard for tiny images), but unlike Alpine, it uses glibc, meaning your real world apps won't break due to missing library links.

Understand whether a container is built using Wolfi

Since Wolfi doesn't have a flashy desktop wallpaper, you have to look at the system identity files and the unique way it handles libraries. By using docker run instead of exec. This creates a container instance from your image, runs the command, and then shuts down.

docker run --rm my-wolfi-app cat /etc/os-release

Checking os-release is likely the most direct proof of wolfi being used to build the container image. Every Linux distribution has a file that describes what it is. The /etc/os-release path is a standard config file in Linux systems (introduced by systemd) that contains operating system identification data, such as the name, version, and ID of the distribution. It is used for identifying the system in scripts, ensuring compatibility, and providing vendor-independent information.

This is the official birth certificate of WolfiOS. You can also check the apk package manager to show your friends where the software actually comes from.

docker run --rm my-wolfi-app cat /etc/apk/repositories

If you want to get really technical, like-for-like comparison of container images to show your friends how many extra (and in some cases unnecessary) tools are sitting inside the general purpose image that a hacker could use - versus Wolfi - just run the below commands:

docker run --rm python:latest ls /usr/bin/ | wc -l
docker run --rm --entrypoint sh cgr.dev/chainguard/python:latest-dev -c "ls /usr/bin/" | wc -l

The below numbers perfectly illustrate why hardened, minimal images like those from Chainguard’s Wolfi are gaining so much traction. The delta we're seeing (655 vs. 326) represents the attack surface. In the official Python image, you have over 300 additional binaries (things like sed, grep, find, and various networking tools) that aren't strictly necessary for your Python code to execute, but provide more living off the land (LOTL-style) opportunities for an attacker.

So, why are the numbers so different?

  • The official python:latest (655 files) image is based on Debian. It includes a full suite of GNU Coreutils, multiple shells, and legacy system utilities. It’s designed for convenience so that almost any script you write "just works" because all the standard Linux tools are there.
  • Chainguard’s python:latest-dev (326 files) is alternatively based on Wolfi. Even in the -dev version (which intentionally includes more tools than the production version), they have stripped out almost everything that isn't required to build or run common Python applications.

The hidden reality here is that if developers were to run that same count on the non-dev Chainguard image (the production version), the number would drop even more drastically - likely down to under 10 files. There is essentially only python and the bare-minimum shared libraries (libc, and stuff like that) the basic packages needed to keep the Python interpreter alive in the Chainguard production version.

In a production Chainguard Python image:

  • There is no ls.
  • There is no sh.
  • There is no pip.

Where the developer experience gains are clear for the official python image, since it includes everything so nothing breaks in Day 2 operations, the free-to-use Chainguard dev images provide the hardened development environment required for my builds so it only includes what's needed to build and test the web application. Going further with the Chainguard licensed images, which were never the focus of this blog post, developers receive maximum security, since it includes no shell, no tools, and no unnecessary binaries. You can do additional testing by using cli tools like pip to list python-specific packages in each image you test.

docker run --rm python:latest pip list
docker run --rm cgr.dev/chainguard/python:latest-dev -m pip list

I hope you found this blog useful.

Cleanup

If you want to delete all the locally stored images that aren't currently being used by a container, use the prune command or just remove the images manually with rmi. I’m lazy, so I’m going to just do a nuclear cleanup on everything created during this exercise:

docker stop $(docker ps -q)
docker rm $(docker ps -aq)
docker rmi $(docker images -q)
docker images

More Posts

I’m a Senior Dev and I’ve Forgotten How to Think Without a Prompt

Karol Modelskiverified - Mar 19

The Audit Trail of Things: Using Hashgraph as a Digital Caliper for Provenance

Ken W. Algerverified - Apr 28

Defending Against AI Worms: Securing Multi-Agent Systems from Self-Replicating Prompts

alessandro_pignati - Apr 2

Why most people quit AWS

Ijay - Feb 3

Building Production-Grade Microservices on AWS ECS Fargate with GitLab CI/CD Automation

shawmeer - Apr 8
chevron_left

Related Jobs

View all jobs →

Commenters (This Week)

2 comments
1 comment

Contribute meaningful comments to climb the leaderboard and earn badges!