5.Comprehensive Guide to Docker Concepts: Distroless Images, Multistage Builds, and Best Practices

Amrit PoudelAmrit Poudel
4 min read

1. Google Distroless Images

What Are Distroless Images?

Distroless images are a specialized type of Docker image designed to contain only the application and its runtime dependencies, without the extra overhead of an operating system. These images are built to be as minimal as possible, excluding unnecessary components like package managers and shells.

Why Use Distroless Images?

  1. Security: By removing unnecessary components, distroless images reduce the attack surface. Since there's no shell or package manager, potential vulnerabilities associated with these tools are eliminated.There is no shell to exec into so attack surface for hackers is significantly reduced also there are no other packages so using those package exploits also not possible.

  2. Size: Distroless images are significantly smaller than traditional images because they exclude everything except the application and its essential libraries. This makes them more efficient to deploy and faster to download.

  3. Performance: Smaller images mean less overhead in terms of storage and bandwidth, which can lead to faster deployment and better performance in resource-constrained environments.

Limitations:

  1. No Shell: Distroless images lack a shell, which means you cannot execute commands inside the container using docker exec. This can make debugging and troubleshooting more challenging.

  2. Limited Flexibility: Because the image doesn’t include tools like curl or wget, you have to ensure that all dependencies are bundled within the image itself.

Example Use Case:

Suppose you have a Node.js application. You can use a distroless image to ensure that your production environment is as secure and lean as possible.

# First stage: Build the application
FROM node:14 AS build

WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
COPY . .
RUN npm run build

# Second stage: Create a minimal runtime image
FROM gcr.io/distroless/nodejs:14

WORKDIR /app
COPY --from=build /app/dist /app
CMD ["index.js"]

In this example, the build stage uses a full Node.js image to compile the application, while the final image only contains the built artifacts and runtime dependencies.

2. Multistage Builds

What Are Multistage Builds?

Multistage builds are a Dockerfile feature that allows you to use multiple FROM instructions in a single Dockerfile. This technique helps you optimize the build process by separating the build environment from the runtime environment.

Benefits of Multistage Builds:

  1. Smaller Final Images: By using a lightweight image for the final stage, you can exclude build tools and dependencies that are only needed during the build process.

  2. Cleaner Dockerfiles: Multistage builds can make Dockerfiles more readable and manageable by clearly separating different build stages.

  3. Reduced Complexity: Simplifies dependency management by allowing you to use different images for different stages of the build process.

Example Use Case:

Consider a Java application that requires Maven for building but does not need Maven in the final image.

# First stage: Build the application
FROM maven:3.8.4-openjdk-11 AS build

WORKDIR /app
COPY pom.xml ./
COPY src ./src
RUN mvn package

# Second stage: Create a minimal runtime image
FROM openjdk:11-jre-slim

WORKDIR /app
COPY --from=build /app/target/myapp.jar /app/
CMD ["java", "-jar", "myapp.jar"]

In this example:

  • The build stage uses a Maven image to compile the application.

  • The final stage uses a slim JRE image that only includes the Java runtime needed to run the application.

3. Best Practices for Writing Dockerfiles

1. Use Official or Trusted Base Images

Starting with a well-maintained and trusted base image helps ensure that your image is secure and reliable. For example, official images from Docker Hub or Google Container Registry are a good starting point.

2. Minimize Image Layers

Each instruction in a Dockerfile creates a new layer in the image. To reduce the number of layers and keep your image size small, combine multiple commands into a single RUN instruction.

Example:

# Instead of:
RUN apt-get update
RUN apt-get install -y \
    curl \
    vim

# Combine into:
RUN apt-get update && \
    apt-get install -y curl vim

3. Avoid Storing Secrets in Dockerfiles

Never include sensitive information like API keys or passwords directly in your Dockerfile. Use environment variables or Docker Secrets for secure management.

4. Clean Up After Installations

To keep your image size small, remove temporary files or caches after installing packages.

Example:

RUN apt-get update && \
    apt-get install -y \
    build-essential \
    && rm -rf /var/lib/apt/lists/*

5. Leverage Dockerfile Instructions Efficiently

  • COPY vs. ADD: Use COPY for simple file transfers and ADD for extracting tar archives or downloading files from URLs. For most cases, COPY is preferred for its simplicity.

  • CMD vs. ENTRYPOINT: Use CMD to specify default arguments for the container, and ENTRYPOINT to define the main executable. Combine them to ensure that the main process is always executed.

Example:

ENTRYPOINT ["gunicorn"]
CMD ["app:app"]

6. Define WORKDIR Early

Set the working directory early in your Dockerfile to simplify path management for subsequent instructions.

Example:

WORKDIR /app
COPY . .
RUN make

7. Use .dockerignore

Create a .dockerignore file to exclude unnecessary files from the build context, reducing build time and image size.

Example .dockerignore:

node_modules
*.log
.git

8. Test Your Image

Regularly test your Docker image to ensure it behaves as expected. Consider using automated testing tools to verify functionality and performance.

1
Subscribe to my newsletter

Read articles from Amrit Poudel directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Amrit Poudel
Amrit Poudel