Stop Using Insecure and Inefficient Dockerfiles
Tags: docker, node, javascript
Published: 2023-09-30 18:20:29

---
title: Stop Using Insecure and Inefficient Dockerfiles
published: true
date: 2023-09-24 14:39:10 UTC
tags: Docker, NodeJS, JavaScript
canonical_url: https://blog.scalablebackend.com/stop-using-insecure-and-inefficient-dockerfiles
---

![Cover](https://cdn.hashnode.com/res/hashnode/image/upload/v1692377174205/2e8928cb-2ef9-4070-8a93-aaca340f9b9a.jpeg?w=1600&h=840&fit=crop&crop=entropy&auto=compress,format&format=webp)

Often, I see terrible Dockerfiles used at length. It has a negative impact on security, productivity, and overall cost.

Today, I want to show you how to improve from a basic to a very efficient Dockerfile, step by step.

### Getting Started

![Basic Dockerfile](https://cdn.hashnode.com/res/hashnode/image/upload/v1692036754126/a64f12e5-84cf-4893-843d-a7ae9274cda4.png)

Its a very short Dockerfile, which has the advantage of being simple while working perfectly fine. It does the job of:

- Using the latest version of the official NodeJS image as a base

- Define a working directory (/app)

- Copying source files

- Installing dependencies

- Exposing port (8000)

- Running the app (yarn start)

But is it efficient? Obviously not. If it was, there would be no point to the article youre about to read.

### Parent Image

![Parent Image](https://cdn.hashnode.com/res/hashnode/image/upload/v1692036773390/d8de4ae6-610f-488e-8f4e-68b0a71975aa.png)

Here, there are three important points.

#### Using a fixed version

Maybe it seems like a good idea to use the latest version of an image as a base (to stay up-to-date), but its not. In practice, its often the source of issues.

If you use the last version of an image (latest), you dont have control over when it gets updated. Its possible ([and will happen](https://en.wikipedia.org/wiki/Murphy%27s_law)) that a new version of an image is published while being incompatible with your app.

Between two builds, your application will suddenly go from working to failing, without any apparent reason.

Thats why we prefer using a fixed version instead of latest. But that also means you have the responsibility to update your base images from time to time.

#### Long-Term-Support

Usually, a software is provided with specific LTS versions. That means creators provide better support for those versions, and should be preferred.

NodeJS provides a list of LTS versions on its [official website](https://nodejs.org/en).

#### Alpine

Docker images are built on top of a given distribution, such as Ubuntu and Debian. Among those distributions is [Alpine](https://www.alpinelinux.org/).

Its known for being very lightweight compared to others and is a huge help in keeping an efficient image.

### Lower Privileges

![Lower Privileges](https://cdn.hashnode.com/res/hashnode/image/upload/v1692036797111/18f5e756-23be-4547-9b44-20b438766e9d.png)

The default user being ROOT, he has unlimited access. For security, its a better idea to provide a user with limited privileges.

Fortunately, with the node image comes a user called node.

### Working Directory

![Working Directory](https://cdn.hashnode.com/res/hashnode/image/upload/v1692036812278/fe4dfc72-6117-42ef-b359-e2a68e64d248.png)

The working directory is the one used by default. Its a good idea to define a specific one for your app.

The most common practice is to use /usr/src/app.

### Caching

![Caching](https://cdn.hashnode.com/res/hashnode/image/upload/v1692036826598/2da42a89-9a42-4a59-88f9-6c0d5a5a6b4e.png)

Docker works with a [caching system](https://docs.docker.com/build/cache/#how-does-the-build-cache-work), which is often ignored. You can think of each step in a Dockerfile as a layer, which is cached.

When one layer changes, all subsequent layers are invalidated. When the image is rebuilt, instead of retrieving a layer from the cache, the necessary command is simply restarted.

One layer changes frequently: the source code. So its best to copy the source code as late as possible.

The most common mistake is to copy the list of dependencies at the same time as the source code, then install the dependencies.

In this case, every time the source code changes and the image is rebuilt, the dependencies will be reinstalled. Thats why we prefer to copy the files defining the dependencies first, then install them, and finally copy the source code.

### Configure the Working Directory

![Working Directory](https://cdn.hashnode.com/res/hashnode/image/upload/v1692036841361/16f9aa72-14ca-4534-bfc6-d937b2fc8286.png)

Previously, we defined a new Working Directory and set up a user with limited privilege. That means we need to create the necessary directory first and give necessary access to the node user.

### Update Package List

![Package List](https://cdn.hashnode.com/res/hashnode/image/upload/v1692036860333/ca5b5f95-8e56-4ca1-82b7-afcc06fe0b81.png)

With Alpine, we arent using [APT](https://wiki.debian.org/Apt), but [APK](https://wiki.alpinelinux.org/wiki/Alpine_Package_Keeper) to manage packages. No matter which tool you use to manage packages, you need to update its list from remote repositories.

That way, you ensure no out-of-date packages are installed. Its mandatory to avoid packages with security and performance issues.

### Adding Packages

![Adding Packages](https://cdn.hashnode.com/res/hashnode/image/upload/v1692362901910/92c10c7d-d2fb-45cd-947d-148b5b30be66.png)

I recommend using theno-cache flag to avoid generating cache you wont use, but will still make your image heavier.

### Multiple RUN

![Multiple RUN](https://cdn.hashnode.com/res/hashnode/image/upload/v1692348012937/4d530c81-66b5-45fc-a0de-06a3ce828add.png)

Inside [Best practices for writing Dockerfiles](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#run), we learn to:

> always combine `RUN apt-get update` with `apt-get install` in the same `RUN` statement.
> 
> Using `apt-get update` alone in a `RUN` statement causes caching issues and subsequent `apt-get install` instructions to fail.

For more information, I highly recommend you give [best practices](https://docs.docker.com/develop/develop-images/dockerfile_best-practices) a read.

### Entrypoint

![Entrypoint](https://cdn.hashnode.com/res/hashnode/image/upload/v1692036882624/186a9415-12ae-4bbf-857c-cfd4c7d58415.png)

I like to keep a basic Dockerfile that doesnt start my application directly. Instead, I use [Tini](https://github.com/krallin/tini), which keeps the container alive and improve how processes are managed.

{% embed https://docs.docker.com/build/building/multi-stage/ %}

Then, you can either use multi-stage builds to augment your basic stage, or start and configure your application from outside the Dockerfile.

Locally you can use the Docker CLI. Also, when you deploy your app, any container management system allows you to configure startup scripts.

That way, you can have a single Dockerfile that is used in different configurations, instead of multiple configurations with barely any difference.

It becomes the source of truth about which environment your app run on.

### Final Result

{% embed https://gist.github.com/morintd/f3a6bb08228bf30d705888b57e8dd1d4 %}

Now, you might be wondering: whats the difference?

Apart from better security and faster re-build, one of my real-world project is 1GB lighter with the improved image:

![Real World](https://cdn-images-1.medium.com/max/1000/1*W0p0twwebC7srWoO7SmJVw.png)

* * *

Do you want to learn more **backend** skills, you can effectively use in a **professional** environment?

{% embed https://www.scalablebackend.com/courses/bundle-scalable-backend-from-zero-to-hero %}

* * *

Cover photo by [Thais Morais](https://unsplash.com/@tata_morais?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/photos/9c6j-akotJQ?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)