Dockerfile best practices for Production
EP 05: Top 12 Dockerfile best practices
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:
- Easy versioning: Dockerfiles can be committed and maintained via version control to track changes and revert any mistakes.
- 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.
- 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.
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.
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.
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.