How to use Dockerfiles with wolfi-base images
At Chainguard, we have developed a suite of tooling to help us build hardened, low-vulnerability Images on top of the Wolfi Linux (un)distribution using apko and melange. But you don't need to use these tools to get started with Wolfi and begin reaping the benefits of minimal, low-vulnerability images. At the outset, it makes sense to keep using the tools you're used to–you can later look at adopting different tools like apko and melange when you want to benefit from some of the advantages they bring.
To help users get started, I've written this short guide for how to use wolfi-base with Docker tooling. We'll look at using Chainguard base images, including static (an equivalent to Google Distroless) and wolfi-base (a minimal image that includes a shell and package manager).
Images and static binaries
The most common place to start with Chainguard Images is with the static base images, which are designed for use with Docker multi-stage builds and are a direct replacement for the Google Distroless images. These are great when you can produce statically compiled binaries for your application, and are about as minimal as you can realistically get.
This example shows using a C compiler to create a static binary and running it on the static base image:
Dockerfile
# syntax=docker/dockerfile:1.4
FROM cgr.dev/chainguard/gcc-glibc as build
COPY <
int main() { printf("Hello!"); }
EOF
RUN cc -static /hello.c -o /hello
FROM cgr.dev/chainguard/static:latest
COPY --from=build /hello /hello
CMD ["/hello"]
You can find similar examples for Rust and Go.
A fairly common occurrence is for software to have further dependencies or for static compilation to not be (easily) possible. For that reason we have the glibc-dynamic Image which contains some common libraries, primarily glibc.
Using wolfi-base for flexibility and extensibility
But often this isn't enough – sometimes your application has more dependencies such as different libraries or runtimes. In some cases this will still be simple, for example, using a JDK image to build an application and a JRE image to provide the production runtime. In other cases it can be important to have more fine-grained control over the exact packages used to build and run an image, including controlling the versions. In these cases, the easiest solution is to reach for wolfi-base. Wolfi-base includes apk tooling and a shell so it's not as slimmed down as some of our other images, but this makes it easy to use in a traditional Dockerfile. For example, the golink project from Tailscale uses wolfi-base to install the required version of the Go compiler without worrying about being pushed to a new version when the cgr.dev/chainguard/go:latest Image updates:
FROM --platform=$BUILDPLATFORM cgr.dev/chainguard/wolfi-base as build
RUN apk update && apk add build-base git openssh go-1.20
WORKDIR /work
COPY go.mod go.sum ./
RUN go mod download
COPY . .
ARG TARGETOS TARGETARCH TARGETVARIANT
RUN \
if [ "${TARGETARCH}" = "arm" ] && [ -n "${TARGETVARIANT}" ]; then \
export GOARM="${TARGETVARIANT#v}"; \
fi; \
GOOS=${TARGETOS} GOARCH=${TARGETARCH} CGO_ENABLED=0 go build -v ./cmd/golink
FROM cgr.dev/chainguard/static:latest
ENV HOME /home/nonroot
COPY --from=build /work/golink /golink
ENTRYPOINT ["/golink"]
CMD ["--sqlitedb", "/home/nonroot/golink.db", "--verbose"]
It's important to note that Wolfi is a rolling distribution. We don't do releases and we only publish a "latest" version. The few packages in wolfi-base are stable and highly unlikely to cause breakages, so it's safe to use "latest" in most use cases.
As this example uses a multi-stage build, our final image is still minimal. There is no shell or package manager present.
Matching build and runtime dependencies
Another common example is using Python. Python typically requires the build and runtime versions to match exactly. In the example below, we create a Python image where the builder and production Image are careful to use version 3.11:
FROM cgr.dev/chainguard/wolfi-base as builder
RUN apk add python3~3.11 py3-pip
RUN adduser -D -u 65332 nonroot
USER nonroot
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt --user
FROM cgr.dev/chainguard/wolfi-base
RUN apk add python3~3.11
RUN adduser -D -u 65332 nonroot
WORKDIR /app
COPY --from=builder /home/nonroot/.local/lib/python3.11/site-packages /home/nonroot/.local/lib/python3.11/site-packages
USER nonroot
COPY main.py .
WORKDIR /app
ENTRYPOINT [ "python", "/app/main.py" ]
This Dockerfile takes advantage of the ~ syntax in apk to choose the version of Python.
Unfortunately, in this case the final Image isn't entirely minimal. It includes both a package manager and shell. If this is important to your use case, consider exploring a Chainguard Images subscription tier, which will give you access to tagged, minimal versions of the Python Chainguard Images.
Adding versioned tooling
Finally, another common use for wolfi-base is just to create an image with more tooling in it. For example, we can easily create an image with various network tooling for debugging and provide versions at different granularities if necessary:
FROM cgr.dev/chainguard/wolfi-base
RUN apk add wget \
curl~8.2.1 \
nmap~7 \
tcptraceroute \
tcpdump~4.99 \
netcat-openbsd
Building this, I get a 50 MB image which is pretty compact for all this tooling in a glibc-based image. If the package you need doesn't exist in Wolfi yet, you'll need to install or build it from upstream sources. To keep things clean and consistent, you can even turn this into an apk package using the melange tooling and contribute it back to Wolfi. Check out this article on Chainguard Academy to get started.
Conclusion
This has been an in-depth tour of how to use Dockerfiles with Wolfi images. The key point to remember is that you can keep using your existing tooling with Wolfi images–you don't have to learn new tools and workflows.
Ready to Lock Down Your Supply Chain?
Talk to our customer obsessed, community-driven team.