DEV Community

Abhay Singh Kathayat
Abhay Singh Kathayat

Posted on

Best Practices for Writing Efficient and Maintainable Dockerfiles

Dockerfile Best Practices

A well-crafted Dockerfile is essential for creating efficient, secure, and maintainable Docker images. Following best practices ensures that your images are lightweight, fast to build, and easy to manage. In this article, we’ll cover essential Dockerfile best practices that can help improve the quality of your Docker images and optimize your development workflows.


1. Use a Minimal Base Image

Start with a minimal base image to reduce the size of the final Docker image. For example, instead of using large images like ubuntu or debian, consider using images like alpine or slim versions that have fewer dependencies and a smaller footprint.

Example:

# Use a minimal base image (e.g., Alpine) for smaller image size
FROM node:16-alpine
Enter fullscreen mode Exit fullscreen mode

This practice reduces the overall size of the image, which is beneficial for faster builds, easier uploads to registries, and reduced resource consumption during runtime.


2. Order Instructions to Leverage Caching

Docker caches each layer of the image during the build process. To optimize build time, place instructions that change less frequently at the top of your Dockerfile and more frequently changing instructions towards the bottom.

Best Practice:

  • Install dependencies first.
  • Copy static files (like package.json, requirements.txt, etc.) early.
  • Copy your source code at the end.

This strategy leverages Docker’s layer caching, so if only the code changes, Docker doesn’t have to reinstall dependencies, speeding up subsequent builds.

Example:

# Install dependencies first, as these change less frequently
COPY package.json /app/
WORKDIR /app
RUN npm install

# Copy source code after installing dependencies
COPY . /app/
Enter fullscreen mode Exit fullscreen mode

3. Avoid Installing Unnecessary Packages

Always aim to install only the packages necessary for your application to function. Installing unnecessary tools or dependencies can significantly increase the size of your image.

  • Avoid installing unnecessary utilities like curl, git, or vim unless they are essential for your container’s runtime environment.

Example:

# Only install necessary dependencies
RUN apk add --no-cache bash
Enter fullscreen mode Exit fullscreen mode

By using the --no-cache option, we prevent Docker from caching package index files, which saves space.


4. Reduce the Number of Layers

Each Dockerfile instruction creates a new image layer. The more layers there are, the larger the image becomes, and the slower the build process is. Try to consolidate multiple commands into fewer layers.

Best Practice:

Combine RUN instructions where possible. For example, instead of running separate RUN commands for each package installation, combine them into a single RUN statement.

Example:

# Instead of running multiple RUN commands
RUN apt-get update && apt-get install -y \
    curl \
    vim \
    git

# Combine them into a single RUN command to reduce layers
RUN apt-get update && apt-get install -y curl vim git
Enter fullscreen mode Exit fullscreen mode

5. Use .dockerignore to Exclude Unnecessary Files

Similar to .gitignore for Git, .dockerignore is used to prevent unnecessary files from being copied into your Docker image, which can reduce the image size and improve build speed.

Best Practice:

Create a .dockerignore file to exclude files like:

  • .git/
  • node_modules/
  • *.log
  • Temporary files

Example:

# .dockerignore
node_modules/
.git/
*.log
*.md
Enter fullscreen mode Exit fullscreen mode

By ignoring files that aren’t needed for the application’s runtime, you minimize the size of your image and prevent Docker from copying unnecessary files during the build process.


6. Leverage Multi-Stage Builds

Multi-stage builds allow you to separate the building of your application from the final image, which helps you produce smaller, cleaner images. You can use one stage to build the app (with build tools) and another stage to copy the built assets into a minimal runtime image.

Best Practice:

  • Use a build stage for compiling code, installing dependencies, or running tests.
  • Copy the resulting artifacts into a smaller final image.

Example:

# Build stage
FROM node:16-alpine AS build
WORKDIR /app
COPY package.json package-lock.json /app/
RUN npm install
COPY . /app/
RUN npm run build

# Final stage
FROM node:16-alpine
WORKDIR /app
COPY --from=build /app/dist /app/dist
CMD ["node", "dist/server.js"]
Enter fullscreen mode Exit fullscreen mode

In this example, the build stage installs dependencies, runs the build command, and compiles the application. The final stage only includes the built files, resulting in a much smaller image.


7. Use Specific Version Tags for Base Images

Avoid using the latest tag for base images, as it can lead to unpredictable behavior when base images are updated. Always specify a fixed version to ensure consistency across builds.

Best Practice:

  • Use specific tags (e.g., node:16-alpine instead of node:alpine).
  • If possible, use exact versions for base images to prevent future breaks.

Example:

# Good practice: use a specific version tag for reproducibility
FROM node:16-alpine
Enter fullscreen mode Exit fullscreen mode

8. Minimize the Use of RUN Instructions

Every RUN instruction adds a new layer to your image. Minimize the number of RUN instructions to reduce image size and build time. Instead of running multiple commands in separate RUN instructions, combine them into a single command.

Best Practice:

Combine multiple commands into a single RUN statement using && to chain them together.

Example:

# Multiple RUN instructions (not ideal)
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git

# Combined RUN instruction (ideal)
RUN apt-get update && apt-get install -y curl git
Enter fullscreen mode Exit fullscreen mode

9. Set Non-Root User for Security

Running containers as root is a security risk. It’s recommended to use a non-root user inside the container to reduce the impact of potential vulnerabilities in the containerized application.

Best Practice:

  • Create and switch to a non-root user after installing necessary packages.

Example:

# Create a non-root user and switch to that user
RUN adduser --disabled-password myuser
USER myuser
Enter fullscreen mode Exit fullscreen mode

10. Explicitly Set Environment Variables

Set environment variables explicitly in the Dockerfile using the ENV instruction. This can help to configure the application and make it more portable across different environments.

Example:

# Set environment variables
ENV NODE_ENV production
ENV PORT 3000
Enter fullscreen mode Exit fullscreen mode

Conclusion

Following Dockerfile best practices is crucial for creating efficient, maintainable, and secure Docker images. By using minimal base images, reducing the number of layers, excluding unnecessary files, and following the other recommended practices, you can create optimized Docker images that are faster to build, smaller in size, and easier to manage. Additionally, employing multi-stage builds and non-root users can improve both security and performance.

By adhering to these best practices, you ensure that your Docker images are efficient, portable, and consistent across different environments.


Top comments (2)

Collapse
 
joodi profile image
Joodi

Love this 🌱

Collapse
 
victora profile image
Victor A.

Knowing how to deal with dockerfiles nowadays is a must have skill, thanks for sharing!