Skip to main content

Docker — Best Practices and Pro Tips for Writing Dockerfiles

· 9 min read
Brent De Bisschop
Student Odisee => Opleiding Bachelor Elektronica-ICT
Bronnen

Bron: artikel integraal overgenomen van DevOps Mojo
Origineel auteur: Ashish Patel

Production Pro Tips: Overview of best practices for writing Dockerfile.

docker-tips

Below is the list of recommended best practices and methods for building efficient images.

Build the smallest image possible.

Building a smaller image offers advantages such as faster upload and download times. This is important for the cold start time of a pod in Kubernetes: the smaller the image, the faster the node can download it.

Use Multi-stage Dockerfiles.

Multi-stage builds feature allows you to use multiple temporary images during the build process, but keep only the latest image as the final artifact. In other words, exclude the build dependencies from the image, while still having them available while building the image. Multi-stage builds let you reduce the size of your final image, by creating a cleaner separation between the building of your image and the final output. Less dependencies and reduced image size.

For example, for a .NET/Java applications running, use one stage to do the compile and build, and another stage to copy the binary artifact(s) and dependencies into the image, discarding all nonessential artifacts. Another example is, for an Angular/React applications, run the npm install and build in one stage and copy the built artifacts in the next stage.

### ASP.NET Core app dockerfile ###
# build image as base
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /source

# copy csproj and restore as distinct layers
COPY *.sln .
COPY aspnetapp/*.csproj ./aspnetapp/
RUN dotnet restore

# copy everything else and build app
COPY aspnetapp/. ./aspnetapp/
WORKDIR /source/aspnetapp
RUN dotnet publish -c release -o /app --no-restore

# final stage/image
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY --from=build /app ./
ENTRYPOINT ["dotnet", "aspnetapp.dll"]
### Angular app dockerfile ###
# alpine image as base
FROM node:16-alpine AS builder
ARG CONFIGURATION='dev'

# make /app as working directory
WORKDIR /app

# copy package.json file
COPY package.json .

# Install dependencies
RUN npm install --legacy-peer-deps

# copy the source code to the /app directory
COPY . .

# Build the application
RUN npm run build -- --output-path=dist --configuration=$CONFIGURATION --output-hashing=all


# final stage/image
FROM nginx:stable-alpine

# remove default nginx website
RUN rm -rf /usr/share/nginx/html/*

# copy nginx config file
COPY ./nginx/nginx.conf /etc/nginx/nginx.conf

# copy dist folder fro build stage to nginx public folder
COPY --from=builder /app/dist /usr/share/nginx/html

# start Nginx service
CMD ["nginx", "-g", "daemon off;"]

Avoid installing unnecessary packages.

Installing unnecessary packages only increases the build time and size of the image which would lead to degraded performance. It is recommended that only include those packages that are of utmost importance and try avoiding installing the same packages again and again.

When you avoid installing extra or unnecessary packages, your images have reduced complexity, reduced dependencies, reduced file sizes, and reduced build times.

Use the smallest base image possible.

The base image is the one referenced in the FROM instruction in your Dockerfile. Every other instruction in the Dockerfile builds on top of this image. Using a larger base image with more packages and libraries installed can increase the size of the final Docker image and potentially decrease performance.

The smaller the base image, the smaller the resulting image is, and the more quickly it can be downloaded, leading to better performance and faster build times. Additionally, using a minimal base image can also improve security by reducing the number of potential vulnerabilities that may be present in the final image.

It is recommended to use a minimal base image, such as Alpine Linux, as a starting point for building a Docker image. Alpine has everything you need to start your application in a container, but is much more lightweight.

Minimize the number of Layers

In general, you should minimize the number of layers you use in your Dockerfiles. Each RUN, COPY, FROM Dockerfiles instructions add a new layer and each layer adds to the build execution time & increases the storage requirements of the image.

It is recommended to tie together commands of the same type. For example, instead of writing multiple pip install or npm install commands to install many packages.

Use Docker ignore file

Usually when we build the image, we don’t need everything we have in the project to run the application inside. For example, we don’t need the auto-generated folders, like targets or build folder, or readme or license files etc.

Docker ignore the files present in the working directory if configured in the .dockerignore file, similar to .gitignore files. Create a .dockerignore file and list all the files and folders that we want to be ignored and when building the image, Docker will look at the contents and ignore anything specified inside. This would result in removing unnecessary files from your Docker Container, reduce the size of the Docker Image, and boost up the build performance.

Keep Application data Elsewhere

Storing application data in the image will unnecessarily increase the size of the images. It’s highly recommended to use the volume feature of the container runtimes to keep the image separate from the data.

Use an official and verified Docker images

Use an official and verified Docker image as a base image, whenever available.

Use latest release base upstream image

Use the latest release of a base image. The latest release should contain the latest security patches available when the base image is built.

Scan your Images for Security Vulnerabilities

Scan docker images for known vulnerabilities. Scan a base or application image to confirm that it doesn’t contain any known security vulnerabilities. Use open-source scanning tools such as Synk or Trivy.

Once you build the image, scan it for security vulnerabilities using the docker scan command. In the background Docker actually uses a service called snyk to do the vulnerability scanning of the images. The scan uses a database of vulnerabilities, which gets constantly updated.

Automate scans

Automated scanning tools should also be implemented in the CI pipeline and on the enterprise registry. It is also recommend deploying runtime scanning on applications in case a vulnerability is uncovered in the future.

Carefully consider whether to use a public image

One of the great advantages of Docker is the number of publicly available images, for all kinds of software and apps. These images allow you to get started quickly. In some cases, using public images is not recommended or not possible, when

You don’t want to depend on an external repository. You want the same base operating system in every image. You want to strictly control vulnerabilities in your production environment. You want to control exactly what is inside your images.

Use the Least Privileged User

By default, when a Dockerfile does not specify a user, it uses a root user. Generally, there is no reason to run containers with root privileges. This introduces a security issue, as it is easier for an attacker to escalate privileges on the host.

It is recommended to create a dedicated user and a dedicated group in the Docker image to run the application and also run the application inside the container with that user.

Do not expose secrets

It is advised to not share or copy the application credentials or any sensitive information in the Dockerfile. Use the .dockerignore file to prevent copying files that might contain sensitive information.

Be mindful of which ports are exposed

When designing your Dockerfile, make sure that you know which ports are exposed. By default, Docker will expose all of the containers to a range of random internal ports. This is problematic, as it can expose critical services to the outside world and leave them open for attack.

If you’re using a service that must be exposed to the public internet, then you must create an entry in the Dockerfile. This is done by adding ‘EXPOSE’ in your Dockerfile.

Properly tag your images

Docker images are generally identified by two components: their name and their tag. The name and tag pair is unique at any given time. When you build an image, follow a coherent and consistent tagging policy. Container images are a way of packaging and releasing a piece of software. Tagging the image lets developers identify a specific version of software in order to download it.

Use a specific image tag or version

Use a specific tag or version for your image, not latest. This gives your image traceability. When troubleshooting the running container, the exact image will be obvious.

The latest tag is unpredictable and causing unexpected behavior such as you might get a different image version as in the previous build or new image version may break stuff.

So instead of a random latest image tag, fixate the version of image just like application version.


// Do this
nginx:1.23.1
myapp:1.2.0

// Don't do this:
nginx:latest
myapp:latest

Optimize for the Docker build cache

Docker images are built layer by layer, and in a Dockerfile, each instruction creates a layer in the resulting image. During a build, when possible, Docker reuses a layer from a previous build and skips a potentially costly step. Docker can use its build cache only if all previous build steps used it. While this behavior is usually a good thing that makes builds go faster, you need to consider a few cases.

It is recommended to order commands in the Dockerfile from the least to the most frequently changing commands to take advantage of caching and this way optimize how fast the image gets built.

Use the best order of statements: Include the most frequently changing statements at the end of your dockerfile.

Use ENV to define environment variables

Setting environment variables will make your containers more portable. This is because your environment variables are the only thing that can change from one execution to the next.

Don’t use your Dockerfile as a build script

Dockerfile is a set of instructions that can be used to create a custom image. It should never be used as a build script because it will make your builds unnecessarily long.

Commit Dockerfile to the repository It is recommended to keep dockerfile in repository along with application source code.

Sort multi-line arguments Whenever possible, sort multi-line arguments alphanumerically to make maintenance easier. This helps to avoid duplication of packages and make the list much easier to update.

Summary

Final best practice to remember is to keep your Dockerfile as simple as possible and don’t try to add unnecessary complexity. The above tips should help you build optimized docker images and write better Dockerfiles.