Dockerfile best practices for Production

EP 05: Top 12 Dockerfile best practices

Jigarkumar Rathod
6 min readJul 29, 2023

Docker adoption rises constantly šŸ“ˆ and many are familiar with it, but not everyone is using Docker according to the best practices. šŸ‘€

Before moving on, if you donā€™t know what Docker is, you can learn everything you need to get started in this Docker for DevOps EngineersšŸ³

This article dives into a curated list of Docker security best practices while writing a Dockerfile for your application.

Some of the benefits Dockerfiles provide are:

  1. Easy versioning: Dockerfiles can be committed and maintained via version control to track changes and revert any mistakes.
  2. Accountability: If you plan on sharing your images, it is often a good idea to provide the Dockerfile that created the image as a way for other users to audit the process.
  3. Flexibility: Creating images from a Dockerfile allows you to override the defaults that interactive builds are given. This means that you do not have to provide as many runtime options to get the image to function as intended.

Docker images have intermediate layers that increase reusability, decrease disk usage, and speed up docker build by allowing each step to be cached.

Image Source and Credits: http://blog.bigstep.com/developers-love-docker/

Best Practices for Writing Dockerfiles:

1)Use an official and verified Docker image as a base image:

Letā€™s say you are developing a Node.js application and want to build and run it as a Docker image. Instead of taking a base operating system image and installing node.js, npm, and whatever other tools you need for your application, use the official node image for your application.

Image Credit: https://www.techworld-with-nana.com

2)Use a .dockerignore file:

Now, usually when we build the image, we donā€™t need everything we have in the project to run the application inside. We donā€™t need auto-generated folders, like targets or build folders; we donā€™t need the readme file, etc.

So how do we exclude such content from ending up in our application image? šŸ¤” Using a .dockerignore file.

Add a .dockerignore file to your working directory to exclude specific files and directories. The syntax is similar to .gitignore:

# ignore .git and .cache folders
.git
.cache

# ignore all markdown files(md)
*.md

3)Use specific Docker image versions:

Using an image such as node:latest in your FROM instructions are risky. Most image authors immediately switch latest to new major versions as soon as theyā€™re released. Rebuilding your image could silently select a different version, causing a broken build or malfunctioning container software.

Selecting a specific tag such as node:17.0.1 is safer because itā€™s more predictable. Only use latest when thereā€™s no alternative available.

Image Credit: https://www.techworld-with-nana.com

4)Make use of Multi-Stage Builds:

Make use of multistage building features to have reproducible builds inside containers.

In a multistage build, you create an intermediate container ā€” or stage ā€” with all the required tools to compile or produce your final artifacts (i.e., the final executable). Then, you copy only the resulting artifacts to the final image, without additional development dependencies, temporary build files, etc.

A well-crafted multistage build includes only the minimal required binaries and dependencies in the final image, and no build tools or intermediate files. This reduces the attack surface, decreasing vulnerabilities.

It is safer, and it also reduces image size.

# base build image
FROM maven:3.6.0-jdk-8 as BUILD

# copy the project file
COPY ./pom.xml ./pom.xml

# copy other files
COPY ./src ./src

# build for release
RUN mvn package

# our final base image
FROM openjdk:8-jre-alpine

# set deployment directory
WORKDIR /my-project

# copy the built artifact from the maven image
COPY --from=BUILD target/springboot-starterkit-1.0.jar ./

# set the startup command to run the binary
CMD ["java", "-jar", "./springboot-starterkit-1.0.jar"]

5)Set a non-root user for your images:

Docker defaults to running container processes as root. This is problematic because root in the container is the same as root on your host. The USER command allows us to manually set the userā€™s ID within the Dockerfile at any point. We strongly suggest you enforce the use of this command in every Dockerfile as early as possible.

# Adding a new user "user1" 
RUN useradd -u user1

# Changing to non-root privilege
USER user1

# Set the user with a UID
USER 1000

# Set the user and group
USER user1:demo-group

6)Label your images for a better organization:

The LABEL property is a feature of Docker that allows you to specify custom metadata in the form of key/value pairs.

Teams with many different images often struggle to organize them all. You can set arbitrary metadata on your images using the Dockerfile LABEL instruction. This provides a convenient way to attach relevant information thatā€™s specific to your project or application. By convention, labels are commonly set using reverse DNS syntax:

LABEL com.example.project=api
LABEL com.example.team=backend

7)Include health/liveness checks:

When using plain Docker or Docker Swarm, include a HEALTHCHECK instruction in your Dockerfile whenever possible. This is critical for long-running or persistent services in order to ensure they are healthy, and manage restarting the service otherwise.

HEALTHCHECK --interval=5m --timeout=3s CMD curl -f http://localhost/ || exit 1

If running your images in Kubernetes, use the liveness probe configuration inside the container definitions, as the docker HEALTHCHECK instruction wonā€™t be applied.

8)FROM: every image should be pulled from the organizationā€™s private registry:

Another argument you can pass to the FROM command is the registry you want the image to be pulled from. By default, this is a public registry, usually Dockerhub. For security purposes, however, you may wish to enforce that every image is pulled from your registry. We highly recommend large organizations make use of a private registry for security purposes.

9)Layer sanity:

Remember that order in the Dockerfile instructions is very important.

Since RUN, COPY, ADD, and other instructions will create a new container layer, grouping multiple commands together will reduce the number of layers.

For example, instead of:

FROM ubuntu:22.10
RUN apt-get install -y wget
RUN wget https://ā€¦/downloadedfile.tar
RUN tar xvzf downloadedfile.tar
RUN rm downloadedfile.tar
RUN apt-get remove wget

It would be a Dockerfile best practice to do:

FROM ubuntu:22.10
RUN apt-get install wget && wget https://ā€¦/downloadedfile.tar && tar xvzf downloadedfile.tar && rm downloadedfile.tar && apt-get remove wget

Also, place the commands that are less likely to change, and easier to cache, first.

Instead of:

FROM ubuntu:22.10
COPY source/* .
RUN apt-get install nodejs
ENTRYPOINT ["/usr/bin/node", "/main.js"]

It would be better to do:

FROM ubuntu:22.10
RUN apt-get install nodejs
COPY source/* .
ENTRYPOINT ["/usr/bin/node", "/main.js"]

10)ADD, COPY:

Both the ADD and COPY instructions provide similar functions in a Dockerfile. However, COPY is more explicit.

Use COPY unless you really need the ADD functionality, like to add files from an URL or from a tar file. COPY is more predictable and less error-prone.

In some cases it is preferred to use the RUN instruction over ADD to download a package using curl or wget, extract it, and then remove the original file in a single step, reducing the number of layers.

Multistage builds also solve this problem and help you follow Dockerfile best practices, allowing you to copy only the final extracted files from a previous stage.

11)Credentials and Confidentiality:

Never put any secret or credentials in the Dockerfile instructions (environment variables, args, or hard coded into any command).

Be extra careful with files that get copied into the container. Even if a file is removed in a later instruction in the Dockerfile, it can still be accessed on the previous layers as it is not really removed, only ā€œhiddenā€ in the final filesystem.

12)Set your ENTRYPOINT and CMD correctly:

ENTRYPOINT and CMD are closely related instructions. ENTRYPOINT sets the process to run when a container starts, while CMD provides default arguments for that process. You can easily override CMD by setting a custom argument when you start containers with docker run.

Conclusion:-

We have seen that container image security is a complex and critical topic that simply cannot be ignored until it explodes with terrible consequences.

Prevention and shifting security left are essential for improving your security posture and reducing the management overhead.

This set of recommendations, focused on Dockerfiles best practices, will help you in this mission.

Thanks for Reading!

If you like my work and want to support meā€¦

The BEST way is to follow me on Medium here.

--

--

Jigarkumar Rathod

Iā€™m Jigar. Passionate about DevOps and cloud-native stuff ā˜ļø, I like working with data a little more than I shouldšŸ˜Š | Exploring Cloud Tech