Deploying a React + NodeJS Application with Docker | by Dan Murphy | Towards Data Science

I recently spent time working on a project with Dylan Edwards, and, in the following article, I will outline how we designed, built, containerized, and deployed a React + Node.js web application. The code for this project is available here.
For this project, we built a SPA (single-page application) where users could input a link to a tweet and retrieve an in-depth sentiment analysis of the Tweet's contents. The sentiment analysis is broken into two parts: the overall document and each individual sentence. Prior to the development of the application, we spent some time mocking up designs in Figma. This helped us iterate early and often on the design of various components, which ultimately helped us build an enjoyable and intuitive UX. Below is the Figma page that outlines several of the designs we tested prior to beginning development.
After landing on a final design (shown at the very bottom of the embedded Figma document), we structured the work into two buckets: frontend and backend. First, we needed to build a backend that retrieved data from the Twitter V2 API, sent it to the Google NLP API, and structured the responses for easy frontend consumption. To accomplish this task, we decided to use an express.js backend with one endpoint that accepts the Tweet ID as a parameter (POST /analyze/:tweet_id). When a user submits a valid tweet URL, the tweet ID is parsed from the URL and sent to the backend, where we then request tweet and user information from the Twitter V2 API, parse the response from Twitter to extract the tweet contents, and send that contents to the Google NLP API for sentiment analysis. After this, pending a successful response from both the Twitter and Google NLP APIs, the data is structured and returned to the frontend, where it is then formatted and passed to various React components. Below is a diagram of this process.
For the frontend, we rely heavily on styled-components for designing the UI, redux for passing data to and from different components (this provided us with a more centralized way to manage the state of our application), and Chart.js for building the Bar Charts. Additionally, the EmbeddedTweet component is built purely with styled-components to replicate the look and feel of a tweet.
After development was finished, we decided to containerize the app with Docker. To do this, we created Dockerfiles for both the frontend and backend applications and then used docker-compose to aggregate the two Dockerfiles and deploy the containers on the same network. For the frontend, the Dockerfile looks like this:
Here is an overview of the commands:
FROM: Defines the image we will use as the basis for our container. In this context, we use Node Version 12.18.3.
LABEL: Defines the metadata for our Dockerfile, including information about the version, description, and maintainers.
WORKDIR: Sets the working directory for the application. If the working directory does not already exist, it is created by Docker.
COPY: Copy a file or files into the Docker container. On line 9, we list several files to copy and then specify where to copy them. The final path specified is the location the files are copied to (in this case, the working directory).
RUN: Specifies a command that should be run by Docker. I typically use this command to install dependencies, debug, and execute commands in the container. In this context, we run npm install --production to only install production dependencies.
EXPOSE: Tells Docker which ports the container should listen on during runtime. In this context, we expose port 3000 since that is the port that React is running on.
CMD: Specifies the command that should be executed when the docker container starts. In this context, we run npm start since that is the command that starts the frontend.
Similarly, we repeat this process for the backend. Below is the Dockerfile for the backend API:
We follow most of the same steps as in the previous Dockerfile, with the exception of the port we expose (5000) and the command we run on startup:
node backend.js
Finally, after the two Dockerfiles are written we can create the docker-compose.yml file in the root of our project directory. Here is what this file looks like:
First, we define the names of the services that make up our application: backend and frontend. Then, within each of these services, we provide the build context, the path to that build context’s Dockerfile, the name for the Docker image, and the necessary port mappings. Additionally, since the backend service depends on a few environment variables, we specify the path to the .env file. Lastly, and most importantly, we use the links key under the frontend service to create an alias by which the frontend service can communicate with the backend. By default, when we run
docker-compose up -d
both services are created under the same network and one service can communicate with the other via that service's name. By defining links in the YML file, we can now reference the backend service via the proxy defined in client/setupProxy.js at either http://be:5000 or http://backend:5000 (you can also define a proxy in the client/package.json file). You can learn more about networking with docker-compose here.
Lastly, to test your application locally, you can run
docker-compose up -d
and navigate to http://localhost:3000. Alternatively, if you want to run the application on port 80, you must change the port's value in the frontend service of the docker-compose.yml from “3000:3000” to “80:3000”. Furthermore, you can use
docker-compose logs
to display the log output from the services, which is useful for debugging.
Now, since we have designed, developed, and containerized our application, the next step is deployment. For this section, it is assumed that you have a basic working knowledge of AWS EC2. There are many ways to deploy a dockerized application, but for the sake of this tutorial, we will use an AWS EC2 instance. To get started. you’ll need to navigate to https://aws.amazon.com/ec2 and click on Get Started with Amazon EC2.
Then, as shown in the below GIF, you can click on Launch Instance, select Ubuntu 18.04, and then Review and Launch.
Prior to launching your instance, you will need to open up Port 80 (assuming this is the port you map the frontend service to). If you are mapping the frontend service to a different port, then you must do the same thing but with a different Port Range. To accomplish this, click on Edit security groups and add a new rule. When finished, your inbound rules should look like this:
Now, you can click Launch, create a new key pair or use an existing one, and then connect to the EC2 instance in the AWS CloudShell or your own terminal. If you need help connecting, use the steps outlined in the following image (replacing the pem file and the public IPv4 DNS with your own instances). Alternatively, AWS has documentation on connecting to an EC2 instance here.
Now, assuming that you successfully SSH’ed into your EC2 instance, you will need to install Docker and update other previously-installed software. This is accomplished with the following commands.
$ sudo apt-get update # run updates$ sudo apt-get remove docker docker-engine docker.io containerd runc # remove old versions of Docker if they exist$ sudo apt install docker.io # install docker$ sudo systemctl start docker # start docker$ sudo systemctl enable docker # enable docker
After this, you can check that the installation was successful with the following command:
$ docker --version
Now, we will clone the repository from GitHub, CD into the root directory, and run docker.
$ git clone https://github.com/dylane1999/SentimentAnalysisApp.git # clone$ cd SentimentAnalysisApp # change into the directory$ docker-compose up -d # run Docker in detached state$ docker-compose logs # check logs
Finally, you can navigate to the Public IPv4 address of your EC2 instance and check out the application. Assuming your Public IPv4 address is 54.175.170.20, the frontend service is mapped to Port 80, Port 80 is open to accept traffic in your inbound security rules, and you have valid Twitter API and Google NLP API Credentials set as environment variables in backend/backend.env, you will be able to open the application at http://54.175.170.20/ and submit tweet URLs for sentiment analysis!
Overall, in this article, you learned about the process of designing, developing, containerizing, and deploying a full-stack web application.
First, to design the application, we used Figma. During this phase, we mapped out the components of the application, iterated on the design, and created a final version that we could then build with React.
After designing, we then turned our mock-ups into a fully-functioning web application. This web app is broken into two ‘buckets’:
Frontend: built with React, Chart.js, Styled-Components, and Redux.
Backend: built with Node.js, Express.js, and the Twitter V2 and Google NLP APIs.
After completing the development of the application, we used Dockerfiles to containerize the frontend and backend and docker-compose to run the frontend and backend containers on our local computer under one network.
Finally, to deploy our application, we used AWS EC2. On our EC2 instance, we opened up port 80 for inbound traffic, installed and updated the necessary software, cloned the repository, and ran docker-compose up -d to run the application.