Building Docker Images
Distroless and Scratch Images — Minimal Containers for Production
Stripping out OS commands and package managers for ultra-secure deployments.
Interview: Crucial for security-compliance audits. Interviewers will check if you know how to build shell-less containers and how to debug them without tools like apt or bash.
Introduction
When a container runs in production, it should do only one thing: run your application. Yet, most production containers are packed with Linux utilities: shells (bash/sh), coreutils (ls, cat, mkdir), package managers (apt/apk), and library databases.
These files represent dead weight. Scratch and Distroless base images provide a way to package your code with absolute zero OS overhead, maximizing security and minimizing disk sizes.
Why It Matters
If an attacker exploits your application and gains access to the container:
• In an Alpine container, they can run apk add curl and write scripts to crawl your private network.
• In a Distroless container, there is no shell and no package manager. Running commands fails immediately, neutralizing the threat.
Scratch vs Distroless
1. Scratch Image (FROM scratch)
scratch is a special, empty image containing zero files, directories, or layers. It is the ultimate starting point for statically compiled binaries (like Go or Rust) that carry their own dependencies and do not rely on system dynamic libraries (glibc).
2. Distroless Images
Maintained by Google, distroless images contain *only* the specific application runtimes (e.g., Python interpreter, JVM, Node engine) along with timezone datasets and CA certificate authorities. They have no shell, no coreutils, and no package manager.
Practical Example
Let's compile a Go application statically and deploy it on an empty scratch image:
Quick Quiz
Q1: Which binary type is required to run a container "FROM scratch" successfully?
A) Dynamically linked binary.
B) Statically compiled binary (CGO disabled, no dynamic library lookups).
C) Python source script.
D) Java class bytecode.
Answer: B — Statically compiled binaries carry all their dynamic requirements. Dynamically linked files will fail to launch because scratch contains no glibc library file.
Scenario-Based Challenge
Production Scenario:
You deploy a distroless container to Kubernetes. During a production outage, you attempt to run kubectl exec -it pod-name -- /bin/bash to check files inside the container. The command fails, saying "executable not found". How do you troubleshoot the container?
Since distroless has no shell, kubectl exec fails. You have two production solutions:
1. **Kubernetes Ephemeral Containers (Recommended):** Use kubectl debug to attach a diagnostic container sharing namespaces with the target container:
kubectl debug -it pod-name --image=busybox --target=app-container
This boots a diagnostic container with a shell, mounting files and processes without altering the production image.
2. **Debug Variants:** During staging, build using Google's debug variant images:
FROM gcr.io/distroless/nodejs20-debian12:debug
These variants include a busybox shell and utilities specifically for development.
Interview Questions
1. Why do statically compiled Go binaries require ca-certificates copied manually in scratch?
The binary requires root trust authorities to authenticate SSL certificates when making outbound HTTPS API requests. Since scratch is empty, CA files are missing, causing HTTPS calls to fail.
2. What happens if a dynamically linked program is executed in a scratch container?
The container crashes immediately with a kernel execution error: standard_init_linux.go:211: exec user process caused "no such file or directory". This happens because the Linux loader cannot locate the dynamic library dependencies (like libc.so) inside the empty image.
Production Considerations
Distroless and scratch represent the gold standard for container security in PCI-DSS or SOC2 compliant environments. Removing shells prevents shell-injection zero-day exploits (like Log4j breakout exploits) from expanding, keeping cluster nodes isolated.