2022-12-03T20:02:17Z

The React Mega-Tutorial, Chapter 12: Production Builds

You have an application that you have been using in your own computer during development. How do you put this application in front of your users? In this chapter you are going to learn how to work with production builds of your application.

The complete course, including videos for every chapter is available to order from my courses site. Ebook and paperback versions of this course are also available from Amazon. Thank you for your support!

For your reference, here is the complete list of articles in this series:

Development vs. Production Builds

The version of the application that you've been running is called a development build. The main goal of a development build is make it easy for the developer to test and debug the application while changes are constantly made.

An important detail to keep in mind is that development builds sacrifice performance and file size in favor of debuggability. Many functions provided by React and related libraries include additional logging or instrumentation, with the purpose of helping the developer detect and fix issues.

A production build is a highly optimized version of the application, both in terms of performance and size, intended to be deployed on a production server and used by real users.

Generating React Production Builds

A production build of the application can be generated at any time with the following command:

npm run build

The command will run for a minute or two, producing output similar to the following:

Creating an optimized production build...
Compiled successfully.

File sizes after gzip:

  85.78 kB (-6.46 kB)  build/static/js/main.52a6162f.js
  24.57 kB             build/static/css/main.d11fbe84.css
  1.78 kB (-96 B)      build/static/js/787.49411143.chunk.js

The project was built assuming it is hosted at /.
You can control this with the homepage field in your package.json.

The build folder is ready to be deployed.
You may serve it with a static server:

  npm install -g serve
  serve -s build

Find out more about deployment here:

  https://cra.link/deployment

The command creates the production build of the application in the build subdirectory of the project. The build process performs the same code checks as the development build and will print any warnings found in your code.

The serve package recommended in the output of the build command was installed locally in Chapter 2. To test the production build on your computer, you can start a web server as follows:

npx serve -s build

Note that for this command to work, you have to stop the development web server, which also runs on port 3000.

Structure of a Production Built Application

After the production build completes, the build subdirectory contains the files that should be deployed to the root of your production web server. Some files you'll find in this directory are:

  • index.html: The entry point of the application.
  • favicon.ico: The application icon, displayed in browser tabs.
  • manifest.json: The web application manifest, which provides instructions on how the application can be installed on the user's device.
  • Additional icon files of different sizes for use in mobile devices.

The build directory also has a subdirectory of its own named static. This is where the application files are located.

Inside static, there are two subdirectories called js and css, which hold the JavaScript and CSS files respectively. The application code is stored in files that start with a main. prefix. Depending on the application and the third-party packages used, a number of additional JavaScript and CSS files can be included in the build.

For each .js and .css file, React also includes corresponding .js.map and .css.map. Map files make it possible to perform some debugging tasks on the production build. As a side effect of that they may also allow your users to inspect a fairly readable version of your application's source code, so depending on your particular case you may decide not to copy the map files to the production server.

All the files inside the static subdirectory have hashes in their filenames, which are unique for each build. For this reason, and with the goal of improving performance, the contents of this directory can be configured to have a long caching controls if the web server provides this option.

For applications that are very large, the main JavaScript bundle may end up being a fairly large file that can adversely affect the application start up time. React supports a technique called code splitting that can be used to move less frequently used components to separate bundle files that are loaded on demand when the components are first rendered.

Deploying the Application

To deploy a React application you have to publish all the files that are inside the build subdirectory (except maybe the .map files) to the root directory on a production web server, preserving the directory structure. There are many options for deployment of React applications, some of which are discussed in the following sections.

An important deployment detail that needs to be observed, is that the web server must serve index.html whenever a non-existent path is requested. This is necessary to support client-side routing. If a user bookmarked a specific client route and attempts to start the application from it, a standard static file web server will return a 404 response, since the requested resource is not known in the server. To let React-Router handle this URL in the client, the index.html page should be served. For the serve command, the -s option achieves this.

The serve command

If you have access to a server connected to the Internet, then the simplest option is actually to use the serve command, as shown above. This web server can be configured to listen on a different port other than the default 3000, if desired. It also has options to add TLS encryption through an SSL certificate.

Static Site Hosting Services

There is a great variety of services that offer static file serving, many at no cost. Here are some of them:

For many of these options, the site can be deployed with a single terminal command, which is very convenient because you can incorporate it as a custom npm command in the scripts section of the package.json file as shown in the following example:

{
  ...
  "scripts": {
    ...
    "deploy": "npm run build && [your deploy command here]"
  },
  ...
}

This would allow you to deploy your application with npm:

npm run deploy

Back End Static Files

In addition to the production build of the React application, you will need to deploy your back end. The method of deployment for the back end will vary depending on the language and framework used, so it is outside the scope of this book to cover this aspect of the deployment.

Aside from specific back end deployment methods, it is very common in back end frameworks to provide an option to serve static files alongside its endpoints. If your back end provides this option, you can configure the static directory for your back end framework to be the build directory of your React application, and then both front and back ends will be served by the back end framework's web server.

Reverse Proxy

Many back end services use a reverse proxy such as Nginx as the public facing web server. For this type of installation, the reverse proxy can be configured to serve the static files of the React application's build and to proxy to the back end for paths that have a predefined prefix such as /api.

The following nginx configuration is an example of this technique. Here the React application is assumed to be in /home/ubuntu/react-microblog, and the back end server is listening on port 5000 in the same host.

server {
    listen       80;
    server_name  localhost;

    root   /home/ubuntu/react-microblog/build;
    index  index.html;

    location / {
        try_files $uri $uri/ /index.html;
        add_header Cache-Control "no-cache";
    }

    location /static {
        expires 1y;
        add_header Cache-Control "public";
    }

    location /api {
        proxy_pass http://localhost:5000;
    }
}

This configuration sets the web root to the build directory of the React project. It configures URL paths starting with /api to be proxied over to http://localhost:5000, while all others are served as static files.

The try_files directive defines /index.html as its final argument, so that Nginx serves the React application for any paths that have no matching file on disk. This allows bookmarks to specific routes such as /explore or /user/susan in the client application to work, instead of returning a 404 error.

The caching recommendations in the Create React App documentation are cache the contents of the /static directory for one year, while serving all other files with caching disabled. This is implemented with the add_header and expires directives in the above configuration.

Production Deployment with Docker

Just to give you an idea of what effort goes into creating a production deployment of the React application plus its back end, in this section you'll learn how to create a complete Docker deployment.

Dockerizing the React Application

While it is possible to create a Docker container that uses the serve command discussed above to serve the React application, a better option is to create a more elaborate deployment based on Nginx, which as you've seen above, can be configured to serve static files, while at the same time act as a proxy to a back end.

Nginx has an official container image on DockerHub that is perfect to use as a base image. The "How to use this image" section of its documentation, shows an example Dockerfile for a derived image that incorporates a custom build directory. Using this as a guide, a Dockerfile for the React front end can be written as follows:

DockerFile: A simple Dockerfile for the React application

FROM nginx
COPY build/ /usr/share/nginx/html/

This Dockerfile uses the official Nginx image as a base, and adds the React production build files in the appropriate directory where Nginx looks for static files to serve.

Before building the container image, make sure you have an updated production build:

npm run build

Now, assuming you have Docker installed, you can build a container image for the application with the following command:

docker build -t react-microblog .

Once the image is built, you can launch a container based on this image with the docker run command:

docker run -p 8080:80 --name microblog -d react-microblog

The -p option tells Docker to map port 80 on the container, which is the port on which Nginx listens for requests, to port 8080 on the host computer. The --name option gives the container a friendly name, and the -d option runs the container in the background, giving you back control of the terminal prompt. The react-microblog at the end of the command is the name of the image to launch.

When this command executes, Docker will print the ID of the launched container to the terminal. You can now open http://localhost:8080 on your browser to access the React application running as a Docker container. Note that at this point the back end configuration is the same that you've used during development, so the same back end service will be used. Reusing a back end between development and production is not a good solution for a real-world project, but it means that until a separate production back end is started you can test the container by logging in with the same account you've been using before.

When you are done testing, you can stop and delete the container with this command:

docker rm -f microblog

The -f option tells Docker to force the removal of the container. This is necessary when removing a container that is running. The microblog name in this command is a reference to the value given for --name option in the docker run command. Alternatively, you can reference a container by its ID, which was printed by the docker run command.

Using a Custom Nginx Configuration

The official Nginx container image comes with its own default configuration, sufficient when all you need to do is serve some static files. The server section of this default configuration is stored in /etc/nginx/conf.d/default.conf inside the Nginx container image. To have a specialized configuration, this file can be replaced during the build of the derived image.

The following server configuration is based on the Nginx example configuration shown earlier in this chapter. Store it in a nginx.default.conf file in the React project.

nginx.default.conf: Custom Nginx configuration

server {
    listen       80;
    server_name  localhost;

    root   /usr/share/nginx/html;
    index  index.html;

    location / {
        try_files $uri $uri/ /index.html;
        add_header Cache-Control "no-cache";
    }

    location /static {
        expires 1y;
        add_header Cache-Control "public";
    }

    location /api {
        proxy_pass http://api:5000;
    }
}

This version of the configuration uses the most appropriate settings for the production build files of the React application. An /api location is also added to proxy requests to a back end server, which is assumed to be running on a host named api, on port 5000. In the next section, you'll add a second container for the back end, and this container will be associated with the api hostname.

This custom Nginx configuration can be added to the Dockerfile, replacing the stock configuration provided with the Nginx container image. Below you can see the updated Dockerfile for the application.

DockerFile: Custom Nginx configuration in the Dockerfile

FROM nginx
COPY build/ /usr/share/nginx/html/
COPY nginx.default.conf /etc/nginx/conf.d/default.conf

A Docker-Compose Configuration for Front and Back Ends

The new Nginx configuration serves the React production build files, and forwards all requests with a path starting with /api to a back end running at http://api:5000. While it is possible to manually start a second container for the back end, and then set up a private network that connects the two containers, this type of complex set up with multiple networked containers is better created using Docker Compose.

A Docker Compose configuration is written in a file named docker-compose.yml, and it describes a collection of containers running services that need to communicate with each other. The general structure of this file follows this pattern:

version: '3'
services:
  frontend:
    # <-- configuration of the frontend container
  api:
    # <-- configuration of the api container
# <-- other options

The frontend and api names in this example are arbitrary names given to the containers that are going to be part of the deployment. Docker Compose sets these names as hostnames in each of the containers. So for example, the frontend container will be able to proxy requests to the api container using api as hostname. And while this serves no purpose in this deployment, the api container can also reference the frontend container by its name.

Below you can see a complete Docker Compose configuration that includes a container for the React front end and another one for the Microblog API back end. The back end is given a volume to use as storage for its database.

docker-compose.yml: A Docker Compose configuration

version: '3.2'
services:
  frontend:
    build: .
    image: react-microblog
    ports:
      - "8080:80"
    restart: always
  api:
    build: ../microblog-api
    image: microblog-api
    volumes:
      - type: volume
        source: data
        target: /data
    env_file: .env.api
    environment:
      DATABASE_URL: sqlite:////data/db.sqlite
    restart: always
volumes:
  data:

The frontend container includes options that are similar to those used in the docker run command shown above. The build option configures the directory where a Dockerfile for the container image is located, which for this container is the current directory. The image option sets the name of the container image that will be built. The ports option configures the mapping of network ports between host and container. The restart option specifies that the container should be automatically restarted if it ever dies.

The definition of the api container is based on the docker-compose.yml file from the Microblog API project. This build option assumes that the repository for the Microblog API project is a sibling of the current directory. The image option sets the name of the API container image. The volumes section creates a mount for a data volume, where the database file will be stored. The benefit of putting the database in a mounted volume over having it in the container's own file system is that the container image can be updated without affecting the data. The env_file option configures a .env.api file with environment variables for the container (you will create this file soon). The environment section defines the DATABASE_URL environment variable explicitly, using the /data path where the volume was configured to be mounted. The restart option ensures that the container is restarted if it is ever interrupted, as with the frontend container. At the bottom there is a volumes top-level section that creates the volume named data, defined as a mount by the api container.

The api container does not have a ports section. Port mapping is only necessary for services that need to be public, as this is what ties a container port to an actual network port on the host computer. This is required in the front end container because that's where the public facing web server (Nginx) runs. The back end container does not need to be accessible to the outside world, so it does not need port mapping. The network that Docker Compose creates will allow the two containers to communicate privately.

Configuring the Docker Production Deployment

In previous chapters you have provided configuration options for the front and back ends in .env files. Using a single .env for configuration is often insufficient, because it does not allow options to change between development and production builds. Consider the REACT_APP_BASE_API_URL variable, which configures the base URL of the back end inside the React application. This variable needs to have a different value then the one currently in use when a production deployment is created.

The build support that was added to the project by Create React App has a set of rules for importing environment variables not only from .env but also from other files. In particular, any variables defined in files .env.development, .env.production and .env.test override those in .env for their specific environment.

To be able to redefine REACT_APP_BASE_API_URL, create a .env.production file in the root of your React project, and enter the following variable in it:

.env.production: Production configuration

REACT_APP_BASE_API_URL=

Why is this variable empty for a production build? Remember than Nginx is going to be the point of contact for both the front and back ends. The Nginx configuration will determine that a request is for the back end when the URL path starts with /api, but to the left of the URL path, URLs for front and back end are going to be the same. By setting the base URL to empty the front end is going to send requests that start with the path, which means that the browser will use the same base URL that was used to load the front end application.

The back end container also needs its configuration. You already have a valid configuration for it, that you've been using since Chapter 5. For this deployment, a copy of this configuration needs to be written in the React project, with name .env.api, as referenced in the docker-compose.yml file. An example configuration file for the API is shown below:

.env.api: Back end configuration

MAIL_SERVER=smtp.sendgrid.net
MAIL_PORT=587
MAIL_USE_TLS=true
MAIL_USERNAME=apikey
MAIL_PASSWORD=xxxxxxxxxxxxxxxxxx
MAIL_DEFAULT_SENDER=donotreply@microblog.example.com

This example configuration file uses SendGrid as email server. The MAIL_PASSWORD variable is shown with a placeholder value that you need to replace with your own SendGrid API key. If you've used a different email provider, you can keep using it for your production deployment.

Building the Docker Production Images

Before you build your production container images, make sure that you are in the react-microblog directory, where you have created the docker-compose.yml, Dockerfile, .env.production and .env.api files. The docker-compose.yml includes a relative path that points to the back end project directory, assuming it is a sibling directory. If you don't have the back end project cloned on your computer, clone it now with the following commands:

cd ..
git clone https://github.com/miguelgrinberg/microblog-api
cd react-microblog

Note how the cd commands are used to ensure that the Microblog API project is installed outside the React project, and under the same parent directory.

From the react-microblog directory, you can now build the front and back end container images with these two commands:

npm run build
docker-compose build

The first command creates a production build of the React application. This needs to be done before the Docker build, so that when Docker builds the container image for the front end, it copies the up-to-date production build files that are built with the production configuration you recently added.

The second command tells Docker Compose to go over all the containers declared in the services section of the docker-compose.yml file and build the container images for them.

Once the build is complete, you should be able to see them listed when you run the docker images command:

$ docker images
REPOSITORY                     TAG         IMAGE ID       CREATED         SIZE
microblog-api                  latest      93e6d0414110   3 minutes ago   170MB
react-microblog                latest      d9f09e24b646   2 minutes ago   143MB
nginx                          latest      12766a6745ee   2 weeks ago     142MB
python                         3.10-slim   9328aba7e797   4 months ago    123MB

In addition to the two images that were just built, Docker has copies of the images on which these two are based. You may see additional images in the list as well, depending on what other projects you use Docker for.

Starting the Production Deployment

After you have built your images, you can start a deployment using the docker-compose up command as follows:

$ docker-compose up -d
[+] Running 3/3
 - Network react-microblog_default       Created
 - Container react-microblog-api-1       Started
 - Container react-microblog-frontend-1  Started

The -d option runs the deployment as a background process, returning the prompt so that you can continue working on the terminal session. The output of the command shows that a private network has been created, and two containers were started.

The docker-compose.yml file defines a mapping between port 80 in the front end container and port 8080 in the host computer, so you now have the production version of the application listening on port 8080. You can check that the application is running by navigating to http://localhost:8080. If you want to connect from another device, replace localhost with the IP address or hostname of the computer running Docker.

This is a completely separate deployment that does not share data with the development back end that you've been using, so you will need to create a new account. The database on this deployment is also completely empty, so you will not see posts from other users until more accounts are created, and they start contributing content.

What happens next? This is pretty much it. If you follow the deployment steps on your production host, when you reach this point you have a fully working application. While this is outside the scope of this article, the Nginx configuration can be further expanded to use a proper domain name that is associated with an SSL certificate.

But you were probably testing the deployment on your development computer, so you need a way to remove these containers when you are done testing. The docker-compose down command achieves that:

$ docker-compose down
[+] Running 3/3
 - Container react-microblog-api-1       Removed
 - Container react-microblog-frontend-1  Removed
 - Network react-microblog_default       Removed

Simplifying the Deployment Process

As you've seen in the previous sections, creating a production deployment involves a few actions:

  • Run npm run build to generate an updated production build of the React project
  • Run docker-compose build to generate updated container images for front and back ends.
  • Run docker-compose up -d to start or restart the production containers.

Remembering these steps can be hard, especially if you don't need to deploy updates very often. To avoid errors in the process, it is best to create a wrapper script that performs all these steps.

You've seen earlier that it is possible to create a custom command for npm, so that technique can be applied here. In the package.json file, locate the scripts section and add an entry for a deploy command:

package.json: Custom deploy command

  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "deploy": "npm run build && docker-compose build && docker-compose up -d",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },

With this new command in place, you can start or update a deployment with a single command:

npm run deploy

Chapter Summary

  • Development builds have instrumentation that makes debugging easier, at the cost of reduced performance.
  • Production builds are optimized for performance, but are more difficult to debug.
  • Use the npm run build command to generate a production build.
  • Quickly test a production build with npx serve -s build.
  • To deploy your project to production, the contents of the build subdirectory must be served on a static file web server.
  • Nginx is an ideal production web server to combine serving the React application files and proxying requests to a back end.

Next Steps

Congratulations on completing this course! In this short section I want to give you some pointers on how to continue on your learning path.

If you are interested in learning more about front end development with React:

  • The React documentation should be a main destination, especially to stay up to date with changes and improvements introduced in new releases.
  • Now that you are familiar with React, you can explore other related projects, such as:
  • Next.js, a framework that extends React by providing server-side rendering and other benefits.
  • Gatsby, a framework that is optimized for creating static (or mostly static) sites.
  • React Native, a framework for building React applications that run natively on Android and iOS devices.

If you are interested in learning about back end development, the field is full of options. The beauty of front and back end using standard communication protocols such as HTTP, WebSocket or GraphQL is that the back end can be written in pretty much any programming language or web framework.

My personal experience is with back ends developed using the Python language and the Flask web framework. If you enjoyed working with the Microblog API back end and would like to learn more about it, here are some pointers:

If Python and/or Flask are not your thing, you should be able to find resources on writing APIs and services in your favorite programming language and web framework.

I would like to thank you for allowing me to share my experience writing React applications with you. I hope that this tutorial has given you the tools that you need to embark on your own front end projects with confidence.

Best of luck with your front end development journey!

1 comment

  • #1 Alex Goulielmos said 2023-01-03T13:01:02Z

    Fun fact: this docker-compose setup was the only way I could get this application to work on my home server. With a normal setup (without docker-compose), browsers complained about CORS issues, and the app didn't work.

    Thanks for this great series. Having finished your React book, I just ordered your two Flask books from Amazon.

Leave a Comment