How I reduced the size of Docker Image from 1.99 GB to 42 MB

How I reduced the size of Docker Image from 1.99 GB to 42 MB

If you are into developing web applications, then you must have tried to deploy them someday so that others can also see your awesome application. There are many different ways to deploy your app, and some of the most popular methods include:

  1. Vercel

  2. Heroku (Used to be free)

  3. Netlify

  4. Firebase

  5. GitHub Pages

We can deploy our app directly using anything from the above, but things can get out of our hands if we don't configure the application properly. You must have come across the famous line "It works on my machine." To solve this problem of deployment across various environments, we use the help of Docker. Docker takes care of all this stuff, all we have to do is create a Dockerfile and specify our requirements in it. Docker then creates a "Docker Image" that is not dependent on the environment where it is running. All it requires is that Docker is installed on that environment. This makes our job much easier when compared to the traditional ways of deployment. However, this Docker Image comes with its own cost, which is its size. If not properly handled, the Docker Image can be very huge.

Let's understand this with an example of dockerizing a basic React application.

Building the React App

I will not use create-react-app to build my app but will be using Parcel to keep it simple. (Parcel is a simple and easy-to-use tool for building your React application without any unnecessary overhead.) "Parcel is a Beast" - Akshay Saini

I won't go into the details of each file, but you can simply copy the following steps and code:

  1. Create a folder for your project and navigate into it

     mkdir React-Docker
     cd React-Docker
    
  2. Now, initialize the npm package, i.e., our project, using the command npm init -y. (I'm using the -y flag to set default values. You can remove this and enter your values.)

  3. Install the required packages (-D for Dev Dependency)

     npm i react react-dom
     npm i -D parcel
    
  4. Create the index.html file and place the following code

     <!DOCTYPE html>
     <html lang="en">
     <head>
         <meta charset="UTF-8">
         <meta name="viewport" content="width=device-width, initial-scale=1.0">
         <title>My React App</title>
     </head>
     <body>
         <div id="root">Not Rendered</div>
         <script type="module" src="index.js"></script>
     </body>
     </html>
    
  5. Create the index.js file and place the following code

     import React from "react";
     import ReactDOM from "react-dom/client"
    
     const root = ReactDOM.createRoot(document.getElementById("root"));
     root.render(<>
         <h1>React Application</h1>
         <p>Hello there!</p>
     </>);
    
  6. Let's write some scripts for building our app in the package.json file

     ....
     ....
     "scripts": {
         "start": "parcel index.html",
         "build": "parcel build index.html",
       },
     ....
     ....
    
  7. Now, to run our app, use the command npm start. By default, Parcel will run our application on http://localhost:1234 if the port is available.

Dockerize the React App

Now let's dockerize our React App by creating a file named as Dockerfile in the root of your project and paste the following code

FROM node:18
WORKDIR /app
COPY package.json ./
RUN npm install
COPY . .
EXPOSE 1234
RUN ["npm","start"]

Let's build the Docker Image from the terminal by running the following command

docker build . -t hashnode/react:1.0

We can now run the image by spinning up a container using the following command

docker run -p 1234:1234 hashnode/react:1.0

Now, let's determine the size of this Docker image by running the following command in our terminal:

docker images

This command will display all the images present in our system, along with their corresponding information, including the size.

We can observe that the Docker Image size is 1.99 GB, which is excessively large for a simple React application that displays "Hello World." Now, let's explore how we can reduce its size.

Best practices to reduce the Docker Image size

Using Dockerignore

You must have heard of the .gitignore file, which is used to exclude unnecessary files from being tracked. Similarly, we use the .dockerignore file to ignore files that are not needed, such as node_modules, etc.

Let's place the below files into the .dockerignore file

dist/
node_modules/
.parcel-cache/

Now we build our image and see the size as we did before.

Comparing it to the previous image it got reduced to 1.63 GB (~18% decrease)

Choosing a Proper Base Image

In the above configuration, we have used node:18 as our base image. However, traditionally, node images are based on Ubuntu, which is unnecessarily heavy for our simple React application. We can instead visit DockerHub (the official Docker Image Registry) and search for Alpine-based images. These images are very similar to the original image but have minimal dependencies and significantly smaller sizes.

Using the alpine-based image the Dockerfile will be as follows

FROM node:18-alpine
WORKDIR /app
COPY package.json ./
RUN npm install
COPY . .
EXPOSE 1234
RUN ["npm","start"]

Now let's build our image and see the size as we did before.

Comparing it to the previous image it got reduced to 711 MB (~65% decrease overall)

Using Multi-Stage Build

This method builds the Docker image in stages rather than all at once. Previously, we copied the entire source code of our application into the Docker image and then built the image. However, this is unnecessary as we only require the build files, not the entire source code.

To address this, we divide the process into two stages: the BUILD stage and the RUN stage. In the BUILD stage, our application is built, and the required build files are returned. Since we are using Parcel as our build tool, it generates the build files in the /dist folder.

While we typically use the server provided by Parcel to run the application, it's not the best option for production. Instead, we can use a more efficient and lightweight server like Nginx to serve our application.

In the RUN stage, we first copy the /dist folder from the BUILD stage into the appropriate working directory of the image, and then we build the image.

The final Dockerfile, utilizing a Multi-stage build, looks like this:

# Stage 1
FROM node:18-alpine AS BUILD

WORKDIR /app

COPY package.json ./

RUN npm install

COPY . /app

RUN npm run build

# Stage 2

FROM nginx:stable-alpine

COPY --from=BUILD /app/dist /usr/share/nginx/html

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

Now let's build our image and see the size as we did before.

Comparing it to the previous image, the size has been reduced to 42 MB, resulting in approximately a 97% decrease overall. This is significantly smaller than the size of the original image. Additionally, we are now using a much more efficient server to serve our awesome application.

We can run our application using the following command

docker run -p 1234:80 hashnode/react:1.3

These are some of the easy ways we can use to reduce the Docker image size. The same principles apply to other images as well. Now, your Docker image is significantly more portable and efficient.

Thank you for reading the article. If you found it informative or interesting, please give it a thumbs up. I would highly appreciate it if you could share it with your friends as well.