The React Mega-Tutorial, Chapter 12: Production Builds
Posted by
on underYou 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:
- Introducing the React Mega-Tutorial
- Chapter 1: Modern JavaScript
- Chapter 2: Hello, React!
- Chapter 3: Working with Components
- Chapter 4: Routing and Page Navigation
- Chapter 5: Connecting to a Back End
- Chapter 6: Building an API Client
- Chapter 7: Forms and Validation
- Chapter 8: Authentication
- Chapter 9: Application Features
- Chapter 10: Memoization
- Chapter 11: Automated Testing
- Chapter 12: Production Builds (this article)
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:
- Netlify
- Cloudflare
- GitHub Pages
- Digital Ocean's App Platform
- Heroku
- AWS
- and many more.
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:
- You can learn by inspecting the Microblog API source code, which is open source and available on GitHub.
- My Flask Mega-Tutorial has a chapter on designing APIs.
- The Flask Web Development book, published by O'Reilly Media, also covers APIs.
- My personal blog has a variety of articles covering various aspects of web development with Python and Flask.
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 Alex Goulielmos said
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.
-
#2 S G said
A detail I missed on first read. All docker containers in created through a docker-compose are on a network. Each one gets the name of the service used in the docker-compose. In this deployment tutorial, the docker-compose uses the name
api
for the service and so the nginx configuration reuses that name forproxy_pass http://api:5000;