How to deploy a React application to production with Docker multi-stage builds

hero-image

Prerequisites

Before reading you should have some fundamental knowledge of the following technologies:

  1. Node.js and npm
  2. React
  3. Docker
  4. Nginx

Let’s suppose you’ve created your app using Facebook’s create-react-app scaffolding tool. One of many benefits of using this tool would be that it completely removes the complexity (aka pain) of setting up Webpack, Babel, and React.

I’d recommend using it, as it kickstarts your development workflow a lot. The command npm run build builds your app to a “build” folder within your project root. It bundles React in production mode and optimizes the build for best performance.

Docker multi-stage builds

Version 17.05 of Docker has introduced a feature called multi-stage builds. With multi-stage builds, you can use several FROM statements in your Dockerfile. Each FROM instruction can use a different base, and each of them begins a new stage of the build. You can copy artifacts from one stage to another and leave behind everything you do not want or need in the final image. As a result, you end up with a lean image.

What does this mean in our context?

Our React application is a simple web project, which consists of static files, such as CSS, Javascript files and a few images. This only requires a web server such as Nginx or Apache at runtime.

Besides the build environment looks different. You might need a preprocessor like SASS or a transpiler like TypeScript plus you get Node.js and npm as dependencies on board. If you do not take this into account you end up with an image containing interpreters you don’t need at runtime.

So let’s have a look on how we can separate these environments. Let’s dive in…

Stage 1: The Build

Image for post

Photo by Iker Urteaga on Unsplash

Within stage one we’ll extend our image from a Node.js base image and label it as “build”. The name does not matter and you’re free to give it any name you want. It will allow you to reference it later on.

FROM node:9.11.1 as build

Next we make a directory for our app and set it as our working directory.

RUN mkdir /usr/src/appWORKDIR /usr/src/app

We expose all Node.js binaries to our PATH environment variable and copy our projects package.json to the app directory. Copying the JSON file rather than the whole working directory allows us to take advantage of Docker’s cache layers.

ENV PATH /usr/src/app/node_modules/.bin:$PATHCOPY package.json /usr/src/app/package.json

Having all in place we’ll install our dependencies by running npm install. Finally we’ll setup react-scripts, copy our app sources and build it with npm run build. Our app is now ready for production.

RUN npm install --silentRUN npm install react-scripts -g --silentCOPY . /usr/src/appRUN npm run build

Stage 2: The Production Environment

Image for post

Photo by Jelleke Vanooteghem on Unsplash

As mentioned above all we need to run the app is a web server. Nginx to be precise. We start stage two by extending the Nginx base image from the official repository hosted on hub.docker.com. The Nginx image we’re using is based on Alpine Linux which is small (~5MB), and thus leads to slimmer images in general.

FROM nginx:1.13.12-alpine

Next we can copy over the contents of our build directory to the directory Nginx serves by default. Docker allows us to reference the results of the first stage by the label we provided: “build”.

COPY --from=build /usr/src/app/build /usr/share/nginx/html

Nginx will serve the on port 80 so we need to expose it. We’ll map this port to a port on the host running the container later on.

EXPOSE 80

Finally we define the default command which Docker will run when executing the container. We tell docker to start the server and direct it to not run as a daemon.

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

Build and Run the Docker Image

Having everything in place we can now build and tag the image by running docker build within your projects directory. Don’t miss the little dot pointing to that directory.

docker build -t myapp:mytag .

To run our freshly baked image we call the docker run command. We tell Docker to run the container detached in the background and finally map the internal port 80 which is used by Nginx to serve our project to port 3000 of the host system our Docker instance is running on.

docker run -d -p 3000:80 myapp:mytag

Conclusion

By using Docker’s multi-stage feature we managed to separate build and runtime environments. As a result we build leaner images, which leads to faster deployments and a more efficient containerization in production.

Check out the whole example on github.com