Blog

Creating slim docker images for react apps

In this blog post, we will create a slim, production ready docker image for a react application. This process is often termed as Dockerization or containerization in developer communities. Containerization helps create portable and reproducible builds of your application that are easy to ship and deploy to any compliant host environment.

Containerizing a React app

To do so, we will leverage multi-staged docker builds and to keep the image size smaller, we will use alpine variant of popular docker images.

Let’s get started.

Ignore files with .dockerignore

First thing first, we want to reduce the size of the build context i.e. make some of the files invisible to docker daemon when running docker commands. By default, build context is a list of files residing in the same directory as that of Dockerfile. A smaller build context means docker can start building images faster.

Let’s create a file named .dockerignore with a list of files or folders to ignore.

We want to ignore the node_modules folder because we want to fetch npm dependencies again while building docker images instead of using existing dependencies on a developer’s machine. This ensures that correct dependencies are installed on the image.

We also want to ignore the build folder which is something specific to react applications and it contains the generated application bundles. We want to create an application build within image and hence can ignore the local build folders.

.dockerignore

node_modules
build

Create a Dockerfile

Instructions for creating docker images are stored in a file called Dockerfile. We are going to use multi-staged builds which means our Dockerfile will include two or more stages i.e. parts of the build process than can be discarded after producing some output.

For us, the first stage is going to be a node process that installs dependencies and then builds the react applications using something along the lines of npm install && npm run build. These instructions are pretty straightforward and React developers should feel right at home.

Let’s see how the first stage looks like in a Dockerfile:

Dockerfile

# build
FROM node:10-alpine as build_stage
WORKDIR /src
COPY package.json .
COPY package-lock.json .
RUN npm ci
COPY . .
RUN npm run build

This set of instructions in the Dockerfile describes our first stage called the build_stage.

What we did here was copy package-lock and package.json first and then run npm ci. We used npm ci instead of npm install because npm ci does a few more checks and makes sure the dependencies installed are from the lock file. This helps prevent accidental upgrades of packages leading to breaking changes. If there’s a discrepancy between lock and package.json, npm ci may choose to fail which helps catch errors earlier in the build process.

After dependencies are installed, we make sure that the source code for the application is available to the build stage by copying current directory into the container. .dockerignore also comes into play here. Instructions like COPY . . use .dockerignore to skip copying ignored files and folders into the container.

And finally we run the npm run build command which creates the distribution bundle for our react application in the build directory.

Next, let’s create the second stage of our multi-stage Dockerfile

# deploy
FROM nginx:stable-alpine as prod_stage
COPY --from=build_stage /src/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

We have used nginx:stable-alpine as the base image for the next stage because this is the production stage that exposes our application to the web. The alpine variant makes sure we get super slim build images, which can reduce network bandwidth, docker repository storage costs and in some cases increase the performance of containers.

We copy the outcome of build_stage i.e. our react application bundles into nginx’s html directory; then expose port 80 and setup a command to be executed when the container is spun off. At this point, the first stage i.e. build_stage is discarded and the final docker image only includes whatever we did within the final stage i.e. the prod_stage.

And that’s it.

To build, run the docker build command:

docker build -t react-starter-2020:latest .

And to run the application:

docker run -p 9090:80 react-starter-2020

With that, we are able to build production ready docker image for our react application. The docker image size for me is 25.9 MB. This can vary depending on bundles sizes, assets etc. You can also checkout relevant sources on my Github - https://github.com/androidfanatic/react-starter-2020.

Thanks for reading.