BuggyCodeMaster

Back

Picture this: You’ve built a beautiful application, containerized it, and shipped it to production. You’re feeling pretty good about yourself—until a security scan reveals your container has 147 vulnerabilities. Facepalm.

Most of these vulnerabilities come from stuff you don’t even need: package managers, shells, and other utilities that make traditional Linux distributions convenient but are completely unnecessary for running your application. Enter distroless images—the Marie Kondo approach to containers.

What Are Distroless Images?#

Distroless images contain only your application and its runtime dependencies. That’s it. No package managers, no shells, no utilities—nothing that doesn’t spark joy (or is required to run your app).

# Traditional Docker image
FROM node:16
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "index.js"]
# Result: ~900MB image with bash, apt, npm, and tons of tools you don't need in production

# Distroless equivalent
FROM node:16 AS build
WORKDIR /app
COPY . .
RUN npm install

FROM gcr.io/distroless/nodejs:16
COPY --from=build /app /app
WORKDIR /app
CMD ["index.js"]
# Result: ~100MB image with just Node.js and your app
bash

The difference? The traditional image is like moving to a new house with all your junk plus the previous owner’s junk. The distroless image is like moving with just the essentials—your application and its runtime.

The Real-World Problems Distroless Solves#

1. Smaller Attack Surface = Better Security#

If you don’t have bash, an attacker can’t run bash scripts. If you don’t have curl, they can’t download malicious files. Each tool you remove is one less way for attackers to compromise your system.

In a real-world scenario I encountered, a production server was compromised because it included Python in the container. The attacker exploited a vulnerability to get shell access, then used Python to download a cryptocurrency miner. With a distroless image, this attack would have failed at multiple points.

2. Smaller Image Size = Faster Deployments#

A typical Node.js application in a distroless container can be 70-80% smaller than its Debian-based equivalent. When you’re deploying hundreds of containers across a fleet, those savings add up.

One client reduced their deployment time from 45 minutes to 12 minutes just by switching to distroless images. Their CI/CD pipeline went from “go grab coffee” to “stay in your seat, it’s almost done.”

3. Less Complexity = Fewer Surprises#

Every tool in your container is something that might act unexpectedly. Removing these tools means fewer surprise behaviors.

Hands-On: Creating Your First Distroless Image#

Let’s create a simple distroless container for a Go application:

Step 1: Write a Simple Go Application#

Create a file named main.go:

package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello, Distroless World!")
    })

    fmt.Println("Server started at :8080")
    http.ListenAndServe(":8080", nil)
}
go

Step 2: Create a Go Module#

Create a file named go.mod:

module distroless-example
go 1.19
go

Step 3: Create a Multi-Stage Dockerfile#

Create a Dockerfile:

# Build stage
FROM golang:1.19 AS build

WORKDIR /app
COPY go.mod .
COPY main.go .

# Build a static binary (important for distroless!)
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

# Runtime stage
FROM gcr.io/distroless/static:nonroot
WORKDIR /app
COPY --from=build /app/app .

# Run as non-root user
USER nonroot:nonroot

# Command to run
CMD ["./app"]
dockerfile

Step 4: Build and Run Your Distroless Container#

docker build -t my-distroless-app .
docker run -p 8080:8080 my-distroless-app
bash

Now visit http://localhost:8080 and you’ll see “Hello, Distroless World!”

Step 5: Verify You Can’t Shell In#

Try to get a shell in your container:

docker exec -it $(docker ps -q --filter ancestor=my-distroless-app) sh
bash

You’ll get an error because there’s no shell! That’s exactly what you want—an attacker wouldn’t be able to get a shell either.

Real-World Debugging: The First Challenge#

“But wait,” you say, “if there’s no shell, how do I debug issues in production?”

Good question! Here are some strategies:

Strategy 1: Debug With a Similar Non-Distroless Image#

For debugging, you can temporarily use a non-distroless version of your container:

# For debugging only
FROM golang:1.19
WORKDIR /app
COPY --from=build /app/app .
CMD ["./app"]
dockerfile

Strategy 2: Use Docker’s Debug Image Variant#

Google’s distroless images offer debug variants that include a busybox shell:

FROM gcr.io/distroless/static:debug
dockerfile

Just remember to switch back to the non-debug version for production!

Strategy 3: Use Better Observability#

The best solution? Robust logging, metrics, and tracing. If your application emits enough information, you rarely need to shell in anyway.

Advanced Distroless: Language-Specific Images#

Google (the creators of distroless) provides several language-specific images:

  • gcr.io/distroless/java - For Java applications
  • gcr.io/distroless/nodejs - For Node.js applications
  • gcr.io/distroless/python3 - For Python applications
  • gcr.io/distroless/cc - For C/C++ applications
  • gcr.io/distroless/static - The most minimal image (good for Go binaries)

Let’s look at a Python example:

FROM python:3.9 AS build
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt --target ./site-packages

FROM gcr.io/distroless/python3
COPY --from=build /app/site-packages /site-packages
COPY app.py /app/
WORKDIR /app
ENV PYTHONPATH=/site-packages
CMD ["app.py"]
dockerfile

The DevSecOps Hat Trick: Distroless + Vulnerability Scanning + SBOM#

For maximum security, combine distroless images with:

  1. Vulnerability scanning using tools like Trivy or Snyk
  2. Software Bill of Materials (SBOM) generation with Syft or similar tools

Here’s a simple CI pipeline that does all three:

stages:
  - build
  - scan
  - deploy

build:
  stage: build
  script:
    - docker build -t myapp:$CI_COMMIT_SHA .
    - docker save myapp:$CI_COMMIT_SHA -o myapp.tar

scan:
  stage: scan
  script:
    # Vulnerability scanning
    - trivy image myapp:$CI_COMMIT_SHA
    # Generate SBOM
    - syft myapp:$CI_COMMIT_SHA -o json > sbom.json
  artifacts:
    paths:
      - sbom.json

deploy:
  stage: deploy
  script:
    - docker push myapp:$CI_COMMIT_SHA
yaml

When NOT to Use Distroless Images#

Distroless isn’t always the right choice:

  1. During development - The quick iteration cycle often benefits from having tools available
  2. For certain troubleshooting scenarios - When you need to debug inside the container
  3. For applications that require system utilities - Some applications expect certain utilities to be present

Conclusion: Less is More (Secure)#

Distroless images are a powerful way to improve your container security posture by simply removing what you don’t need. They follow the principle of least privilege by providing only what’s necessary to run your application.

In a world where container vulnerabilities are discovered daily, reducing your attack surface isn’t just good practice—it’s essential hygiene. Distroless images might feel limiting at first, but that’s precisely their strength. As Antoine de Saint-Exupéry said, “Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away.”

Next time you build a container, ask yourself: “Do I really need all this stuff?” Your security team will thank you.


Got questions about distroless images or container security? Leave a comment below!

Distroless Docker: Minimalism for Maximum Security
https://sanjaybalaji.dev/blog/distroless-docker
Author Sanjay Balaji
Published at April 25, 2025