Optimizing Docker Images with Distroless

Cemal Ünal
Picus Security Engineering
4 min readMay 11, 2022
Photo by Richard Sagredo on Unsplash

A Brief Introduction to Containers

Containerization is a packaging and virtualization mechanism for an application. It contains the application code along with the dependencies that are required to run this application. They are more lightweight, and easy to launch, scale, and replace compared to virtual machines. Thus, enabling us to improve the performance of our systems.

Container adoption grew rapidly in the technology industry and most of the applications started to run in containers. Docker has become an industry standard for containerization technology.

Also, there are some challenges when working with Docker containers:

  • Keeping only the related libraries: Helps us to eliminate/minimize the attack surface, making the image more secure against possible cyber attacks.
  • Container image size: Keeping the image size as small as possible helps us to reduce the build time, upload/download, and storage costs.
  • Keeping Containers Stateless: Keeping containers stateless helps us to replace the unhealthy containers with new ones easily. And it helps us to easily scale our application. Also, we should not treat containers as servers. For example, we should not SSH into a container and execute commands in it.

To run our apps in Docker containers, we need to create Docker images using a file named Dockerfile. In Dockerfile, we are specifying the steps that are required to run our application. For example, we have a Go server and we want to dockerize it. First, we specify a base image that provides the related environment for Go development (isolated from the host operation system), then we build the binary using this environment and finally run the binary.

An example Dockerfile for this Go application can be the following:

FROM golang:1.17.4 AS buildWORKDIR /goCOPY go.mod .
COPY go.sum .
RUN go mod download
COPY . .
RUN GOOS=linux go build -o /sample-app /go/sample-app.go
FROM centos:latestWORKDIR /root/
COPY --from=build /sample-app .
ENTRYPOINT ["/root/sample-app"]

Note: In the above Dockerfile, you can see that we used FROM instruction twice. This is a common pattern when building Docker images to reduce the final image size. For more information about multi-stage builds, you can refer to: https://docs.docker.com/develop/develop-images/multistage-build/

We can create a new Docker image using this Dockerfile:

$ docker build -t sample-go-app .sample-go-app   latest    3d41164a3f8d    About a minute ago  317MB

Everything is great so far, we have dockerized our application and it is ready to run. We see that size of the image is 317 MB and it is a bit huge. Also, let’s scan it using the docker scan command:

$ docker scan sample-go-appTested 180 dependencies for known vulnerabilities, found 306 vulnerabilities.

We can easily see that are lots of dependencies (most of them are unnecessary) and vulnerabilities found by scan operation.

It would be great to reduce the image size while reducing the vulnerabilities at the same time. This is where the Distroless images come into play.

Distroless Container Images

Distroless is a Google Container Tools project and it is defined as follows:

Distroless images contain only your application and its runtime dependencies. They do not contain package managers, shells, or any other programs you would expect to find in a standard Linux distribution.

So it seems a really good fit to overcome the challenges we listed at the beginning of this article. Let’s give it a try and see the results. To do that, we will need to update our Dockerfile to use a Distroless base image:
(Here we used gcr.io/distroless/static as our base image. Please refer to their documentation for choosing it according to your needs.)

FROM golang:1.17.4 AS buildWORKDIR /goCOPY go.mod .
COPY go.sum .
RUN go mod download
COPY . .
RUN GOOS=linux go build -o /sample-app /go/sample-app.go
FROM gcr.io/distroless/staticWORKDIR /app
COPY --from=build --chown=nonroot:nonroot /sample-app .
USER nonroot
ENTRYPOINT ["/app/sample-app"]

Note: One of the best practices while running a container is to run processes with a non-root user. By default, containers run using the root user, and it can be changed using the USER instruction in Dockerfile. Distroless images come with a non-root user named as nonroot, and here we used it in our Dockerfile.

Now let’s see how our final image size and vulnerabilities will change:

$ docker build -t sample-go-app-distroless .sample-go-app-distroless  latest   329ce894667a  2 days ago   36.2MB

After that let’s scan the new image using the docker scan command:

$ docker scan sample-go-app-distrolessTested 3 dependencies for known vulnerabilities, no vulnerable paths found.

This time the image size is approximately 36 MB since we removed the unnecessary dependencies. Now we see that there are only 3 dependencies and no vulnerabilities found. 🎉

Conclusion

Using the distroless base images:

  • We removed unnecessary dependencies (like a package manager, shell, etc.) from the final image which helped us to reduce the attack surface. Now, we have got more secure container images.
  • The final image size is drastically reduced. This enabled us to reduce build time. Also, enabled us to reduce storage and data transfer costs in AWS Elastic Container Registry.

--

--

Cemal Ünal
Picus Security Engineering

Cloud Software Engineer @ Picus Security | AWS Certified DevOps Engineer Professional