Top 8 Docker Best Practices for using Docker in Production ✅
Tags: docker, devops, tutorial, beginners
Published: 2021-11-21 12:53:45

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 [free Docker Crash Course 🐳](https://youtu.be/3c-iBn73dDE)

---
## Why using Best Practices? 🤷‍♀️ ##
So, in my [new video 'Top 8 Docker Production Best Practices'](https://youtu.be/8vXoMqWgbQQ) I want to show you 8 ways you can use Docker in a right way in your projects to:
- ✅ **improve security**,
- ✅ **optimize** the **image size**, 
- ✅ take advantage of some of the useful Docker features 
- ✅ and also write **cleaner and more maintainable Dockerfiles**

---
## 1️⃣ Best Practice ##
>**Use an official and verified Docker image as a base image, whenever available.**

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.

![1st best practice](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/spd6rkqkwnitp2riio29.png)

**Improvements:**
- Cleaner Dockerfile
- Official and verified image, which is already built with the best practices


---
## 2️⃣ Best Practice ##
>**Use specific Docker image versions**

Okay, so we have selected the base image, but now when we build our applications image from this Dockerfile, it will always use the `latest` tag of the node image.

Now why is this a problem? 🤔
❌ - you might get a different image version as in the previous build 
❌ - the new image version may break stuff
❌ - `latest` tag is unpredictable, causing unexpected behavior 

So instead of a random latest image tag, you want to fixate the version and just like you deploy your own application with a specific version you want to use the official image with a specific version. 
And the rule here is: **the more specific the better**

![2nd best practice](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zikxyawtnrwk526xov6z.png)

**Improvements:**
- Transparency to know exactly what version of the base image you're using


---
## 3️⃣ Best Practice ##
> **Use Small-Sized Official Images**

When choosing a Node.js image, you will see there are actually multiple official images. Not only with different version numbers, but also with **different operating system distributions**:
![Node image versions](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n4szjwytxzttkc4wovxz.png)

So the question is: Which one do you choose and why is it important? 🤷🏻‍♂️

**1) Image Size**
❌  Well, if the image is based on a **full-blown OS distribution** like Ubuntu or Centos, you will have a bunch of tools already packaged in the image. So the **image size will be larger**, but you **don't need most of these tools** in your application images.

✅  In contrast having **smaller images means you need less storage space** in image repository as well as on a deployment server and of course you can transfer the images faster when pulling or pushing them from the repository. 

**2) Security Issue**
❌  In addition to that, with lots of tools installed inside, you need to consider the security aspect. Because such base images usually contain [hundreds of known vulnerabilities](https://snyk.io/blog/opensourcesecurity-2020survey/) and basically create a **larger attack surface** to your application image.

This way you basically end up introducing unnecessary security issues from the beginning to your image! 🙉

✅  In comparison by using **smaller images with leaner OS distributions**, which only bundle the necessary system
tools and libraries, you're also minimizing the attack surface and making sure that you build **more secure images**.


So the best practice here would be to select an image with a specific version based on a leaner OS distribution like alpine for example:

![3rd best practice](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/36oou4hn3wq55n14u9dz.png)

Alpine has everything you need to start your application in a container, but is much more lightweight. And for most of the images that you look on a Docker Hub, you will see a version tag with alpine distribution inside.

It is one of the most common and popular base images for Docker containers.


---
## 4️⃣ Best Practice ##
> **Optimize caching for image layers when building an image**

So what are image layers and what does caching and image layer mean? 🤔

**1) What are Image Layers?**
A Docker image is built based on a Dockerfile.
And in a Dockerfile **each command or instruction creates an image layer**:
![Docker Image Layers](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dmtb90pbzqygh0h86a10.png)

So when we use a base image of node alpine like in the above example it already has layers, because it was already built using its own Dockerfile. Plus, in our Dockerfile on top of that we have a couple of other commands that each will add a new layer to this image.

**2) Now what about caching?**
Each layer will get cached by Docker. 👍
So when you rebuild your image, if your Dockerfile hasn't changed, Docker will just use the cached layers to build the image.

**Advantages** of cached image layers:
✅  - Faster image building
✅  - Faster pulling and pushing of new image versions: 
If I pull a new image version of the same application and let's say 2 new layers have been added in the new version: Only the newly added layers will be downloaded, the rest are already locally cached by Docker.

**3) Optimize the Caching**
So to optimize the caching, you need to know that:
Once a layer changes, **all following or downstream layers have to be re-created as well**. In other words: when you change the contents of one line in the Dockerfile, caches of all the following lines or layers will be busted and invalidated. 😣

So the rule here and the best practice is:
**Order your 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. 🚀


---
## 5️⃣ Best Practice ##
> **Use `.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 the auto-generated folders**, like `targets` or `build` folder, 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.

It's pretty straightforward. We basically just create this `.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.

![5th best practice](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9fso63260yg2o7sddcxa.png)

**Improvements:**
- Reduced image size

---
## 6️⃣ Best Practice ##
> **Make use of Multi-Stage Builds**

But now let's say there are some contents (like development, testing tools and libraries) in your project that you NEED for building the image - so during the
build process - but you DON'T NEED them in the final image itself to run the application.

If you keep these artifacts in your final image even though they're absolutely unnecessary for running the application, it will again result in an increased image size and increased attack surface. 🧐

So how do we **separate the build stage from the runtime stage**.
In other words, how do we exclude the build dependencies from the image, while still having them available while building the image? 🤷‍♀️

Well, for that you can use what's called **multi-stage builds** 💡

The 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:

![6th best practice](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/trdfkkdl96x1tkj4ffkh.png)

So these previous steps (marked "1st" in the above picture) will be discarded.

**Improvements:**
- Separation of Build Tools and Dependencies from what's needed for runtime
- Less dependencies and reduced image size


---
## 7️⃣ Best Practice ##
> **Use the Least Privileged User**

Now, when we create this image and eventually run it as a container, **which operating system user** will be used to start the application inside? 🤔
By default, when a Dockerfile does not specify a user, it uses a **root user**. 🙉  But in reality there is mostly no reason to run containers with root privileges.

❌  This basically introduces a security issue, because when container starts on the host it, will potentially have root access on the Docker host.
So running an application inside the container with a root user will make it **easier for an attacker to escalate privileges on the host** and basically get hold of the underlying host and its processes, not only the container itself 🤯  Especially if the application inside the container is vulnerable to exploitation.

✅  To avoid this, the best practice is to simply **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**:
![7th best practice](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zrjr4m1885u1yqbnsjik.png)

You can use a directive called `USER` with the username and then start the application conveniently.

**Tip:** Some images already have a **generic user** bundled in, which you can use. So you don't have to create a new one. For example the node.js image already bundles a generic user called `node`, which you can simply use to run the application inside the container. 👍


---
## 8️⃣ Best Practice ##
> **Scan your Images for Security Vulnerabilities**

Finally, how do you make sure and validate the image you build has a few or no security vulnerabilities? 🧐

So my final best practice is, once you build the image to **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. 

**Example output** of `docker scan` command:
![docker scan output](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tmwear5nj98iyfm63rik.png)

You see: 
1) the **type of vulnerability**, 
2) a **URL for more information** 
3) but also what's very useful and interesting you see **which version** of the relevant library actually **fixes that vulnerability**. So you can update your libraries to get rid of these issues. 👍

**Automate the scanning 🚀**
In addition to scanning your images manually with `docker scan` command on a CLI, you can also **configure Docker Hub** to **scan the images automatically**, when they get pushed to the repository. And of course you can **integrate** this check in your **CI/CD pipeline** when building your Docker images.

---
So these are **8 production best practices that you can apply today to make your Docker images leaner and more secure**! 🚀😊  Hope it is helpful for some of you! Of course there are many more best practices related to Docker, but I think applying these will already give you great results when using Docker in production.

>Do you know some other best practices, which you think are
super important and have to be mentioned? 
Please share them in the comments for others 🙌 👍


The full video is available here: 🤓
{% youtube 8vXoMqWgbQQ %}

-----------------
**Like, share and follow me** 😍 for more content:
- [Instagram - Posting many behind-the-scene stuff](https://www.instagram.com/techworld_with_nana/)
- [Private FB group](https://www.facebook.com/groups/techworldwithnana)
- [LinkedIn](linkedin.com/in/nana-janashia)
- [Twitter](https://twitter.com/Njuchi_)
- [YouTube](https://www.youtube.com/channel/UCdngmbVKX1Tgre699-XLlUA?sub_confirmation=1)