Dockerizing a React App | Introduction to Multi-Stage Builds
Have you ever wondered🤔, the dockerized React app that we use in development is not the same that is deployed in production? There is an extra build step required! But wait, isn't that the primary purpose of containerization, i.e, to have the same environment and same code in the development and production. This extra build step beats the whole idea of the same environment and same code. Well, let's have a look further.
The Development Setup
A dockerized React app in development has a Dockerfile that looks something like this,
FROM node WORKDIR /app COPY package.json . RUN npm install COPY . . EXPOSE 3000 CMD ["npm", "start"]
This is a pretty standard
Dockerfile that we would write to containerize a React app during development. But we won't deploy the same in production.
Generally, any front-end framework or library be it React, Angular, Vue, etc, requires a build step. Consider a React app, where we have a
build script that spits out JS code that runs in the browser. The requirement of a build step is due to reasons such as,
The front-end code is meant to run in the browser so it ships with its own mini development server basically a
nodeJSserver that compiles the code(JSX) to browser executable code and also supports live reload.
The mini development
nodeJSserver that a react app ships with serves the
index.htmlfile which in turn imports the transformed JS code from our
srcfolder! That's how these front-end projects work.
The development code cannot run in the browser natively because we use experimental JS features that we use to write code during development hence compilation is required.
npm startcommand runs the start-script which uses a third-party package
react-scriptsto transform JSX into browser-friendly code, optimizes it, shrinks it to make it as small as possible, and does a bunch of other stuff behind the scenes.
If the same setup is used/ deployed in Production, it would be too resource-heavy and too inefficient!
The build step basically is an optimization script that needs to be executed after Development but before Deployment.
So, the development server is not meant to run in production. It is not optimized for that and it would be way too slow to be used in production. For production, React projects, and all similar projects bring their
buildwill not start any server instead it compiles and optimizes our code and spits out this transformed and optimized code in a separate folder that we can then serve ourselves using any web server of our choice.
The Production Setup
From the above
Dockerfile we have a great development setup for our containerized React setup but we cannot simply replace the
CMD ["npm", "start"] instruction with something like
CMD ["npm", "run", "build"] as a final command because it will not start any web server or process that would be reachable by any HTTP request. Instead, it will just give us the finished code.
Therefore we have to find a way of building a Dockerfile which can be used to build a container that runs this application in production.
Our React app needs to be executed differently in development and in production. We need separate containers for development and the final build/deployment process. Therefore we are setting up two different environments because our React app forces us to do so.
So, let's build the other container! We will use a separate Dockerfile for the build container. We can name this Dockerfile
FROM node:14-alpine WORKDIR /app COPY package.json . RUN npm install COPY . . CMD ["npm", "run", "build"]
Here, we have changed the base image from
node:14-alpine which is even a slimmer and lighter version of nodeJS. We are not going to use nodeJS as our production server. It is for the build step only hence a slimmer version to speed up the build process.
Also, you can see that rest of the instructions are the same as our original Dockerfile. Except for the last one where we use a
CMD ["npm", "run", "build"] instruction to generate the finished code inside the container. But, wait this is not a finished Dockerfile. This will just give us the finished code but not a running server.
If we want to use this container in production we also need a server along with the finished files(optimized code). To solve this problem let me introduce you to the multi-stage build that docker offers.
Introducing Multi-Stage Builds
Multi-stage builds allows us to have one
Dockerfile but define multiple build steps/stages or setup steps inside of that Dockerfile. These stages can
copy results from one another. This means that in our React application,
One stage to create the optimized files.
Another stage to serve the optimized files.
We can either build the complete image going through all the stages step by step from top to bottom or we can select individual stages up to which we want to build that will skip all stages that would come after them.
So we can consider the above
dockerfile.production up to the
CMD ... instruction as our first stage where we build our deployable source code.
So let's build the second stage and the completed Dockerfile will look like this,
FROM node:14-alpine as build WORKDIR /app COPY package.json . RUN npm install COPY . . RUN npm run build FROM nginx:stable-alpine COPY --from=build /app/build /usr/share/nginx/html EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]
Let's see what we are doing here in this
We are using the
node:14-alpineas our base image for the build process but as soon as we write a second
FROM ...instruction Docker discards our previous step/stage and we will switch to a new base image.
But we don't want to discard our previous stage as we need our finished deployable code from our build step, i.e,
RUN npm run buildin our second stage. So, we add a special instruction that can be added after every
FROM ...instruction. We use the
askeyword and we can give any name of our choice. I have taken
buildas the name of this step here.
Rest all the instructions up to
COPY . .are the same as our development Dockerfile. The
RUN npm run buildmarks the end of stage 1 of the build process where instead of
CMD ...we use
RUN ...so that we can continue with more steps thereafter.
The final container will only include the second stage but it will build stage 1 to derive the final stage.
FROM nginx:stable-alpineinstruction marks the start of our second stage where we switch from
nginx:stable-alpineas our base image. This image is also a very lightweight and trimmed down version of
nginx. More details on the image on dockerhub.
Now we do something special in the
COPY ...instruction of our second stage. We copy from the first stage using the
--fromflag. Then we specify the name of the stage from which we want to copy.
We have our first stage named
buildso we specify
--from=. This specifies that we copy the final content from the
buildstage to the second stage.
This tells Docker that the
COPY ...instruction in our second stage does not refer to our
localhostproject on our local machine but instead to the file system from the
We specify the source path for what we need to copy. So in the case of React app production builds the generated serveable code lives inside the build folder of the project root. Inside our container, we have
/appas our working directory. So we need to copy from our
We also need to specify the default folder from where
nginxwill try to serve the files. So we specify the location
usr/share/nginx/html. More details can be found on the official
nginximage on Dockerhub.
80internally by default. So we specify the
EXPOSE 80instruction to expose the container port.
Then we specify the final instruction
CMD ["nginx", "-g", "daemon off;"]which will start the
nginxserver. And as the official documentation states on Dockerhub, we should add the
-g daemon off;option if we start the server ourselves which we are doing here, i.e, we start the server after we copy our custom code from the
This is all we need to do! We have completed our multi-stage Dockerfile and this will successfully build a container that has production-ready code and is ready to be used in production.
As you can see we have 2 dockerfiles here. So normally if you are developing your app then docker will pick up the default Dockerfile that is just named
Dockerfile. Build your production-ready image by specifying the correct Dockerfile by using the command below with the
docker build -f frontend/Dockerfile.production -t <your_repository_name> ./frontend
Then push your build image to the repository you are using. In my case, I am using Dockerhub. So,
docker push <your_repository_name>
In the case of deployment on AWS ECS
URL's inside your react app resembles something like
http://localhost/users it won't work. This is due to the fact that on AWS ECS
localhost is a special keyword that will allow your Dockerized application to send requests to other containers running in the same AWS ECS task.
In short, the react code runs on the browser of end-users so
localhost will refer to their machine instead. The proper domain depends on how you are deploying your application.
In my case, I deployed my app in the same task as my nodeJS REST API which means that my react app will be reachable via the same URL. So I am using just
/users instead of
It totally depends on your method of deployment. So I would recommend having a good look at the documentation of your own Cloud Service Provider.
We can have as many stages as we might need in the complete build of our application. This is a very powerful feature offered by Docker because it's great for situations like this where we have an application which we can't serve the way it is shipped to us but we need to build first. That's why the multi-stage builds are awesome😍And overall Docker is awesome!
I hope you liked this post and an emoji on the post would definitely make me happy😄. Feel free to ask your question regarding this post in the comments. Happy Coding👨🏽💻👩💻!