Deploying ReactJS With Docker

How to package ReactJS with Docker and deploy it to Digital Ocean

Clone Create React App

For the purposes of this project, we’re going to use the standard Facebook Create React App as a base.

Assuming you have NodeJS version 10+ and can use the new npx feature, we’re going to scaffold out the project with:

npx create-react-app reactdocker;
cd reactdocker;

Test out the project:

npm start;

Should see it working on port 3000:

React Working On Port 3000

Stop the server with ctrl + c.

Creating The Environment

Next step is to create the ideal docker environment. Considering this is just HTML, CSS, and JavaScript, the only thing we need is an http server that can server up regular HTML. For this we’ll need NGINX. Starting fresh with alpine, let’s do this while in the repo directory:

docker run -it -p 3000:3000 -p 80:80 -v $PWD:/var/www/localhost/htdocs --name reactdocker alpine /bin/sh

While we’re in the container, let’s remove the node modules because they were installed with Mac OS and we need them to be installed with Alpine.

cd /var/www/localhost/htdocs;
# may take a few seconds
rm -rf node_modules;

We’ll need to install NodeJS and NPM for alpine:

apk add nodejs;
apk add npm;

Install the dependencies in the directory:

npm install;

Start the server:

npm start;
# [Expected Output]
# Compiled successfully!
# You can now view reactdocker in the browser.
# Local:            http://localhost:3000/
# On Your Network: http://172.17.0.2:3000/
# Note that the development build is not optimized.
# To create a production build, use npm run build.
Same React Output But From Docker

Perfect, let’s stop the server and get it to build.

# press ctrl + c
npm run build;

We can see the files in the /build folder:

cd build;
ls -al;

Those files will need an http server to expose them on port 80, for this we’re going to add nginx:

apk add nginx;

Next we’ll need to configure the nginx configuration file to point to the build folder:

# add nano to edit the file
apk add nano;
# modify the nginx default.conf file
nano /etc/nginx/conf.d/default.conf;

Here is the original file before:

default.conf

# This is a default site configuration which will simply return 404, preventing
# chance access to any other virtualhost.
server {
listen 80 default_server;
listen [::]:80 default_server;
# Everything is a 404
location / {
return 404;
}
# You may need this to prevent return 404 recursion.
location = /404.html {
internal;
}
}

Here is the file modified:

default.conf

server {
listen 80 default_server;
listen [::]:80 default_server;
        location / {
root /var/www/localhost/htdocs/build;
                # this will make so all routes will lead to      
# index.html so that react handles the routes
try_files $uri $uri/ /index.html;
}
# You may need this to prevent return 404 recursion.
location = /404.html {
internal;
}
}

Create the directory to allow nginx to run on a process id:

mkdir /run/nginx;

Test the configuration file and start the server:

nginx -t;
# [Expected Output]
# nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
# nginx: configuration file /etc/nginx/nginx.conf test is successful
nginx;

Now if we go to port 80 we can see build folder been served on nginx:

Nginx Serving React Build Folder

We can also see that the routing is working to point everything to react and give it responsibility to manage the routes:

Nginx Routing All Routes To React index.html

We’ve successfully created the environment so now we can create the Dockerfile to package things for deployment.

Create Dockerfile

Now that we know what dependencies we need, we’ll create a Dockerfile and optimize if for deployment with the dependencies to do a build.

Before we destroy the container, let’s copy the nginx configuration file we modified to the repository folder itself. Press ctrl + p then ctrl + q to exit the container, then:

# create a new config directory
mkdir config;
# copy default.conf to config/default.conf
docker cp reactdocker:/etc/nginx/conf.d/default.conf config/default.conf;
# now we can destroy the container
docker rm -f reactdocker;

Let’s start with the base Dockerfile in root of the repository:

FROM alpine
EXPOSE 80
ADD config/default.conf /etc/nginx/conf.d/default.conf
COPY . /var/www/localhost/htdocs
RUN apk add nginx && \
mkdir /run/nginx && \
apk add nodejs && \
apk add npm && \
cd /var/www/localhost/htdocs && \
rm -rf node_modules && \
npm install && \
npm run build;
CMD ["/bin/sh", "-c", "exec nginx -g 'daemon off;';"]
WORKDIR /var/www/localhost/htdocs

Let’s build it:

docker build . -t reactdocker

Running the container:

docker run -it -d -p 80:80 --name rdocker reactdocker;
React running from Docker container image

Now our container is ready to be push to Docker Hub and ready to be deployed.

Optimize Docker Image

You’ll notice that the COPY takes a bit of time to complete, so we’ll add an ignore file and remove some of the dependencies that we no longer need.

.dockerignore

node_modules

Next we’ll modify the Dockerfile to remove the dependencies we don’t need:

FROM alpine
EXPOSE 80
ADD config/default.conf /etc/nginx/conf.d/default.conf
COPY . /var/www/localhost/htdocs
RUN apk add nginx && \
mkdir /run/nginx && \
apk add nodejs && \
apk add npm && \
cd /var/www/localhost/htdocs && \
npm install && \
npm run build && \
apk del nodejs && \
apk del npm && \
mv /var/www/localhost/htdocs/build /var/www/localhost && \
cd /var/www/localhost/htdocs && \
rm -rf * && \
mv /var/www/localhost/build /var/www/localhost/htdocs;
CMD ["/bin/sh", "-c", "exec nginx -g 'daemon off;';"]
WORKDIR /var/www/localhost/htdocs

Let’s look at the size of the container before we build it again:

docker images | grep "reactdocker";
# [Expected Output]
# reactdocker latest 3c160b1a5941 16 minutes ago 489MB

Now we’ll build it again:

docker build . -t reactdocker;
# and see the size difference
docker images | grep "reactdocker";
# [Expected Output]
# reactdocker latest 669c991b23b6 20 seconds ago 36.6MB

That’s a huge difference from 489MB to 36.6MB.

Now run the container again:

docker run -it -d -p 80:80 --name rdocker reactdocker
Optimized React Docker Container

Now that we have our optimized Docker image, let’s push it to Docker Hub.

docker tag reactdocker {docker-hub-username}/reactdocker;
docker push {docker-hub-username}/reactdocker;

Create Docker Digital Ocean Droplet

After we’re successfully created our container, we’ll now go into Digital Ocean and deploy that same image.

Digital Ocean Create Droplet

Set some configuration for the Droplet:

Select pre-configured Docker One-Click app
Choose a small size because this is just a demo
Choose a region that is close to your for speed
Make sure you’re ssh is set up correctly and create the droplet

Wait for things to complete and copy the IP address:

Copy Droplet IP address once complete

Deploy Container

Now that we have the IP address, we can SSH into the container and perform the same docker run action:

# replace this droplet IP address with yours
ssh root@142.93.153.135;

It might prompt you for adding the key to the droplet, say yes.

Now that you’re in the droplet, let’s create that docker container:

docker run -it -d -p 80:80 --name rdocker {docker-hub-username}/reactdocker;

Once it’s up and running go the IP address in your browser, and you’ll see the react application is now running on the Digital Ocean droplet.

React running on Digital Ocean with Docker

We’ve now successfully packaged a ReactJS application in Docker and deployed it to Digital Ocean. 👏

What Else We Could Do

There are a few other things we could have done to improve things, including:

  1. Start tagging images with specific version to match a GitHub branch or tag
  2. Create a reverse proxy with a backend under the same url
  3. Create an SSL certificate to serve over HTTPS
  4. We could go a bit further into the nginx configuration with https://nginxconfig.io

I’ll create some more tutorials for those later on.

Any feedback or praise would greatly be appreciated.