loading...

Containerize React app with Docker for Production

mubbashir10 profile image Mubbashir Mustafa Updated on ・6 min read

Create a new react app

This is only required if you are starting from scratch and don't have an existing code/app that you would like to containerize.

npx create-react-app my-app

Using "npx" ensures that you are always running the latest version of "create-react-app".

Docker - a very brief overview

Docker helps us to make containerized apps. What that means is that we create a single package that not only contains the code of our app but also all the required Libraries, software, configurations, etc.

Key Terms

I am not going to write down the actual definitions, rather I will try to explain them via examples or analogies.

Image: A package that contains everything needed to run your app. Think of it as a class in OOP.

Container: It is an instance of the image, just like an object in OOP

Volume: Images are read-only, to persist data you have to use volume. In simplest terms, you share a folder (on host OS) with your docker image to read/write data from/to it.

Dockerfile: This is where you define what will be inside the image you are trying to build. Like OS (e.g Ubuntu 16), Softwares (e.g Node), etc.

Tag: For now just consider it in literal terms.

Install Docker

Download and install docker. You might have to create a free docker hub account for that as well.

Configuration Files

We need to create conf. files for:

  1. Nginx (our web server)
  2. Dockerfile (to build the Docker image)
Nginx

Inside your app (my-app) directory, create another directory and name it nginx. Inside the nginx directory, you just created, create a new file and name it nginx.conf. You can also use to following commands (one-by-one to achieve it).

cd my-app
mkdir nginx
cd nginx
touch nginx.conf
Enter fullscreen mode Exit fullscreen mode

Edit the "nginx.conf" file you just created in whatever editor you would like and add the following code to it. This is nothing to be scared off, it's just a standard Nginx server block - even if you don't understand a single line in it it's just fine. The gist of what we are doing is that we are telling Nginx to listen on port 80, redirect every request to "index.html" and the root is "/usr/share/nginx/html" (the directory where to serve from).

server {

  listen 80;

  location / {
    root   /usr/share/nginx/html;
    index  index.html index.htm;

    # to redirect all the requests to index.html, 
    # useful when you are using react-router

    try_files $uri /index.html; 
  }

  error_page   500 502 503 504  /50x.html;

  location = /50x.html {
    root   /usr/share/nginx/html;
  }

}
Enter fullscreen mode Exit fullscreen mode
But why do we need NGINX???

The build files of Reactjs are just static (HTML, CSS, JS, etc.) files and you need some production-grade web server to serve your static files like Nginx, Apache, OpenLiteSpeed, etc.

Dockerfile

Inside your app directory create a new file and name it as Dockerfile.prod. Inside that file write the following lines of code.

# stage1 - build react app first 
FROM node:12.16.1-alpine3.9 as build
WORKDIR /app
ENV PATH /app/node_modules/.bin:$PATH
COPY ./package.json /app/
RUN yarn --silent
COPY . /app
RUN yarn build

# stage 2 - build the final image and copy the react build files
FROM nginx:1.17.8-alpine
COPY --from=build /app/build /usr/share/nginx/html
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx/nginx.conf /etc/nginx/conf.d
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Enter fullscreen mode Exit fullscreen mode

Create a new file and name it as .dockerignore and add node_modules inside it. This is simply to tell Docker to ignore the node_modules directory.

So your directory structure should be like this

my-app
│   Dockerfile.prod
│   .dockerignore    
│
└───nginx
      nginx.conf

Enter fullscreen mode Exit fullscreen mode
Step by step explanation:

Stage 1

  1. We will use a multi-stage Docker build. This is a new feature introduced in v17 of Docker.
  2. FROM tells what base image to use. You can check out base images at Docker Hub. You always have to specify a base image first.
  3. WORKDIR is used to specify the working directory (inside the image, not your host OS)
  4. ENV PATH adds node_modules in the PATH
  5. COPY is used to copy package.json from the current directory (on the host) to the working directory (in the image).
  6. RUN is used to run command, here we want to run Yarn with --silent flag to turn off the installation log. You might not wanna use this flag, as it silences the errors as well.
  7. COPY is run again to copy all the code from host OS to working directory in the image
  8. Finally, run yarn build to build our app

We copied package.json first to install the dependencies and didn't copy node_modules into the image. This is to leverage the excellent caching system of Docker and reduce build times.

Stage 2

In the first stage, we copied package.json to the working directory, installed the dependencies, copied our code and built the final static files. In stage 2 we are:

  1. Using Nginx as a base image. In "nginx:1.17.8-alpine", "nginx" is the image, and "1.17.8-alpine" is the tag. It's like we are telling what particular version/release of the Nginx base image we will use.
  2. We want to copy the build files from stage 1 to "/usr/share/nginx/html". This is the default directory where Nginx serves from. So we are copying our build files into this directory.
  3. Remove the default Nginx configuration file present at "/etc/nginx/conf.d/default.conf"
  4. Copy the configuration file we created into the docker image
  5. EXPOSE is used to expose the port (80 is the default HTTP port, that's what we want our Nginx to listen to). One pitfall here is that it doesn't actually expose the port, rather it is just for the sake of documentation. Like a new developer comes in, look at the Dockerfile and he knows what port(s) to expose while running the image.
  6. Finally, we want to run Nginx in the foreground, not as a daemon (i.e in the background).

Both CMD and RUN are used to run commands. The difference is that RUN is an image build step, whereas CMD is the command the container executes by default when you launch the built image (as a container).

Build Image

Its time to build image from the Dockerfile we created above. Make sure you are in the app directory at root level. Run the following command to build & tag your image.

docker build -f Dockerfile.prod -t my-first-image:latest .

  1. -f is used to specify the filename. If you don't specify it then you must rename your file to Dockerfile - that's what build command looks for in the current directory by default.
  2. -t is used to tag the image. You can tag your image the way you want (e.g v1.0.0, v2.0.0, production, latest, etc.)
  3. . at the end is important, and it should be added to tell docker to use the current directory.
Run your image as a container (instantiate your image)

The final step is to run your image (as a container)
docker run -it -p 80:80 --rm my-first-image:latest

  1. -it for interactive
  2. -p to expose and bind ports. Here we are exposing port 80 of the container and binding it with port 80 of the host machine. The first one is of your machine (host OS) and the second one is of the docker image container. For example, if you use -p 1234:80 then you will need to go to http://localhost:1234 on your browser.
  3. --rm to remove the container once it is stopped
  4. my-first-image:latest the name:tag of the image we want to run container of

Now open up your browser and go to http://localhost and you will see your app being served from the docker.

Extras

  1. run docker images or docker image ls to see list of all the images on your machine
  2. run docker container ls or docker ps to see all the running containers
  3. run docker system prune to prune the containers (be careful while using this command, read docs for options before using them)


Next: Deploy Your React App to ECS

Discussion

pic
Editor guide
Collapse
xai1983kbu profile image
xai1983kbu

Hi! Thank you for your article!
Is it possible to containerize NextJs App with Nginx in one container?
I don't understand how to run two processes in one container.
I find this answer stackoverflow.com/a/63038894/9783262 but have no success yet.

Can we containerize only NextJs App (without Nginx) for usage with "Fargate + Application Load Balancer"?

Collapse
xai1983kbu profile image
xai1983kbu

I manage to launch containerize NextJs App without Nginx
Example with Pulumi
github.com/pulumi/examples/issues/...

It's interesting now - Is it the right way (don't use Nginx)?

Collapse
mubbashir10 profile image
Mubbashir Mustafa Author

Umm, I would throw in Nginx there to do the load balancing, caching, compressing, etc. But yeah that's just my opinion :)
If you are generally interested in why we want to use Nginx with node server, this might help: expressjs.com/en/advanced/best-pra...

Thread Thread
mubbashir10 profile image
Mubbashir Mustafa Author

Btw you can add more than one container in ECS. That's how usually the node js apps are deployed (the other being a sidecar container).

Thread Thread
xai1983kbu profile image
xai1983kbu

Thank you soo much!

Thread Thread
abhipise81 profile image
Abhishek Pise

Can you guide me How to deploy Preact app into ecs?

Thread Thread
mubbashir10 profile image
Collapse
jayanath profile image
Jay Amaranayake

Well written series! nicely done!

Collapse
mubbashir10 profile image